beiken1201 commited on
Commit
eaa8608
·
verified ·
1 Parent(s): faf1f6d

Unify BioMod names, flatten weight paths, and remove duplicate source directories

Browse files

Rename project-owned entrypoints, resource directories, classes and helpers to BioMod/biomod, and update imports, notebooks, documentation and command examples. Preserve old CLI flags as aliases and keep serialized state/configuration keys, raw gene-set IDs, result schemas and third-party interfaces compatible. Move the existing checkpoint/configuration pair to weight/BioMod.pth and weight/BioMod.pkl using their unchanged objects. Remove only the eight approved duplicate files from the source module-data directory and nested attention package; retain the shared copies. Raw datasets, legacy reference data copies and all weight contents remain unchanged. Validation: 90 Python 3 files, 65 notebook cells, 12 utility tests, 7 synthetic backbone comparisons and 10 classifier comparisons passed. No real model weights or datasets were deserialized, and no real-data training was run.

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +3 -7
  2. BioMod/README.md +57 -73
  3. BioMod/{performer_module.py → biomod.py} +25 -25
  4. BioMod/{module_dataset.py → biomod_dataset.py} +4 -4
  5. BioMod/cell_annotation/{module_data/module1_hallmarks.json → biomod_data/biomod1_hallmarks.json} +0 -0
  6. BioMod/cell_annotation/{module_data/module2_reactome_2022_filtered.json → biomod_data/biomod2_reactome_2022_filtered.json} +0 -0
  7. BioMod/cell_annotation/{module_data/load_module.py → biomod_data/load_biomod.py} +2 -2
  8. BioMod/cell_annotation/{performer_module_finetune.py → biomod_finetune.py} +75 -74
  9. BioMod/cell_annotation/cmd_finetune.md +4 -4
  10. BioMod/command.txt +4 -4
  11. BioMod/drug_combination/README.md +23 -23
  12. BioMod/drug_combination/{train_drug_combination_performer_module.py → train_drug_combination_biomod.py} +60 -60
  13. BioMod/drug_response/cmd.txt +4 -4
  14. BioMod/drug_response/{train_drug_performer_module.py → train_drug_biomod.py} +78 -78
  15. BioMod/environment.yml +2 -2
  16. BioMod/gene_perturbation/README.md +30 -30
  17. BioMod/gene_perturbation/gears_reduce_local.py +2 -2
  18. BioMod/gene_perturbation/{model_module.py → model_biomod.py} +31 -31
  19. BioMod/gene_perturbation/{train_module.py → train_biomod.py} +20 -20
  20. BioMod/interpretability/README.md +55 -55
  21. BioMod/interpretability/__init__.py +4 -4
  22. BioMod/interpretability/attention/README.md +12 -12
  23. BioMod/interpretability/attention/README_gene_attention.md +11 -11
  24. BioMod/interpretability/attention/{module_crosstalk.py → biomod_crosstalk.py} +21 -21
  25. BioMod/interpretability/attention/{module_gene_attention.py → biomod_gene_attention.py} +19 -19
  26. BioMod/interpretability/{module_geometry.py → biomod_geometry.py} +27 -27
  27. BioMod/interpretability/{module_importance → biomod_importance}/README.md +32 -32
  28. BioMod/interpretability/{module_importance → biomod_importance}/RESULTS.md +5 -5
  29. BioMod/interpretability/{module_importance → biomod_importance}/TODO.md +1 -1
  30. BioMod/interpretability/{module_importance → biomod_importance}/__init__.py +0 -0
  31. BioMod/interpretability/{module_importance/module_importance.py → biomod_importance/biomod_importance.py} +32 -32
  32. BioMod/interpretability/{module_importance/module_importance_publication.py → biomod_importance/biomod_importance_publication.py} +5 -5
  33. BioMod/interpretability/{module_importance → biomod_importance}/cross_class_similarity.py +0 -0
  34. BioMod/interpretability/{module_importance → biomod_importance}/family_ablation.py +8 -8
  35. BioMod/interpretability/{module_importance → biomod_importance}/family_importance_analysis.py +0 -0
  36. BioMod/interpretability/{module_importance → biomod_importance}/marker_gene_verification.py +8 -8
  37. BioMod/interpretability/{module_importance → biomod_importance}/masking_ablation.py +8 -8
  38. BioMod/interpretability/{module_importance → biomod_importance}/pub_utils.py +7 -7
  39. BioMod/interpretability/{module_importance → biomod_importance}/random_baseline.py +9 -9
  40. BioMod/interpretability/{module_importance → biomod_importance}/tcell_reactome_analysis.py +0 -0
  41. BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/DESIGN.md +33 -33
  42. BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/MANUSCRIPT_SECTION.md +0 -0
  43. BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/__init__.py +0 -0
  44. BioMod/interpretability/{module_text_alignment/module_text_alignment.py → biomod_text_alignment/biomod_text_alignment.py} +14 -14
  45. BioMod/interpretability/{module_text_alignment/module_text_alignment_extended.py → biomod_text_alignment/biomod_text_alignment_extended.py} +15 -15
  46. BioMod/interpretability/{module_text_alignment/module_text_alignment_publication.py → biomod_text_alignment/biomod_text_alignment_publication.py} +33 -33
  47. BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/fetch_reactome_descriptions.py +9 -9
  48. BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/reactome_descriptions.json +0 -0
  49. BioMod/interpretability/utils.py +5 -5
  50. BioMod/{models_performer_module.py → models_biomod.py} +38 -38
.gitignore CHANGED
@@ -31,12 +31,8 @@ __pycache__/
31
  .env
32
  *.local
33
 
34
- # Dated release checkpoints are intentionally versioned on Hugging Face.
35
  !weight/
36
  weight/*
37
- !weight/0629model/
38
- !weight/0629model/*.pkl
39
- !weight/0629model/*.pth
40
- !weight/0810model/
41
- !weight/0810model/*.pkl
42
- !weight/0810model/*.pth
 
31
  .env
32
  *.local
33
 
34
+ # BioMod model weights and their matching configuration are versioned on Hugging Face.
35
  !weight/
36
  weight/*
37
+ !weight/BioMod.pkl
38
+ !weight/BioMod.pth
 
 
 
 
BioMod/README.md CHANGED
@@ -1,7 +1,7 @@
1
- # BioMod / PerformerModule
2
 
3
  Source code and tutorial notebooks for BioMod single-cell pretraining and
4
- downstream experiments using PerformerModule representations.
5
 
6
  This source directory contains the pretraining entry point and shared training
7
  utilities, alongside downstream task runners, tutorial notebooks, environment
@@ -29,67 +29,63 @@ from these repository trees.
29
  ```text
30
  BioMod/ # Task code: training, evaluation, interpretability and tutorials
31
  ├── cell_annotation/ # Cell-type annotation and model fine-tuning
32
- │ ├── module_data/ # Hallmark and Reactome modules with loading utilities
33
- │ │ ├── load_module.py
34
- │ │ ├── module1_hallmarks.json
35
- │ │ └── module2_reactome_2022_filtered.json
36
  │ ├── cmd_finetune.md
37
- │ └── performer_module_finetune.py
38
  ├── drug_combination/ # Drug-combination synergy prediction
39
  │ ├── README.md
40
- │ └── train_drug_combination_performer_module.py
41
  ├── drug_response/ # Single-drug response prediction and training
42
  │ ├── cmd.txt
43
- │ └── train_drug_performer_module.py
44
  ├── gene_perturbation/ # Perturbation response prediction: processing, training and inference
45
  │ ├── data_utils_local.py
46
  │ ├── gears_reduce_local.py
47
  │ ├── inference_local.py
48
- │ ├── model_module.py
49
  │ ├── pertdata_local.py
50
  │ ├── README.md
51
  │ ├── scfm_utils_local.py
52
- │ ├── train_module.py
53
  │ └── utils.py
54
  ├── interpretability/ # Module representations and biological interpretability
55
  │ ├── attention/ # Module crosstalk and within-module gene attention
56
  │ │ ├── __init__.py
57
- │ │ ├── module_crosstalk.py
58
- │ │ ├── module_gene_attention.py
59
  │ │ ├── README.md
60
  │ │ └── README_gene_attention.md
61
- │ ├── module_importance/ # Module importance, ablation studies and marker-gene validation
62
  │ │ ├── __init__.py
63
  │ │ ├── cross_class_similarity.py
64
  │ │ ├── family_ablation.py
65
  │ │ ├── family_importance_analysis.py
66
  │ │ ├── marker_gene_verification.py
67
  │ │ ├── masking_ablation.py
68
- │ │ ├── module_importance.py
69
- │ │ ├── module_importance_publication.py
70
  │ │ ├── pub_utils.py
71
  │ │ ├── random_baseline.py
72
  │ │ ├── README.md
73
  │ │ ├── RESULTS.md
74
  │ │ ├── tcell_reactome_analysis.py
75
  │ │ └── TODO.md
76
- │ ├── module_text_alignment/ # Semantic alignment of module representations and pathway text
77
  │ │ ├── __init__.py
78
  │ │ ├── DESIGN.md
79
  │ │ ├── fetch_reactome_descriptions.py
80
  │ │ ├── MANUSCRIPT_SECTION.md
81
- │ │ ├── module_text_alignment.py
82
- │ │ ├── module_text_alignment_extended.py
83
- │ │ ├── module_text_alignment_publication.py
84
  │ │ └── reactome_descriptions.json
85
  │ ├── __init__.py
86
- │ ├── module_geometry.py
87
  │ ├── README.md
88
  │ └── utils.py
89
- ├── module_data_all/ # Module definitions, merging and uncovered-gene tools
90
- │ ├── create_uncovered_modules.py
91
- │ ├── merge_modules.py
92
- │ └── merged_modules.json
93
  ├── tests/ # CPU tests for shared training utilities
94
  │ └── test_training_utils.py
95
  ├── toturials/ # Jupyter tutorials for five downstream tasks
@@ -103,7 +99,7 @@ BioMod/ # Task code: training,
103
  │ │ ├── cmd.txt
104
  │ │ ├── make_low_depth_h5ad.py
105
  │ │ └── run_zero_shot_low_depth.py
106
- │ ├── performer_module_zero_shot.py
107
  │ ├── README.md
108
  │ └── visualize_results.py
109
  ├── .env.example
@@ -111,9 +107,9 @@ BioMod/ # Task code: training,
111
  ├── .gitignore
112
  ├── command.txt
113
  ├── environment.yml
114
- ├── models_performer_module.py
115
- ├── module_dataset.py
116
- ├── performer_module.py
117
  ├── README.md
118
  ├── requirements.txt
119
  └── training_utils.py
@@ -130,7 +126,7 @@ environment:
130
  ```bash
131
  cd BioMod
132
  conda env create -f environment.yml
133
- conda activate performer_module
134
  ```
135
 
136
  Or install Python packages into an existing CUDA/PyTorch environment:
@@ -143,7 +139,7 @@ From this source directory, activate the supplied environment and start Jupyter
143
  with the tutorial directory as its working directory:
144
 
145
  ```bash
146
- conda activate performer_module
147
  cd toturials
148
  jupyter lab
149
  ```
@@ -161,26 +157,26 @@ imports after installing the dependencies:
161
 
162
  ```bash
163
  python -B -m unittest discover -s tests -p test_training_utils.py -v
164
- python -B performer_module.py --help
165
  ```
166
 
167
  The utility tests use synthetic tensors and temporary checkpoints; they do not
168
  load the supplied pretrained weights. Passing these checks does not validate a
169
  full training run. The pretraining entry point requires CUDA and an NCCL
170
  process group. Supply `--data_path` explicitly: the pretraining corpus is not
171
- bundled. Configure module inputs with `--module_data_dir` or `--module_data_path`.
172
  Generated checkpoints default to `tutorial_runs/pretraining/` under the outer
173
  repository root, with periodic latest checkpoints under its `latest/` subdirectory.
174
 
175
  ## Shared Assets
176
 
177
  The BioMod model weights and matching configuration are stored in
178
- the outer repository's `weight/0810model/`. Use the supplied `.pth` and `.pkl`
179
  together.
180
 
181
- Tutorials expect a pretrained BioMod/PerformerModule checkpoint and shared
182
  gene assets. For the shell examples below, set `BIOMOD_ROOT` to the directory containing
183
- `weight/0810model/` and `model_dependencies/` (the outer `BioMod/` project directory).
184
 
185
  ```bash
186
  # Run this from the inner BioMod/ source directory.
@@ -190,45 +186,33 @@ export BIOMOD_ROOT="$(cd .. && pwd)"
190
  Current local asset files:
191
 
192
  ```text
193
- $BIOMOD_ROOT/weight/0810model/ # BioMod model weights and matching configuration
194
- ├── mae_tost_70M_75mask_ec2.pkl
195
- └── mae_tost_70M_75mask_ec2_epoch0.pth
196
 
197
  $BIOMOD_ROOT/model_dependencies/ # Shared gene mappings, gene lists, embeddings and model components
198
- ├── module_data_all/ # Shared module definitions and construction tools
199
- │ ├── create_uncovered_modules.py
200
- │ ├── merge_modules.py
201
- │ └── merged_modules.json
202
- ├── performer_pytorch_cont/ # Performer attention and model components
203
- │ ├── performer_pytorch_cont/ # Nested package containing copies of the model component source files
204
- │ │ ├── __init__.py
205
- │ │ ├── ding_models.py
206
- │ │ ├── performer_pytorch.py
207
- │ │ ├── performer_pytorch_old.py
208
- │ │ └── reversible.py
209
  │ ├── __init__.py
210
  │ ├── ding_models.py
211
- │ ├── performer_pytorch.py
212
- │ ├── performer_pytorch_old.py
213
  │ └── reversible.py
214
  ├── gene2ens.csv
215
  ├── selected_gene2vec_27k.npy
216
  └── selected_genes_27k.txt
217
  ```
218
 
219
- Project-local module definitions are kept in:
220
-
221
- ```text
222
- BioMod/module_data_all/ # Module definitions, merging and uncovered-gene tools
223
- ├── create_uncovered_modules.py
224
- ├── merge_modules.py
225
- └── merged_modules.json
226
- ```
227
 
228
  ## BioMod model weights
229
 
230
- Use `mae_tost_70M_75mask_ec2_epoch0.pth` with
231
- `mae_tost_70M_75mask_ec2.pkl` from the shared-assets directory above. The supplied
232
  configuration specifies a 2-layer encoder, a 27,874-gene axis, and 660 module
233
  definitions. Keep this configuration paired with the weights so that task
234
  runners can restore the corresponding architecture settings.
@@ -240,8 +224,8 @@ runners can restore the corresponding architecture settings.
240
  | Cell-type annotation | `toturials/cell_annotation.ipynb` | task script under `cell_annotation/` | metrics, classification report, confusion matrix |
241
  | Zero-shot batch integration | `toturials/zero_shot_batch_integration.ipynb` | `zero_shot_batch/` runner | UMAP PDF/PNG, avgBatch, avgBio |
242
  | Gene perturbation | `toturials/gene_perturbation.ipynb` | `gene_perturbation/` runner | selected model, configuration, parameters, and training log |
243
- | Drug response prediction | `toturials/drug_response_prediction.ipynb` | `drug_response/train_drug_performer_module.py` | scatter, residuals, ROC, per-drug and per-cancer tables |
244
- | Drug-combination synergy | `toturials/drug_combination.ipynb` | `drug_combination/train_drug_combination_performer_module.py` | label balance, loss, validation metrics, and ROC curves |
245
 
246
  Each notebook derives paths from `PROJECT_ROOT = Path('../..').resolve()` when
247
  Jupyter runs from `BioMod/toturials/`. From the repository root, change this to
@@ -296,12 +280,12 @@ Run these commands from the inner `BioMod/` source directory after setting
296
  `BIOMOD_ROOT` as shown above. Drug response prediction with the BioMod model weights:
297
 
298
  ```bash
299
- python drug_response/train_drug_performer_module.py \
300
  --gpu_id 0 \
301
  --data_path "$BIOMOD_ROOT/data/downstream/final_data/single_drug_response/data" \
302
- --checkpoint_path "$BIOMOD_ROOT/weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth" \
303
- --hyper_params_path "$BIOMOD_ROOT/weight/0810model/mae_tost_70M_75mask_ec2.pkl" \
304
- --module_data_dir "$BIOMOD_ROOT/model_dependencies/module_data_all" \
305
  --gene_list_path "$BIOMOD_ROOT/model_dependencies/selected_genes_27k.txt" \
306
  --gene_mapping_path "$BIOMOD_ROOT/model_dependencies/gene2ens.csv" \
307
  --gene2vec_path "$BIOMOD_ROOT/model_dependencies/selected_gene2vec_27k.npy" \
@@ -315,10 +299,10 @@ python drug_response/train_drug_performer_module.py \
315
  Drug-combination synergy prediction:
316
 
317
  ```bash
318
- python drug_combination/train_drug_combination_performer_module.py \
319
- --checkpoint_path "$BIOMOD_ROOT/weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth" \
320
- --hyper_params_path "$BIOMOD_ROOT/weight/0810model/mae_tost_70M_75mask_ec2.pkl" \
321
- --module_data_dir "$BIOMOD_ROOT/model_dependencies/module_data_all" \
322
  --gene_list_path "$BIOMOD_ROOT/model_dependencies/selected_genes_27k.txt" \
323
  --gene_mapping_path "$BIOMOD_ROOT/model_dependencies/gene2ens.csv" \
324
  --gene2vec_path "$BIOMOD_ROOT/model_dependencies/selected_gene2vec_27k.npy" \
@@ -376,7 +360,7 @@ files and shows which entries are missing.
376
 
377
  Check `BIOMOD_ROOT` for shell commands or `PROJECT_ROOT` in a notebook, and verify that the BioMod model weights, matching
378
  `.pkl` configuration, gene2vec file, gene list, gene mapping, and
379
- `performer_pytorch_cont/` directory exist at the paths listed under Shared Assets.
380
 
381
  ### No space left on device
382
 
 
1
+ # BioMod
2
 
3
  Source code and tutorial notebooks for BioMod single-cell pretraining and
4
+ downstream experiments using BioMod representations.
5
 
6
  This source directory contains the pretraining entry point and shared training
7
  utilities, alongside downstream task runners, tutorial notebooks, environment
 
29
  ```text
30
  BioMod/ # Task code: training, evaluation, interpretability and tutorials
31
  ├── cell_annotation/ # Cell-type annotation and model fine-tuning
32
+ │ ├── biomod_data/ # Hallmark and Reactome modules with loading utilities
33
+ │ │ ├── load_biomod.py
34
+ │ │ ├── biomod1_hallmarks.json
35
+ │ │ └── biomod2_reactome_2022_filtered.json
36
  │ ├── cmd_finetune.md
37
+ │ └── biomod_finetune.py
38
  ├── drug_combination/ # Drug-combination synergy prediction
39
  │ ├── README.md
40
+ │ └── train_drug_combination_biomod.py
41
  ├── drug_response/ # Single-drug response prediction and training
42
  │ ├── cmd.txt
43
+ │ └── train_drug_biomod.py
44
  ├── gene_perturbation/ # Perturbation response prediction: processing, training and inference
45
  │ ├── data_utils_local.py
46
  │ ├── gears_reduce_local.py
47
  │ ├── inference_local.py
48
+ │ ├── model_biomod.py
49
  │ ├── pertdata_local.py
50
  │ ├── README.md
51
  │ ├── scfm_utils_local.py
52
+ │ ├── train_biomod.py
53
  │ └── utils.py
54
  ├── interpretability/ # Module representations and biological interpretability
55
  │ ├── attention/ # Module crosstalk and within-module gene attention
56
  │ │ ├── __init__.py
57
+ │ │ ├── biomod_crosstalk.py
58
+ │ │ ├── biomod_gene_attention.py
59
  │ │ ├── README.md
60
  │ │ └── README_gene_attention.md
61
+ │ ├── biomod_importance/ # Module importance, ablation studies and marker-gene validation
62
  │ │ ├── __init__.py
63
  │ │ ├── cross_class_similarity.py
64
  │ │ ├── family_ablation.py
65
  │ │ ├── family_importance_analysis.py
66
  │ │ ├── marker_gene_verification.py
67
  │ │ ├── masking_ablation.py
68
+ │ │ ├── biomod_importance.py
69
+ │ │ ├── biomod_importance_publication.py
70
  │ │ ├── pub_utils.py
71
  │ │ ├── random_baseline.py
72
  │ │ ├── README.md
73
  │ │ ├── RESULTS.md
74
  │ │ ├── tcell_reactome_analysis.py
75
  │ │ └── TODO.md
76
+ │ ├── biomod_text_alignment/ # Semantic alignment of module representations and pathway text
77
  │ │ ├── __init__.py
78
  │ │ ├── DESIGN.md
79
  │ │ ├── fetch_reactome_descriptions.py
80
  │ │ ├── MANUSCRIPT_SECTION.md
81
+ │ │ ├── biomod_text_alignment.py
82
+ │ │ ├── biomod_text_alignment_extended.py
83
+ │ │ ├── biomod_text_alignment_publication.py
84
  │ │ └── reactome_descriptions.json
85
  │ ├── __init__.py
86
+ │ ├── biomod_geometry.py
87
  │ ├── README.md
88
  │ └── utils.py
 
 
 
 
89
  ├── tests/ # CPU tests for shared training utilities
90
  │ └── test_training_utils.py
91
  ├── toturials/ # Jupyter tutorials for five downstream tasks
 
99
  │ │ ├── cmd.txt
100
  │ │ ├── make_low_depth_h5ad.py
101
  │ │ └── run_zero_shot_low_depth.py
102
+ │ ├── biomod_zero_shot.py
103
  │ ├── README.md
104
  │ └── visualize_results.py
105
  ├── .env.example
 
107
  ├── .gitignore
108
  ├── command.txt
109
  ├── environment.yml
110
+ ├── models_biomod.py
111
+ ├── biomod_dataset.py
112
+ ├── biomod.py
113
  ├── README.md
114
  ├── requirements.txt
115
  └── training_utils.py
 
126
  ```bash
127
  cd BioMod
128
  conda env create -f environment.yml
129
+ conda activate biomod
130
  ```
131
 
132
  Or install Python packages into an existing CUDA/PyTorch environment:
 
139
  with the tutorial directory as its working directory:
140
 
141
  ```bash
142
+ conda activate biomod
143
  cd toturials
144
  jupyter lab
145
  ```
 
157
 
158
  ```bash
159
  python -B -m unittest discover -s tests -p test_training_utils.py -v
160
+ python -B biomod.py --help
161
  ```
162
 
163
  The utility tests use synthetic tensors and temporary checkpoints; they do not
164
  load the supplied pretrained weights. Passing these checks does not validate a
165
  full training run. The pretraining entry point requires CUDA and an NCCL
166
  process group. Supply `--data_path` explicitly: the pretraining corpus is not
167
+ bundled. Configure module inputs with `--biomod_data_dir` or `--biomod_data_path`.
168
  Generated checkpoints default to `tutorial_runs/pretraining/` under the outer
169
  repository root, with periodic latest checkpoints under its `latest/` subdirectory.
170
 
171
  ## Shared Assets
172
 
173
  The BioMod model weights and matching configuration are stored in
174
+ the outer repository's `weight/`. Use the supplied `.pth` and `.pkl`
175
  together.
176
 
177
+ Tutorials expect a pretrained BioMod checkpoint and shared
178
  gene assets. For the shell examples below, set `BIOMOD_ROOT` to the directory containing
179
+ `weight/` and `model_dependencies/` (the outer `BioMod/` project directory).
180
 
181
  ```bash
182
  # Run this from the inner BioMod/ source directory.
 
186
  Current local asset files:
187
 
188
  ```text
189
+ $BIOMOD_ROOT/weight/ # BioMod model weights and matching configuration
190
+ ├── BioMod.pkl
191
+ └── BioMod.pth
192
 
193
  $BIOMOD_ROOT/model_dependencies/ # Shared gene mappings, gene lists, embeddings and model components
194
+ ├── biomod_data_all/ # Shared module definitions and construction tools
195
+ │ ├── create_uncovered_biomods.py
196
+ │ ├── merge_biomods.py
197
+ │ └── merged_biomods.json
198
+ ├── pytorch_cont/ # Attention and model components
 
 
 
 
 
 
199
  │ ├── __init__.py
200
  │ ├── ding_models.py
201
+ │ ├── pytorch.py
202
+ │ ├── pytorch_old.py
203
  │ └── reversible.py
204
  ├── gene2ens.csv
205
  ├── selected_gene2vec_27k.npy
206
  └── selected_genes_27k.txt
207
  ```
208
 
209
+ Module definitions and construction tools are shared through
210
+ `$BIOMOD_ROOT/model_dependencies/biomod_data_all/`.
 
 
 
 
 
 
211
 
212
  ## BioMod model weights
213
 
214
+ Use `BioMod.pth` with
215
+ `BioMod.pkl` from the shared-assets directory above. The supplied
216
  configuration specifies a 2-layer encoder, a 27,874-gene axis, and 660 module
217
  definitions. Keep this configuration paired with the weights so that task
218
  runners can restore the corresponding architecture settings.
 
224
  | Cell-type annotation | `toturials/cell_annotation.ipynb` | task script under `cell_annotation/` | metrics, classification report, confusion matrix |
225
  | Zero-shot batch integration | `toturials/zero_shot_batch_integration.ipynb` | `zero_shot_batch/` runner | UMAP PDF/PNG, avgBatch, avgBio |
226
  | Gene perturbation | `toturials/gene_perturbation.ipynb` | `gene_perturbation/` runner | selected model, configuration, parameters, and training log |
227
+ | Drug response prediction | `toturials/drug_response_prediction.ipynb` | `drug_response/train_drug_biomod.py` | scatter, residuals, ROC, per-drug and per-cancer tables |
228
+ | Drug-combination synergy | `toturials/drug_combination.ipynb` | `drug_combination/train_drug_combination_biomod.py` | label balance, loss, validation metrics, and ROC curves |
229
 
230
  Each notebook derives paths from `PROJECT_ROOT = Path('../..').resolve()` when
231
  Jupyter runs from `BioMod/toturials/`. From the repository root, change this to
 
280
  `BIOMOD_ROOT` as shown above. Drug response prediction with the BioMod model weights:
281
 
282
  ```bash
283
+ python drug_response/train_drug_biomod.py \
284
  --gpu_id 0 \
285
  --data_path "$BIOMOD_ROOT/data/downstream/final_data/single_drug_response/data" \
286
+ --checkpoint_path "$BIOMOD_ROOT/weight/BioMod.pth" \
287
+ --hyper_params_path "$BIOMOD_ROOT/weight/BioMod.pkl" \
288
+ --biomod_data_dir "$BIOMOD_ROOT/model_dependencies/biomod_data_all" \
289
  --gene_list_path "$BIOMOD_ROOT/model_dependencies/selected_genes_27k.txt" \
290
  --gene_mapping_path "$BIOMOD_ROOT/model_dependencies/gene2ens.csv" \
291
  --gene2vec_path "$BIOMOD_ROOT/model_dependencies/selected_gene2vec_27k.npy" \
 
299
  Drug-combination synergy prediction:
300
 
301
  ```bash
302
+ python drug_combination/train_drug_combination_biomod.py \
303
+ --checkpoint_path "$BIOMOD_ROOT/weight/BioMod.pth" \
304
+ --hyper_params_path "$BIOMOD_ROOT/weight/BioMod.pkl" \
305
+ --biomod_data_dir "$BIOMOD_ROOT/model_dependencies/biomod_data_all" \
306
  --gene_list_path "$BIOMOD_ROOT/model_dependencies/selected_genes_27k.txt" \
307
  --gene_mapping_path "$BIOMOD_ROOT/model_dependencies/gene2ens.csv" \
308
  --gene2vec_path "$BIOMOD_ROOT/model_dependencies/selected_gene2vec_27k.npy" \
 
360
 
361
  Check `BIOMOD_ROOT` for shell commands or `PROJECT_ROOT` in a notebook, and verify that the BioMod model weights, matching
362
  `.pkl` configuration, gene2vec file, gene list, gene mapping, and
363
+ `pytorch_cont/` directory exist at the paths listed under Shared Assets.
364
 
365
  ### No space left on device
366
 
BioMod/{performer_module.py → biomod.py} RENAMED
@@ -2,14 +2,14 @@
2
 
3
  """
4
  ================================================================================
5
- Pre-training script for Encoder-only Performer model
6
  ================================================================================
7
- Refer to MAE's data input and masking methods, but use Performer as the encoder
8
  Main features:
9
  1. Use module data input (refer to MAE)
10
  2. Mask the module (refer to MAE’s random_masking)
11
  3. Use a configurable reconstruction decoder or direct prediction heads
12
- 4. Use Performer as encoder (instead of standard Transformer Block)
13
 
14
  ================================================================================
15
  [Data flow example - taking default parameters as an example]
@@ -25,11 +25,11 @@
25
  - 3 = batch_size
26
  - 27874 = num_genes (gene expression value)
27
 
28
- Step 3: Model forward propagation (see models_performer_module.py for details)
29
  ├─> 3.1 Flat to Module: (3, 27874) → (3, num_modules, module_avg_size)
30
  ├─> 3.2 Module Embedding: → (3, num_modules, 1024)
31
  ├─> 3.3 Random Masking: mask out 75% of modules and keep 25%
32
- ├─> 3.4 Performer Encoder: Process visible modules
33
  ├─> 3.5 Reconstruction: Predict the expression value of the masked module
34
  └─> 3.6 Calculate Loss: Calculate MSE only on the masked module
35
 
@@ -63,11 +63,11 @@
63
  ASSET_ROOT = PROJECT_ROOT / 'model_dependencies'
64
  if str(SOURCE_DIR) not in sys.path:
65
  sys.path.insert(0, str(SOURCE_DIR))
66
- from models_performer_module import (
67
- PerformerEncoderOnly_Module,
68
- sanitize_performer_module_config,
69
  )
70
- from module_dataset import ModuleSCDataset, load_module_data
71
  from training_utils import (
72
  CosineAnnealingWarmupRestarts,
73
  SequentialDistributedSampler,
@@ -125,7 +125,7 @@
125
  parser.add_argument("--dec_heads", type=int, default=8, help='Number of decoder attention heads (valid when performer/cross_attn)')
126
  parser.add_argument("--dec_dim_head", type=int, default=64, help='Performer decoder dim per head (ignored when cross_attn)')
127
  parser.add_argument("--ckpt_dir", type=str, default=str(PROJECT_ROOT / 'tutorial_runs' / 'pretraining'), help='Directory for generated checkpoints; user relative paths use the current working directory.')
128
- parser.add_argument("--model_name", type=str, default='performer_module_27k', help='Pretrained model name.')
129
  parser.add_argument("--data_path", type=str, required=True,
130
  help='Pretraining input: a directory for npz/txt or a file for h5/h5ad. Supply your own pretraining data; it is not bundled.')
131
  parser.add_argument("--data_format", type=str, default="npz", choices=["npz", "h5", "h5ad", "txt"],
@@ -133,8 +133,8 @@
133
  parser.add_argument("--h5_dataset_key", type=str, default="expression",
134
  help='The data set key name of the expression matrix in h5 format. The default is to read h5[\"expression\"]')
135
  # Module related parameters
136
- parser.add_argument("--module_data_path", type=str, default=None, help='Path to module JSON file (single file).')
137
- parser.add_argument("--module_data_dir", type=str, default=None, help='Path to directory containing module JSON files.')
138
  parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / 'gene2ens.csv'), help='Path to gene symbol to Ensembl ID mapping CSV.')
139
  parser.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / 'selected_gene2vec_27k.npy'))
140
  parser.add_argument("--fusion_method", type=str, default="multiply",
@@ -167,8 +167,8 @@
167
  parser.add_argument("--load_opt_and_sched", action='store_true')
168
  parser.add_argument("--lr_step_within_epoch", action='store_true',
169
  help='Update the learning rate according to the optimizer update step; the cycle length is updates_per_epoch (gradually decreasing in each epoch)')
170
- parser.add_argument("--shuffle_module_genes", action='store_true',
171
- help='Whether to disrupt the gene allocation within the module (used for comparative experiments to verify the effectiveness of module division)')
172
  parser.add_argument("--shuffle_seed", type=int, default=2021,
173
  help='Random seed used when scrambling module genes (downstream tasks need to use the same value)')
174
  # Gene2vec ablation experiment parameters
@@ -278,7 +278,7 @@ def restore_model_args_from_config(args, config):
278
  args.model_config_path,
279
  )
280
  with open(resolved_model_config_path, "rb") as handle:
281
- resume_model_config = sanitize_performer_module_config(
282
  pickle.load(handle),
283
  source=resolved_model_config_path,
284
  log_fn=print,
@@ -325,11 +325,11 @@ def restore_model_args_from_config(args, config):
325
 
326
  # WandB resume logic
327
  wandb_init_kwargs = {
328
- "project": "pretrain_performer_module",
329
  "name": args.model_name,
330
  "entity": "icml_2023_mlb",
331
  "config": {
332
- "architecture": "performer_module",
333
  "gene_num": args.gene_num,
334
  "cell_num": args.cell_num,
335
  "load model": args.model_path,
@@ -490,7 +490,7 @@ def resolve_total_cells_for_split():
490
  if is_master:
491
  print(f"Using {len(module_sets)} modules restored from model config PKL")
492
  else:
493
- module_sets = load_module_data(
494
  module_data_path=args.module_data_path,
495
  module_data_dir=args.module_data_dir,
496
  gene_list_path=args.gene_list_path,
@@ -616,14 +616,14 @@ def resolve_total_cells_for_split():
616
  )
617
 
618
  # Step 3.3: Instantiate the model
619
- # PerformerEncoderOnly_Module model process:
620
  # Input: (batch_size, num_genes) = (3, 27874)
621
  # ├─> Module division: (batch_size, num_modules, module_avg_size) ≈ (3, 500, 55)
622
  # ├─> Module Embedding: (batch_size, num_modules, base_dim) = (3, 500, 1024)
623
  # ├─> Random Masking (75%): Keep about 125 visible modules
624
- # ├─> Performer Encoder: Process visible modules
625
  # └─> Reconstruction prediction: Output the reconstruction value of the masked module
626
- model = PerformerEncoderOnly_Module(**model_params)
627
 
628
  def configure_trainable_modules(model_obj):
629
  for param in model_obj.parameters():
@@ -721,8 +721,8 @@ def save_initialized_checkpoint_and_exit(model_obj):
721
  # Step 2.1/2.2 has been completed previously
722
 
723
  # Step 2.3: Create training and validation datasets
724
- # ModuleSCDataset directly returns the gene expression value, and the module conversion is completed inside the model.
725
- train_dataset = ModuleSCDataset(
726
  data_path=args.data_path,
727
  module_sets=module_sets, # Pass module_sets to maintain compatibility, but no longer used in __getitem__
728
  data=None,
@@ -735,7 +735,7 @@ def save_initialized_checkpoint_and_exit(model_obj):
735
  cell_num=None,
736
  verbose=is_master
737
  )
738
- val_dataset = ModuleSCDataset(
739
  data_path=args.data_path,
740
  module_sets=module_sets,
741
  data=None,
@@ -880,7 +880,7 @@ def any_rank_true(local_flag: bool) -> bool:
880
  # 1. Flat to Module: (3, 27874) -> (3, num_modules, module_avg_size)
881
  # 2. Module Embedding: -> (3, num_modules, 1024)
882
  # 3. Random Masking: mask out 75% of modules
883
- # 4. Performer Encoder: Process visible modules
884
  # 5. Reconstruction: Predict the masked module
885
  # Output: loss=MSE on masked module, preds=reconstructed value, mask=mask mark
886
  loss, preds, mask = model(data)
 
2
 
3
  """
4
  ================================================================================
5
+ Pre-training script for encoder-only BioMod model
6
  ================================================================================
7
+ Refer to MAE's data input and masking methods, but use the BioMod attention encoder
8
  Main features:
9
  1. Use module data input (refer to MAE)
10
  2. Mask the module (refer to MAE’s random_masking)
11
  3. Use a configurable reconstruction decoder or direct prediction heads
12
+ 4. Use the BioMod attention encoder
13
 
14
  ================================================================================
15
  [Data flow example - taking default parameters as an example]
 
25
  - 3 = batch_size
26
  - 27874 = num_genes (gene expression value)
27
 
28
+ Step 3: Model forward propagation (see models_biomod.py for details)
29
  ├─> 3.1 Flat to Module: (3, 27874) → (3, num_modules, module_avg_size)
30
  ├─> 3.2 Module Embedding: → (3, num_modules, 1024)
31
  ├─> 3.3 Random Masking: mask out 75% of modules and keep 25%
32
+ ├─> 3.4 BioMod encoder: Process visible modules
33
  ├─> 3.5 Reconstruction: Predict the expression value of the masked module
34
  └─> 3.6 Calculate Loss: Calculate MSE only on the masked module
35
 
 
63
  ASSET_ROOT = PROJECT_ROOT / 'model_dependencies'
64
  if str(SOURCE_DIR) not in sys.path:
65
  sys.path.insert(0, str(SOURCE_DIR))
66
+ from models_biomod import (
67
+ BioMod,
68
+ sanitize_biomod_config,
69
  )
70
+ from biomod_dataset import BioModSCDataset, load_biomod_data
71
  from training_utils import (
72
  CosineAnnealingWarmupRestarts,
73
  SequentialDistributedSampler,
 
125
  parser.add_argument("--dec_heads", type=int, default=8, help='Number of decoder attention heads (valid when performer/cross_attn)')
126
  parser.add_argument("--dec_dim_head", type=int, default=64, help='Performer decoder dim per head (ignored when cross_attn)')
127
  parser.add_argument("--ckpt_dir", type=str, default=str(PROJECT_ROOT / 'tutorial_runs' / 'pretraining'), help='Directory for generated checkpoints; user relative paths use the current working directory.')
128
+ parser.add_argument("--model_name", type=str, default='biomod_27k', help='Pretrained model name.')
129
  parser.add_argument("--data_path", type=str, required=True,
130
  help='Pretraining input: a directory for npz/txt or a file for h5/h5ad. Supply your own pretraining data; it is not bundled.')
131
  parser.add_argument("--data_format", type=str, default="npz", choices=["npz", "h5", "h5ad", "txt"],
 
133
  parser.add_argument("--h5_dataset_key", type=str, default="expression",
134
  help='The data set key name of the expression matrix in h5 format. The default is to read h5[\"expression\"]')
135
  # Module related parameters
136
+ parser.add_argument("--biomod_data_path", "--module_data_path", type=str, default=None, help='Path to module JSON file (single file).', dest='module_data_path')
137
+ parser.add_argument("--biomod_data_dir", "--module_data_dir", type=str, default=None, help='Path to directory containing module JSON files.', dest='module_data_dir')
138
  parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / 'gene2ens.csv'), help='Path to gene symbol to Ensembl ID mapping CSV.')
139
  parser.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / 'selected_gene2vec_27k.npy'))
140
  parser.add_argument("--fusion_method", type=str, default="multiply",
 
167
  parser.add_argument("--load_opt_and_sched", action='store_true')
168
  parser.add_argument("--lr_step_within_epoch", action='store_true',
169
  help='Update the learning rate according to the optimizer update step; the cycle length is updates_per_epoch (gradually decreasing in each epoch)')
170
+ parser.add_argument("--shuffle_biomod_genes", "--shuffle_module_genes", action='store_true',
171
+ help='Whether to disrupt the gene allocation within the module (used for comparative experiments to verify the effectiveness of module division)', dest='shuffle_module_genes')
172
  parser.add_argument("--shuffle_seed", type=int, default=2021,
173
  help='Random seed used when scrambling module genes (downstream tasks need to use the same value)')
174
  # Gene2vec ablation experiment parameters
 
278
  args.model_config_path,
279
  )
280
  with open(resolved_model_config_path, "rb") as handle:
281
+ resume_model_config = sanitize_biomod_config(
282
  pickle.load(handle),
283
  source=resolved_model_config_path,
284
  log_fn=print,
 
325
 
326
  # WandB resume logic
327
  wandb_init_kwargs = {
328
+ "project": "pretrain_biomod",
329
  "name": args.model_name,
330
  "entity": "icml_2023_mlb",
331
  "config": {
332
+ "architecture": "biomod",
333
  "gene_num": args.gene_num,
334
  "cell_num": args.cell_num,
335
  "load model": args.model_path,
 
490
  if is_master:
491
  print(f"Using {len(module_sets)} modules restored from model config PKL")
492
  else:
493
+ module_sets = load_biomod_data(
494
  module_data_path=args.module_data_path,
495
  module_data_dir=args.module_data_dir,
496
  gene_list_path=args.gene_list_path,
 
616
  )
617
 
618
  # Step 3.3: Instantiate the model
619
+ # BioMod model process:
620
  # Input: (batch_size, num_genes) = (3, 27874)
621
  # ├─> Module division: (batch_size, num_modules, module_avg_size) ≈ (3, 500, 55)
622
  # ├─> Module Embedding: (batch_size, num_modules, base_dim) = (3, 500, 1024)
623
  # ├─> Random Masking (75%): Keep about 125 visible modules
624
+ # ├─> BioMod encoder: Process visible modules
625
  # └─> Reconstruction prediction: Output the reconstruction value of the masked module
626
+ model = BioMod(**model_params)
627
 
628
  def configure_trainable_modules(model_obj):
629
  for param in model_obj.parameters():
 
721
  # Step 2.1/2.2 has been completed previously
722
 
723
  # Step 2.3: Create training and validation datasets
724
+ # BioModSCDataset directly returns the gene expression value, and the module conversion is completed inside the model.
725
+ train_dataset = BioModSCDataset(
726
  data_path=args.data_path,
727
  module_sets=module_sets, # Pass module_sets to maintain compatibility, but no longer used in __getitem__
728
  data=None,
 
735
  cell_num=None,
736
  verbose=is_master
737
  )
738
+ val_dataset = BioModSCDataset(
739
  data_path=args.data_path,
740
  module_sets=module_sets,
741
  data=None,
 
880
  # 1. Flat to Module: (3, 27874) -> (3, num_modules, module_avg_size)
881
  # 2. Module Embedding: -> (3, num_modules, 1024)
882
  # 3. Random Masking: mask out 75% of modules
883
+ # 4. BioMod encoder: Process visible modules
884
  # 5. Reconstruction: Predict the masked module
885
  # Output: loss=MSE on masked module, preds=reconstructed value, mask=mask mark
886
  loss, preds, mask = model(data)
BioMod/{module_dataset.py → biomod_dataset.py} RENAMED
@@ -25,7 +25,7 @@
25
  h5py = None
26
 
27
 
28
- def load_module_data(
29
  module_data_path: Optional[str] = None,
30
  module_data_dir: Optional[str] = None,
31
  gene_list_path: Optional[str] = None,
@@ -219,7 +219,7 @@ def _convert_genes_to_indices(genes: list) -> list[int]:
219
  elif isinstance(data, list):
220
  modules = data
221
  else:
222
- raise ValueError("module_data format is incorrect")
223
 
224
  module_sets = []
225
  for p in modules:
@@ -246,13 +246,13 @@ def _convert_genes_to_indices(genes: list) -> list[int]:
246
  raise ValueError("module_data_path or module_data_dir must be provided")
247
 
248
 
249
- class ModuleSCDataset(Dataset):
250
  """
251
  single cell data set
252
  Supports .npz (sparse CSR), .h5ad and .txt formats
253
 
254
  Note: This class no longer performs module conversion (redundant _flat_to_modules removed)
255
- Module conversion is completed by the ModuleEmbedding layer inside the model to avoid repeated calculations
256
  """
257
 
258
  def __init__(
 
25
  h5py = None
26
 
27
 
28
+ def load_biomod_data(
29
  module_data_path: Optional[str] = None,
30
  module_data_dir: Optional[str] = None,
31
  gene_list_path: Optional[str] = None,
 
219
  elif isinstance(data, list):
220
  modules = data
221
  else:
222
+ raise ValueError("biomod_data format is incorrect")
223
 
224
  module_sets = []
225
  for p in modules:
 
246
  raise ValueError("module_data_path or module_data_dir must be provided")
247
 
248
 
249
+ class BioModSCDataset(Dataset):
250
  """
251
  single cell data set
252
  Supports .npz (sparse CSR), .h5ad and .txt formats
253
 
254
  Note: This class no longer performs module conversion (redundant _flat_to_modules removed)
255
+ Module conversion is completed by the BioModEmbedding layer inside the model to avoid repeated calculations
256
  """
257
 
258
  def __init__(
BioMod/cell_annotation/{module_data/module1_hallmarks.json → biomod_data/biomod1_hallmarks.json} RENAMED
File without changes
BioMod/cell_annotation/{module_data/module2_reactome_2022_filtered.json → biomod_data/biomod2_reactome_2022_filtered.json} RENAMED
File without changes
BioMod/cell_annotation/{module_data/load_module.py → biomod_data/load_biomod.py} RENAMED
@@ -29,8 +29,8 @@ def load_json_files(file1_path, file2_path):
29
 
30
 
31
  # hallmark_data, reactome_data = load_json_files(
32
- # 'module1_hallmarks.json',
33
- # 'module2_reactome_2022_filtered.json'
34
  # )
35
  # len(hallmark_data.keys())
36
  # len(reactome_data.keys())
 
29
 
30
 
31
  # hallmark_data, reactome_data = load_json_files(
32
+ # 'biomod1_hallmarks.json',
33
+ # 'biomod2_reactome_2022_filtered.json'
34
  # )
35
  # len(hallmark_data.keys())
36
  # len(reactome_data.keys())
BioMod/cell_annotation/{performer_module_finetune.py → biomod_finetune.py} RENAMED
@@ -2,12 +2,12 @@
2
  # -*- coding: utf-8 -*-
3
 
4
  """
5
- End-to-end cell annotation fine-tuning for Performer Module.
6
 
7
  This script is intentionally independent from the legacy two-stage pipeline:
8
  1. No feature extraction to .npy files.
9
- 2. No imports from other scripts in performer_module/cell_annotation.
10
- 3. Reuses the pretrained Performer Module backbone directly.
11
 
12
  Supported cell embedding heads:
13
  - module_mean: mean over restored module tokens
@@ -48,26 +48,26 @@
48
  from torch.utils.data.distributed import DistributedSampler
49
 
50
  SCRIPT_DIR = Path(__file__).resolve().parent
51
- PERFORMER_MODULE_ROOT = SCRIPT_DIR.parent
52
- SCLONG_ROOT = PERFORMER_MODULE_ROOT.parent
53
 
54
  REPO_ROOT = Path(__file__).resolve().parents[2]
55
  ASSET_DIR = REPO_ROOT / "model_dependencies"
56
  DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
57
- WEIGHTS_DIR = REPO_ROOT / "weight" / "0810model"
58
 
59
- if str(PERFORMER_MODULE_ROOT) not in sys.path:
60
- sys.path.insert(0, str(PERFORMER_MODULE_ROOT))
61
  if str(SCLONG_ROOT) not in sys.path:
62
  sys.path.insert(0, str(SCLONG_ROOT))
63
 
64
- from models_performer_module import (
65
- migrate_legacy_module_config,
66
- migrate_legacy_module_state_dict,
67
  )
68
 
69
  if TYPE_CHECKING:
70
- from models_performer_module import PerformerEncoderOnly_Module
71
 
72
 
73
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
@@ -373,7 +373,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
373
  return self.out_layer(x)
374
 
375
 
376
- class ModuleSelector(nn.Module):
377
  """Per-module scorer: soft weighting during training, hard threshold during inference."""
378
 
379
  def __init__(self, embed_dim: int):
@@ -395,10 +395,10 @@ def forward(
395
  return x, scores
396
 
397
 
398
- class PerformerModuleCellClassifier(nn.Module):
399
  def __init__(
400
  self,
401
- backbone: "PerformerEncoderOnly_Module",
402
  module_sets: List[List[int]],
403
  num_classes: int,
404
  head_type: str,
@@ -430,13 +430,13 @@ def __init__(
430
  self.use_module_selector = use_module_selector
431
  self.selector_tau = 0.5
432
  if use_module_selector:
433
- self.module_selector = ModuleSelector(self.embed_dim)
434
  else:
435
  self.module_selector = None
436
  self._selector_scores = None
437
 
438
  if self.head_type == "gene_mean":
439
- module_idx, gene_idx, gene_counts = self._build_module_gene_index()
440
  self.register_buffer("agg_module_idx", module_idx)
441
  self.register_buffer("agg_gene_idx", gene_idx)
442
  self.register_buffer("agg_gene_counts", gene_counts)
@@ -454,12 +454,12 @@ def __init__(
454
  def load_state_dict(self, state_dict, strict: bool = True, assign: bool = False):
455
  """Accept current module names and older serialized backbone names."""
456
  return super().load_state_dict(
457
- migrate_legacy_module_state_dict(state_dict),
458
  strict=strict,
459
  assign=assign,
460
  )
461
 
462
- def _build_module_gene_index(self) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
463
  module_indices = []
464
  gene_indices = []
465
  gene_counts = torch.zeros(self.num_genes, dtype=torch.long)
@@ -475,7 +475,7 @@ def _build_module_gene_index(self) -> Tuple[torch.Tensor, torch.Tensor, torch.Te
475
  gene_counts,
476
  )
477
 
478
- def _restore_module_order(self, latent: torch.Tensor, ids_restore: torch.Tensor) -> torch.Tensor:
479
  if self.use_cls:
480
  module_tokens = latent[:, 1:, :]
481
  else:
@@ -483,7 +483,7 @@ def _restore_module_order(self, latent: torch.Tensor, ids_restore: torch.Tensor)
483
  gather_index = ids_restore.unsqueeze(-1).expand(-1, -1, module_tokens.shape[-1])
484
  return torch.gather(module_tokens, dim=1, index=gather_index)
485
 
486
- def _module_to_gene(self, module_tokens: torch.Tensor) -> torch.Tensor:
487
  batch_size, _, dim = module_tokens.shape
488
  gene_feats = torch.zeros(
489
  batch_size,
@@ -519,7 +519,7 @@ def forward(self, x_model: torch.Tensor) -> torch.Tensor:
519
  if self.head_type == "cls":
520
  cell_emb = latent[:, 0, :]
521
  else:
522
- module_tokens = self._restore_module_order(latent, ids_restore)
523
  if self.module_selector is not None:
524
  module_tokens, self._selector_scores = self.module_selector(
525
  module_tokens, tau=self.selector_tau, hard=not self.training,
@@ -531,7 +531,7 @@ def forward(self, x_model: torch.Tensor) -> torch.Tensor:
531
  elif self.head_type == "attn_pool":
532
  cell_emb, _ = self.pooler(module_tokens)
533
  elif self.head_type == "gene_mean":
534
- gene_tokens = self._module_to_gene(module_tokens)
535
  cell_emb = gene_tokens.mean(dim=1)
536
  else:
537
  raise ValueError(f"Unsupported head_type: {self.head_type}")
@@ -540,13 +540,13 @@ def forward(self, x_model: torch.Tensor) -> torch.Tensor:
540
  return self.classifier(cell_emb)
541
 
542
 
543
- def get_encoder_blocks(backbone: "PerformerEncoderOnly_Module") -> List[nn.Module]:
544
  net = backbone.encoder.net
545
  if hasattr(net, "layers"):
546
  return list(net.layers)
547
  if hasattr(net, "blocks"):
548
  return list(net.blocks)
549
- raise AttributeError("Cannot find encoder blocks on Performer backbone")
550
 
551
 
552
  def set_requires_grad(module: nn.Module, flag: bool) -> None:
@@ -555,7 +555,7 @@ def set_requires_grad(module: nn.Module, flag: bool) -> None:
555
 
556
 
557
  def configure_backbone_trainability(
558
- backbone: "PerformerEncoderOnly_Module",
559
  finetune_mode: str,
560
  unfreeze_last_n: int,
561
  train_pos_emb: bool,
@@ -604,7 +604,7 @@ def configure_backbone_trainability(
604
 
605
 
606
  def build_optimizer(
607
- model: PerformerModuleCellClassifier,
608
  lr_head: float,
609
  lr_backbone: float,
610
  weight_decay: float,
@@ -669,20 +669,20 @@ def resolve_batch_sizes(args, world_size: int) -> Tuple[int, int, int]:
669
  return train_batch_size, eval_batch_size, effective_global_batch
670
 
671
 
672
- def infer_model_config(args, checkpoint: dict, pkl_config: Optional[dict], module_data: List[List[int]]) -> dict:
673
- from models_performer_module import sanitize_performer_module_config
674
 
675
  if pkl_config is not None and isinstance(pkl_config, dict):
676
- config = sanitize_performer_module_config(
677
  pkl_config,
678
  source=str(args.config_pkl),
679
  log_fn=logger.info,
680
  )
681
  logger.info("Using config from PKL: %s", args.config_pkl)
682
  if "module_sets" in config:
683
- module_data = config["module_sets"]
684
  elif isinstance(checkpoint, dict) and "config" in checkpoint:
685
- config = sanitize_performer_module_config(
686
  checkpoint["config"],
687
  source=str(resolve_path(args.performer_checkpoint)),
688
  log_fn=logger.info,
@@ -695,7 +695,7 @@ def infer_model_config(args, checkpoint: dict, pkl_config: Optional[dict], modul
695
  )
696
 
697
  return {
698
- "module_sets": module_data,
699
  "num_genes": config["num_genes"],
700
  "base_dim": config["base_dim"],
701
  "enc_depth": config["enc_depth"],
@@ -722,9 +722,9 @@ def infer_model_config(args, checkpoint: dict, pkl_config: Optional[dict], modul
722
  }
723
 
724
 
725
- def compute_module_family_ids(module_data_dir: str) -> List[int]:
726
- """Read merged_modules.json, return family ID (0-3) for each module."""
727
- merged_path = Path(module_data_dir) / "merged_modules.json"
728
  with open(merged_path) as f:
729
  modules = json.load(f)
730
  PREFIXES = [
@@ -744,32 +744,32 @@ def compute_module_family_ids(module_data_dir: str) -> List[int]:
744
  return family_ids
745
 
746
 
747
- def load_backbone(args, device: torch.device) -> Tuple["PerformerEncoderOnly_Module", List[List[int]], dict]:
748
- from models_performer_module import PerformerEncoderOnly_Module
749
- from module_dataset import load_module_data
750
 
751
  config_pkl = None
752
  config_path = resolve_path(args.config_pkl)
753
  if config_path.exists():
754
  with open(config_path, "rb") as handle:
755
- config_pkl = migrate_legacy_module_config(pickle.load(handle))
756
  else:
757
  logger.warning("Config PKL not found at %s; fallback to checkpoint config", config_path)
758
 
759
  ckpt_path = resolve_path(args.performer_checkpoint)
760
  checkpoint = torch.load(ckpt_path, map_location=device)
761
 
762
- module_data = None
763
  if isinstance(config_pkl, dict) and "module_sets" in config_pkl:
764
- module_data = config_pkl["module_sets"]
765
- logger.info("Using module_sets from PKL (%d modules)", len(module_data))
766
  elif isinstance(checkpoint, dict) and isinstance(checkpoint.get("config"), dict) and "module_sets" in checkpoint["config"]:
767
- module_data = checkpoint["config"]["module_sets"]
768
- logger.info("Using module_sets from checkpoint config (%d modules)", len(module_data))
769
  else:
770
  if not args.module_data_dir:
771
  raise ValueError("module_data_dir is required when module_sets are not stored in the PKL/checkpoint config")
772
- module_data = load_module_data(
773
  module_data_dir=str(resolve_path(args.module_data_dir)),
774
  gene_list_path=str(resolve_path(args.gene_list_path)),
775
  gene_mapping_path=str(resolve_path(args.gene_mapping_path)),
@@ -779,18 +779,18 @@ def load_backbone(args, device: torch.device) -> Tuple["PerformerEncoderOnly_Mod
779
  if args.shuffle_module_genes:
780
  logger.info("Shuffling genes across modules with seed=%d", args.shuffle_seed)
781
  rng = random.Random(args.shuffle_seed)
782
- module_sizes = [len(module) for module in module_data]
783
- all_genes = [gene for module in module_data for gene in module]
784
  rng.shuffle(all_genes)
785
  shuffled = []
786
  start = 0
787
  for size in module_sizes:
788
  shuffled.append(all_genes[start:start + size])
789
  start += size
790
- module_data = shuffled
791
 
792
- model_config = infer_model_config(args, checkpoint, config_pkl, module_data)
793
- module_data = model_config["module_sets"]
794
 
795
  if "model_state_dict" in checkpoint:
796
  state_dict = checkpoint["model_state_dict"]
@@ -807,7 +807,7 @@ def load_backbone(args, device: torch.device) -> Tuple["PerformerEncoderOnly_Mod
807
  model_config["base_dim"],
808
  model_config["enc_depth"],
809
  model_config["enc_heads"],
810
- len(module_data),
811
  use_cls_token,
812
  )
813
 
@@ -815,13 +815,13 @@ def load_backbone(args, device: torch.device) -> Tuple["PerformerEncoderOnly_Mod
815
  if getattr(args, "use_family_emb", False):
816
  pdd = getattr(args, "module_data_dir", None)
817
  if pdd:
818
- module_family_ids = compute_module_family_ids(str(resolve_path(pdd)))
819
  logger.info("Family embedding enabled: %d module family IDs computed", len(module_family_ids))
820
 
821
- backbone = PerformerEncoderOnly_Module(
822
- module_sets=module_data,
823
  num_genes=model_config["num_genes"],
824
- max_seq_len=len(module_data),
825
  base_dim=model_config["base_dim"],
826
  enc_depth=model_config["enc_depth"],
827
  enc_heads=model_config["enc_heads"],
@@ -866,7 +866,7 @@ def load_backbone(args, device: torch.device) -> Tuple["PerformerEncoderOnly_Mod
866
  logger.warning("Unexpected keys: %s", unexpected_keys[:10])
867
 
868
  backbone.to(device)
869
- return backbone, module_data, model_config
870
 
871
 
872
  def stratified_split(
@@ -908,7 +908,7 @@ def stratified_train_val_split(
908
 
909
  @torch.no_grad()
910
  def evaluate(
911
- model: PerformerModuleCellClassifier,
912
  data_loader: DataLoader,
913
  preprocessor: DataPreprocessor,
914
  reindex_state: dict,
@@ -948,7 +948,7 @@ def evaluate(
948
 
949
 
950
  def train_one_epoch(
951
- model: PerformerModuleCellClassifier,
952
  data_loader: DataLoader,
953
  preprocessor: DataPreprocessor,
954
  reindex_state: dict,
@@ -1075,7 +1075,7 @@ def apply_dataset_preset(args) -> None:
1075
 
1076
 
1077
  def parse_args():
1078
- parser = argparse.ArgumentParser(description="End-to-end fine-tuning for Performer Module cell annotation")
1079
 
1080
  parser.add_argument("--seed", type=int, default=2021)
1081
  parser.add_argument("--device", type=str, default="cuda")
@@ -1117,13 +1117,13 @@ def parse_args():
1117
  )
1118
  parser.add_argument("--unfreeze_last_n", type=int, default=1)
1119
  parser.add_argument("--train_pos_emb", action="store_true")
1120
- parser.add_argument("--train_module_embed", action="store_true")
1121
  parser.add_argument("--use_family_emb", action="store_true",
1122
  help="Add family-type embedding (Hallmarks/Reactome/Hotspot/WGCNA)")
1123
- parser.add_argument("--use_module_gate", action="store_true",
1124
- help="Add learnable sparse gate per module")
1125
- parser.add_argument("--use_module_selector", action="store_true",
1126
- help="Dynamic module selector with sparsity loss (soft train, hard inference)")
1127
  parser.add_argument("--selector_sparsity_weight", type=float, default=0.1,
1128
  help="Lambda for sparsity loss on module selector scores")
1129
  parser.add_argument("--selector_tau", type=float, default=0.5,
@@ -1152,32 +1152,33 @@ def parse_args():
1152
  parser.add_argument("--min_cells", type=int, default=10)
1153
  parser.add_argument("--max_cells", type=int, default=None)
1154
 
1155
- parser.add_argument("--config_pkl", type=str, default=str(WEIGHTS_DIR / "mae_tost_70M_75mask_ec2.pkl"))
1156
  parser.add_argument(
1157
- "--performer_checkpoint",
1158
  type=str,
1159
- default=str(WEIGHTS_DIR / "mae_tost_70M_75mask_ec2_epoch0.pth"),
 
1160
  )
1161
- parser.add_argument("--module_data_dir", type=str, default=str(ASSET_DIR / "module_data_all"))
1162
  parser.add_argument("--gene_list_path", type=str, default=str(ASSET_DIR / "selected_genes_27k.txt"))
1163
  parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_DIR / "gene2ens.csv"))
1164
  parser.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_DIR / "selected_gene2vec_27k.npy"))
1165
  parser.add_argument("--gene2ens_file", type=str, default=str(ASSET_DIR / "gene2ens.csv"))
1166
  parser.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
1167
  parser.add_argument(
1168
- "--shuffle_module_genes",
1169
  action="store_true",
1170
  help="Apply an extra downstream reshuffle on top of module_sets restored from PKL. "
1171
  "Do not pass this when reproducing the pretrained backbone as-is.",
1172
- )
1173
  parser.add_argument(
1174
  "--shuffle_seed",
1175
  type=int,
1176
  default=2021,
1177
- help="Random seed for the downstream extra reshuffle when --shuffle_module_genes is enabled.",
1178
  )
1179
 
1180
- parser.add_argument("--output_dir", type=str, default="./ckpts_performer_module_finetune")
1181
  parser.add_argument("--run_name", type=str, default="")
1182
  parser.add_argument(
1183
  "--save_test_artifacts",
@@ -1384,7 +1385,7 @@ def main():
1384
  train_module_embed=args.train_module_embed,
1385
  )
1386
 
1387
- model = PerformerModuleCellClassifier(
1388
  backbone=backbone,
1389
  module_sets=module_sets,
1390
  num_classes=num_classes,
 
2
  # -*- coding: utf-8 -*-
3
 
4
  """
5
+ End-to-end cell annotation fine-tuning for BioMod.
6
 
7
  This script is intentionally independent from the legacy two-stage pipeline:
8
  1. No feature extraction to .npy files.
9
+ 2. No imports from other scripts in biomod/cell_annotation.
10
+ 3. Reuses the pretrained BioMod backbone directly.
11
 
12
  Supported cell embedding heads:
13
  - module_mean: mean over restored module tokens
 
48
  from torch.utils.data.distributed import DistributedSampler
49
 
50
  SCRIPT_DIR = Path(__file__).resolve().parent
51
+ BIOMOD_ROOT = SCRIPT_DIR.parent
52
+ SCLONG_ROOT = BIOMOD_ROOT.parent
53
 
54
  REPO_ROOT = Path(__file__).resolve().parents[2]
55
  ASSET_DIR = REPO_ROOT / "model_dependencies"
56
  DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
57
+ WEIGHTS_DIR = REPO_ROOT / "weight"
58
 
59
+ if str(BIOMOD_ROOT) not in sys.path:
60
+ sys.path.insert(0, str(BIOMOD_ROOT))
61
  if str(SCLONG_ROOT) not in sys.path:
62
  sys.path.insert(0, str(SCLONG_ROOT))
63
 
64
+ from models_biomod import (
65
+ migrate_legacy_biomod_config,
66
+ migrate_legacy_biomod_state_dict,
67
  )
68
 
69
  if TYPE_CHECKING:
70
+ from models_biomod import BioMod
71
 
72
 
73
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
 
373
  return self.out_layer(x)
374
 
375
 
376
+ class BioModSelector(nn.Module):
377
  """Per-module scorer: soft weighting during training, hard threshold during inference."""
378
 
379
  def __init__(self, embed_dim: int):
 
395
  return x, scores
396
 
397
 
398
+ class BioModCellClassifier(nn.Module):
399
  def __init__(
400
  self,
401
+ backbone: "BioMod",
402
  module_sets: List[List[int]],
403
  num_classes: int,
404
  head_type: str,
 
430
  self.use_module_selector = use_module_selector
431
  self.selector_tau = 0.5
432
  if use_module_selector:
433
+ self.module_selector = BioModSelector(self.embed_dim)
434
  else:
435
  self.module_selector = None
436
  self._selector_scores = None
437
 
438
  if self.head_type == "gene_mean":
439
+ module_idx, gene_idx, gene_counts = self._build_biomod_gene_index()
440
  self.register_buffer("agg_module_idx", module_idx)
441
  self.register_buffer("agg_gene_idx", gene_idx)
442
  self.register_buffer("agg_gene_counts", gene_counts)
 
454
  def load_state_dict(self, state_dict, strict: bool = True, assign: bool = False):
455
  """Accept current module names and older serialized backbone names."""
456
  return super().load_state_dict(
457
+ migrate_legacy_biomod_state_dict(state_dict),
458
  strict=strict,
459
  assign=assign,
460
  )
461
 
462
+ def _build_biomod_gene_index(self) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
463
  module_indices = []
464
  gene_indices = []
465
  gene_counts = torch.zeros(self.num_genes, dtype=torch.long)
 
475
  gene_counts,
476
  )
477
 
478
+ def _restore_biomod_order(self, latent: torch.Tensor, ids_restore: torch.Tensor) -> torch.Tensor:
479
  if self.use_cls:
480
  module_tokens = latent[:, 1:, :]
481
  else:
 
483
  gather_index = ids_restore.unsqueeze(-1).expand(-1, -1, module_tokens.shape[-1])
484
  return torch.gather(module_tokens, dim=1, index=gather_index)
485
 
486
+ def _biomod_to_gene(self, module_tokens: torch.Tensor) -> torch.Tensor:
487
  batch_size, _, dim = module_tokens.shape
488
  gene_feats = torch.zeros(
489
  batch_size,
 
519
  if self.head_type == "cls":
520
  cell_emb = latent[:, 0, :]
521
  else:
522
+ module_tokens = self._restore_biomod_order(latent, ids_restore)
523
  if self.module_selector is not None:
524
  module_tokens, self._selector_scores = self.module_selector(
525
  module_tokens, tau=self.selector_tau, hard=not self.training,
 
531
  elif self.head_type == "attn_pool":
532
  cell_emb, _ = self.pooler(module_tokens)
533
  elif self.head_type == "gene_mean":
534
+ gene_tokens = self._biomod_to_gene(module_tokens)
535
  cell_emb = gene_tokens.mean(dim=1)
536
  else:
537
  raise ValueError(f"Unsupported head_type: {self.head_type}")
 
540
  return self.classifier(cell_emb)
541
 
542
 
543
+ def get_encoder_blocks(backbone: "BioMod") -> List[nn.Module]:
544
  net = backbone.encoder.net
545
  if hasattr(net, "layers"):
546
  return list(net.layers)
547
  if hasattr(net, "blocks"):
548
  return list(net.blocks)
549
+ raise AttributeError("Cannot find encoder blocks on BioMod backbone")
550
 
551
 
552
  def set_requires_grad(module: nn.Module, flag: bool) -> None:
 
555
 
556
 
557
  def configure_backbone_trainability(
558
+ backbone: "BioMod",
559
  finetune_mode: str,
560
  unfreeze_last_n: int,
561
  train_pos_emb: bool,
 
604
 
605
 
606
  def build_optimizer(
607
+ model: BioModCellClassifier,
608
  lr_head: float,
609
  lr_backbone: float,
610
  weight_decay: float,
 
669
  return train_batch_size, eval_batch_size, effective_global_batch
670
 
671
 
672
+ def infer_model_config(args, checkpoint: dict, pkl_config: Optional[dict], biomod_data: List[List[int]]) -> dict:
673
+ from models_biomod import sanitize_biomod_config
674
 
675
  if pkl_config is not None and isinstance(pkl_config, dict):
676
+ config = sanitize_biomod_config(
677
  pkl_config,
678
  source=str(args.config_pkl),
679
  log_fn=logger.info,
680
  )
681
  logger.info("Using config from PKL: %s", args.config_pkl)
682
  if "module_sets" in config:
683
+ biomod_data = config["module_sets"]
684
  elif isinstance(checkpoint, dict) and "config" in checkpoint:
685
+ config = sanitize_biomod_config(
686
  checkpoint["config"],
687
  source=str(resolve_path(args.performer_checkpoint)),
688
  log_fn=logger.info,
 
695
  )
696
 
697
  return {
698
+ "module_sets": biomod_data,
699
  "num_genes": config["num_genes"],
700
  "base_dim": config["base_dim"],
701
  "enc_depth": config["enc_depth"],
 
722
  }
723
 
724
 
725
+ def compute_biomod_family_ids(module_data_dir: str) -> List[int]:
726
+ """Read merged_biomods.json, return family ID (0-3) for each module."""
727
+ merged_path = Path(module_data_dir) / "merged_biomods.json"
728
  with open(merged_path) as f:
729
  modules = json.load(f)
730
  PREFIXES = [
 
744
  return family_ids
745
 
746
 
747
+ def load_backbone(args, device: torch.device) -> Tuple["BioMod", List[List[int]], dict]:
748
+ from models_biomod import BioMod
749
+ from biomod_dataset import load_biomod_data
750
 
751
  config_pkl = None
752
  config_path = resolve_path(args.config_pkl)
753
  if config_path.exists():
754
  with open(config_path, "rb") as handle:
755
+ config_pkl = migrate_legacy_biomod_config(pickle.load(handle))
756
  else:
757
  logger.warning("Config PKL not found at %s; fallback to checkpoint config", config_path)
758
 
759
  ckpt_path = resolve_path(args.performer_checkpoint)
760
  checkpoint = torch.load(ckpt_path, map_location=device)
761
 
762
+ biomod_data = None
763
  if isinstance(config_pkl, dict) and "module_sets" in config_pkl:
764
+ biomod_data = config_pkl["module_sets"]
765
+ logger.info("Using module_sets from PKL (%d modules)", len(biomod_data))
766
  elif isinstance(checkpoint, dict) and isinstance(checkpoint.get("config"), dict) and "module_sets" in checkpoint["config"]:
767
+ biomod_data = checkpoint["config"]["module_sets"]
768
+ logger.info("Using module_sets from checkpoint config (%d modules)", len(biomod_data))
769
  else:
770
  if not args.module_data_dir:
771
  raise ValueError("module_data_dir is required when module_sets are not stored in the PKL/checkpoint config")
772
+ biomod_data = load_biomod_data(
773
  module_data_dir=str(resolve_path(args.module_data_dir)),
774
  gene_list_path=str(resolve_path(args.gene_list_path)),
775
  gene_mapping_path=str(resolve_path(args.gene_mapping_path)),
 
779
  if args.shuffle_module_genes:
780
  logger.info("Shuffling genes across modules with seed=%d", args.shuffle_seed)
781
  rng = random.Random(args.shuffle_seed)
782
+ module_sizes = [len(module) for module in biomod_data]
783
+ all_genes = [gene for module in biomod_data for gene in module]
784
  rng.shuffle(all_genes)
785
  shuffled = []
786
  start = 0
787
  for size in module_sizes:
788
  shuffled.append(all_genes[start:start + size])
789
  start += size
790
+ biomod_data = shuffled
791
 
792
+ model_config = infer_model_config(args, checkpoint, config_pkl, biomod_data)
793
+ biomod_data = model_config["module_sets"]
794
 
795
  if "model_state_dict" in checkpoint:
796
  state_dict = checkpoint["model_state_dict"]
 
807
  model_config["base_dim"],
808
  model_config["enc_depth"],
809
  model_config["enc_heads"],
810
+ len(biomod_data),
811
  use_cls_token,
812
  )
813
 
 
815
  if getattr(args, "use_family_emb", False):
816
  pdd = getattr(args, "module_data_dir", None)
817
  if pdd:
818
+ module_family_ids = compute_biomod_family_ids(str(resolve_path(pdd)))
819
  logger.info("Family embedding enabled: %d module family IDs computed", len(module_family_ids))
820
 
821
+ backbone = BioMod(
822
+ module_sets=biomod_data,
823
  num_genes=model_config["num_genes"],
824
+ max_seq_len=len(biomod_data),
825
  base_dim=model_config["base_dim"],
826
  enc_depth=model_config["enc_depth"],
827
  enc_heads=model_config["enc_heads"],
 
866
  logger.warning("Unexpected keys: %s", unexpected_keys[:10])
867
 
868
  backbone.to(device)
869
+ return backbone, biomod_data, model_config
870
 
871
 
872
  def stratified_split(
 
908
 
909
  @torch.no_grad()
910
  def evaluate(
911
+ model: BioModCellClassifier,
912
  data_loader: DataLoader,
913
  preprocessor: DataPreprocessor,
914
  reindex_state: dict,
 
948
 
949
 
950
  def train_one_epoch(
951
+ model: BioModCellClassifier,
952
  data_loader: DataLoader,
953
  preprocessor: DataPreprocessor,
954
  reindex_state: dict,
 
1075
 
1076
 
1077
  def parse_args():
1078
+ parser = argparse.ArgumentParser(description="End-to-end fine-tuning for BioMod cell annotation")
1079
 
1080
  parser.add_argument("--seed", type=int, default=2021)
1081
  parser.add_argument("--device", type=str, default="cuda")
 
1117
  )
1118
  parser.add_argument("--unfreeze_last_n", type=int, default=1)
1119
  parser.add_argument("--train_pos_emb", action="store_true")
1120
+ parser.add_argument("--train_biomod_embed", "--train_module_embed", action="store_true", dest='train_module_embed')
1121
  parser.add_argument("--use_family_emb", action="store_true",
1122
  help="Add family-type embedding (Hallmarks/Reactome/Hotspot/WGCNA)")
1123
+ parser.add_argument("--use_biomod_gate", "--use_module_gate", action="store_true",
1124
+ help="Add learnable sparse gate per module", dest='use_module_gate')
1125
+ parser.add_argument("--use_biomod_selector", "--use_module_selector", action="store_true",
1126
+ help="Dynamic module selector with sparsity loss (soft train, hard inference)", dest='use_module_selector')
1127
  parser.add_argument("--selector_sparsity_weight", type=float, default=0.1,
1128
  help="Lambda for sparsity loss on module selector scores")
1129
  parser.add_argument("--selector_tau", type=float, default=0.5,
 
1152
  parser.add_argument("--min_cells", type=int, default=10)
1153
  parser.add_argument("--max_cells", type=int, default=None)
1154
 
1155
+ parser.add_argument("--config_pkl", type=str, default=str(WEIGHTS_DIR / "BioMod.pkl"))
1156
  parser.add_argument(
1157
+ "--biomod_checkpoint", "--performer_checkpoint",
1158
  type=str,
1159
+ default=str(WEIGHTS_DIR / "BioMod.pth"),
1160
+ dest='performer_checkpoint',
1161
  )
1162
+ parser.add_argument("--biomod_data_dir", "--module_data_dir", type=str, default=str(ASSET_DIR / "biomod_data_all"), dest='module_data_dir')
1163
  parser.add_argument("--gene_list_path", type=str, default=str(ASSET_DIR / "selected_genes_27k.txt"))
1164
  parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_DIR / "gene2ens.csv"))
1165
  parser.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_DIR / "selected_gene2vec_27k.npy"))
1166
  parser.add_argument("--gene2ens_file", type=str, default=str(ASSET_DIR / "gene2ens.csv"))
1167
  parser.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
1168
  parser.add_argument(
1169
+ "--shuffle_biomod_genes", "--shuffle_module_genes",
1170
  action="store_true",
1171
  help="Apply an extra downstream reshuffle on top of module_sets restored from PKL. "
1172
  "Do not pass this when reproducing the pretrained backbone as-is.",
1173
+ dest='shuffle_module_genes')
1174
  parser.add_argument(
1175
  "--shuffle_seed",
1176
  type=int,
1177
  default=2021,
1178
+ help="Random seed for the downstream extra reshuffle when --shuffle_biomod_genes is enabled.",
1179
  )
1180
 
1181
+ parser.add_argument("--output_dir", type=str, default="./ckpts_biomod_finetune")
1182
  parser.add_argument("--run_name", type=str, default="")
1183
  parser.add_argument(
1184
  "--save_test_artifacts",
 
1385
  train_module_embed=args.train_module_embed,
1386
  )
1387
 
1388
+ model = BioModCellClassifier(
1389
  backbone=backbone,
1390
  module_sets=module_sets,
1391
  num_classes=num_classes,
BioMod/cell_annotation/cmd_finetune.md CHANGED
@@ -3,9 +3,9 @@
3
  Run this command from the repository root in a CUDA/NCCL environment. Resource defaults resolve from the script location; explicit relative paths resolve from the working directory.
4
 
5
  ```bash
6
- torchrun --nproc_per_node=4 BioMod/cell_annotation/performer_module_finetune.py \
7
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
8
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
9
  --data_path data/downstream/final_data/Zheng68K.h5ad \
10
  --label_column celltype \
11
  --head_type attn_pool \
@@ -16,6 +16,6 @@ torchrun --nproc_per_node=4 BioMod/cell_annotation/performer_module_finetune.py
16
  --lr_backbone 1e-5 \
17
  --batch_size 256 \
18
  --epochs 300 \
19
- --module_data_dir model_dependencies/module_data_all \
20
  --output_dir tutorial_runs/cell_annotation_zheng68k
21
  ```
 
3
  Run this command from the repository root in a CUDA/NCCL environment. Resource defaults resolve from the script location; explicit relative paths resolve from the working directory.
4
 
5
  ```bash
6
+ torchrun --nproc_per_node=4 BioMod/cell_annotation/biomod_finetune.py \
7
+ --config_pkl weight/BioMod.pkl \
8
+ --biomod_checkpoint weight/BioMod.pth \
9
  --data_path data/downstream/final_data/Zheng68K.h5ad \
10
  --label_column celltype \
11
  --head_type attn_pool \
 
16
  --lr_backbone 1e-5 \
17
  --batch_size 256 \
18
  --epochs 300 \
19
+ --biomod_data_dir model_dependencies/biomod_data_all \
20
  --output_dir tutorial_runs/cell_annotation_zheng68k
21
  ```
BioMod/command.txt CHANGED
@@ -1,10 +1,10 @@
1
  # Run from the outer repository root in a Linux CUDA/NCCL environment.
2
  # Set BIOMOD_PRETRAIN_DATA to your preprocessed per-cell NPZ directory.
3
  # The pretraining corpus is not included in this repository.
4
- conda activate performer_module
5
  : "${BIOMOD_PRETRAIN_DATA:?Set BIOMOD_PRETRAIN_DATA to the pretraining NPZ directory}"
6
  torchrun --standalone --nproc_per_node="${NPROC_PER_NODE:-1}" \
7
- BioMod/performer_module.py \
8
  --gene_list_path model_dependencies/selected_genes_27k.txt \
9
  --cell_num 4000000 \
10
  --epoch 15 \
@@ -15,11 +15,11 @@ torchrun --standalone --nproc_per_node="${NPROC_PER_NODE:-1}" \
15
  --log_every 100 \
16
  --mask_ratio 0.75 \
17
  --data_path "$BIOMOD_PRETRAIN_DATA" \
18
- --module_data_dir model_dependencies/module_data_all \
19
  --gene_mapping_path model_dependencies/gene2ens.csv \
20
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
21
  --ckpt_dir tutorial_runs/pretraining \
22
- --model_name performer_module_pretraining \
23
  --fusion_method multiply \
24
  --loss_mode gene_mean \
25
  --decoder_type simple \
 
1
  # Run from the outer repository root in a Linux CUDA/NCCL environment.
2
  # Set BIOMOD_PRETRAIN_DATA to your preprocessed per-cell NPZ directory.
3
  # The pretraining corpus is not included in this repository.
4
+ conda activate biomod
5
  : "${BIOMOD_PRETRAIN_DATA:?Set BIOMOD_PRETRAIN_DATA to the pretraining NPZ directory}"
6
  torchrun --standalone --nproc_per_node="${NPROC_PER_NODE:-1}" \
7
+ BioMod/biomod.py \
8
  --gene_list_path model_dependencies/selected_genes_27k.txt \
9
  --cell_num 4000000 \
10
  --epoch 15 \
 
15
  --log_every 100 \
16
  --mask_ratio 0.75 \
17
  --data_path "$BIOMOD_PRETRAIN_DATA" \
18
+ --biomod_data_dir model_dependencies/biomod_data_all \
19
  --gene_mapping_path model_dependencies/gene2ens.csv \
20
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
21
  --ckpt_dir tutorial_runs/pretraining \
22
+ --model_name biomod_pretraining \
23
  --fusion_method multiply \
24
  --loss_mode gene_mean \
25
  --decoder_type simple \
BioMod/drug_combination/README.md CHANGED
@@ -1,19 +1,19 @@
1
  # Drug Combination Synergy Prediction with BioMod
2
 
3
  This task predicts drug-combination synergy using BioMod cell embeddings from
4
- the PerformerModule encoder and GCN-based drug molecular graph encoders. Use the
5
  [BioMod model weights and matching configuration](../../README.md#biomod-model-weights).
6
 
7
  It is adapted from scLong's drug-combination implementation
8
  (DeepDDs), replacing the scfm/geneformer cell-line representation with
9
- PerformerModule.
10
 
11
  ## Model Architecture
12
 
13
  | Component | Description |
14
  | --- | --- |
15
  | Drug branch | Three PyG GCNConv layers (78 -> 78 -> 156 -> 312), `global_max_pool`, and two fully connected layers to 128 dimensions |
16
- | Cell branch | Pretrained PerformerModule encoder, module mean pooling, and an MLP to 128 dimensions |
17
  | Fusion layer | `concat(drug1, drug2, cell) = 384 -> MLP -> 2-class prediction` |
18
 
19
  The drug1 and drug2 branches share weights, matching scLong.
@@ -55,20 +55,20 @@ pip install RDKit -i https://pypi.tuna.tsinghua.edu.cn/simple
55
  ### Basic Run (Domain Generalization Mode, Default)
56
 
57
  ```bash
58
- python BioMod/drug_combination/train_drug_combination_performer_module.py \
59
- --checkpoint_path weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
60
- --hyper_params_path weight/0810model/mae_tost_70M_75mask_ec2.pkl \
61
- --module_data_dir model_dependencies/module_data_all \
62
  --data_dir data/downstream/final_data/drug_combination_synergy/data_summary
63
  ```
64
 
65
  ### Custom Paths
66
 
67
  ```bash
68
- python BioMod/drug_combination/train_drug_combination_performer_module.py \
69
- --checkpoint_path weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
70
- --hyper_params_path weight/0810model/mae_tost_70M_75mask_ec2.pkl \
71
- --module_data_dir model_dependencies/module_data_all \
72
  --gene_list_path model_dependencies/selected_genes_27k.txt\
73
  --gene_mapping_path model_dependencies/gene2ens.csv \
74
  --gene2vec_path model_dependencies/selected_gene2vec_27k.npy \
@@ -87,20 +87,20 @@ Reference note: `1e-4 500 0.6824301861702127`.
87
  ### Five-Fold Cross-Validation (Disable Domain Generalization)
88
 
89
  ```bash
90
- python BioMod/drug_combination/train_drug_combination_performer_module.py \
91
- --checkpoint_path weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
92
- --hyper_params_path weight/0810model/mae_tost_70M_75mask_ec2.pkl \
93
- --module_data_dir model_dependencies/module_data_all \
94
  --no_domain_gen
95
  ```
96
 
97
  ### Force Reprocessing After Changing the Model or Data
98
 
99
  ```bash
100
- python BioMod/drug_combination/train_drug_combination_performer_module.py \
101
- --checkpoint_path weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
102
- --hyper_params_path weight/0810model/mae_tost_70M_75mask_ec2.pkl \
103
- --module_data_dir model_dependencies/module_data_all \
104
  --force_preprocess
105
  ```
106
 
@@ -109,8 +109,8 @@ python BioMod/drug_combination/train_drug_combination_performer_module.py \
109
  | Argument | Default | Description |
110
  | --- | --- | --- |
111
  | `--checkpoint_path` | Required | Path to the BioMod model weights |
112
- | `--hyper_params_path` | Required | Path to the PerformerModule hyper-parameter pkl file |
113
- | `--module_data_dir` | None | Directory containing module data files |
114
  | `--data_dir` | `data/downstream/final_data/drug_combination_synergy/data_summary` | Data directory |
115
  | `--validation_label_file` | `test_data/independent_input.csv` | Validation labels for checkpoint selection; legacy alias: `--test_label_file` |
116
  | `--validation_cell_file` | `test_data/independent_cell_features_954.csv` | Validation cell features; legacy alias: `--test_cell_file` |
@@ -120,7 +120,7 @@ python BioMod/drug_combination/train_drug_combination_performer_module.py \
120
  | `--n_folds` | 5 | Number of folds |
121
  | `--domain_gen` | True | Domain generalization mode: train on the full training set and select checkpoints using a separate validation set |
122
  | `--eval_interval` | 5 | Evaluate every N epochs |
123
- | `--pool_method` | mean | PerformerModule pooling method (`mean`, `max`, or `concat`) |
124
  | `--cache_emb` | True | Cache cell embeddings to a `.npy` file |
125
  | `--force_preprocess` | False | Force regeneration of the PyG dataset |
126
 
@@ -130,7 +130,7 @@ Results are saved under `--output_dir` (default: `BioMod/drug_combination/result
130
 
131
  - `best_model_fold{i}.pt` - model weights selected by the highest validation AUC for each fold
132
  - `fpr_tpr_fold{i}.pkl` - validation FPR/TPR curve data for drawing ROC curves
133
- - `cell_emb_cache_{pool_method}.npy` - cached PerformerModule cell embeddings
134
 
135
  ## Evaluation Metrics
136
 
 
1
  # Drug Combination Synergy Prediction with BioMod
2
 
3
  This task predicts drug-combination synergy using BioMod cell embeddings from
4
+ the BioMod encoder and GCN-based drug molecular graph encoders. Use the
5
  [BioMod model weights and matching configuration](../../README.md#biomod-model-weights).
6
 
7
  It is adapted from scLong's drug-combination implementation
8
  (DeepDDs), replacing the scfm/geneformer cell-line representation with
9
+ BioMod.
10
 
11
  ## Model Architecture
12
 
13
  | Component | Description |
14
  | --- | --- |
15
  | Drug branch | Three PyG GCNConv layers (78 -> 78 -> 156 -> 312), `global_max_pool`, and two fully connected layers to 128 dimensions |
16
+ | Cell branch | Pretrained BioMod encoder, module mean pooling, and an MLP to 128 dimensions |
17
  | Fusion layer | `concat(drug1, drug2, cell) = 384 -> MLP -> 2-class prediction` |
18
 
19
  The drug1 and drug2 branches share weights, matching scLong.
 
55
  ### Basic Run (Domain Generalization Mode, Default)
56
 
57
  ```bash
58
+ python BioMod/drug_combination/train_drug_combination_biomod.py \
59
+ --checkpoint_path weight/BioMod.pth \
60
+ --hyper_params_path weight/BioMod.pkl \
61
+ --biomod_data_dir model_dependencies/biomod_data_all \
62
  --data_dir data/downstream/final_data/drug_combination_synergy/data_summary
63
  ```
64
 
65
  ### Custom Paths
66
 
67
  ```bash
68
+ python BioMod/drug_combination/train_drug_combination_biomod.py \
69
+ --checkpoint_path weight/BioMod.pth \
70
+ --hyper_params_path weight/BioMod.pkl \
71
+ --biomod_data_dir model_dependencies/biomod_data_all \
72
  --gene_list_path model_dependencies/selected_genes_27k.txt\
73
  --gene_mapping_path model_dependencies/gene2ens.csv \
74
  --gene2vec_path model_dependencies/selected_gene2vec_27k.npy \
 
87
  ### Five-Fold Cross-Validation (Disable Domain Generalization)
88
 
89
  ```bash
90
+ python BioMod/drug_combination/train_drug_combination_biomod.py \
91
+ --checkpoint_path weight/BioMod.pth \
92
+ --hyper_params_path weight/BioMod.pkl \
93
+ --biomod_data_dir model_dependencies/biomod_data_all \
94
  --no_domain_gen
95
  ```
96
 
97
  ### Force Reprocessing After Changing the Model or Data
98
 
99
  ```bash
100
+ python BioMod/drug_combination/train_drug_combination_biomod.py \
101
+ --checkpoint_path weight/BioMod.pth \
102
+ --hyper_params_path weight/BioMod.pkl \
103
+ --biomod_data_dir model_dependencies/biomod_data_all \
104
  --force_preprocess
105
  ```
106
 
 
109
  | Argument | Default | Description |
110
  | --- | --- | --- |
111
  | `--checkpoint_path` | Required | Path to the BioMod model weights |
112
+ | `--hyper_params_path` | Required | Path to the BioMod hyper-parameter pkl file |
113
+ | `--biomod_data_dir` | None | Directory containing module data files |
114
  | `--data_dir` | `data/downstream/final_data/drug_combination_synergy/data_summary` | Data directory |
115
  | `--validation_label_file` | `test_data/independent_input.csv` | Validation labels for checkpoint selection; legacy alias: `--test_label_file` |
116
  | `--validation_cell_file` | `test_data/independent_cell_features_954.csv` | Validation cell features; legacy alias: `--test_cell_file` |
 
120
  | `--n_folds` | 5 | Number of folds |
121
  | `--domain_gen` | True | Domain generalization mode: train on the full training set and select checkpoints using a separate validation set |
122
  | `--eval_interval` | 5 | Evaluate every N epochs |
123
+ | `--pool_method` | mean | BioMod pooling method (`mean`, `max`, or `concat`) |
124
  | `--cache_emb` | True | Cache cell embeddings to a `.npy` file |
125
  | `--force_preprocess` | False | Force regeneration of the PyG dataset |
126
 
 
130
 
131
  - `best_model_fold{i}.pt` - model weights selected by the highest validation AUC for each fold
132
  - `fpr_tpr_fold{i}.pkl` - validation FPR/TPR curve data for drawing ROC curves
133
+ - `cell_emb_cache_{pool_method}.npy` - cached BioMod cell embeddings
134
 
135
  ## Evaluation Metrics
136
 
BioMod/drug_combination/{train_drug_combination_performer_module.py → train_drug_combination_biomod.py} RENAMED
@@ -1,18 +1,18 @@
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  """
4
- Drug Combination Synergy Prediction with PerformerModule cell embeddings.
5
 
6
  Aligned with scLong/drug_combination (DeepDDs) architecture:
7
  - Drug branch: PyG GCNConv + global_max_pool (shared weights for drug1 & drug2)
8
- - Cell branch: PerformerModule pretrained encoder -> MLP
9
  - Fusion: concat(drug1, drug2, cell) -> MLP -> 2-class classification
10
 
11
  Usage:
12
  # Run from the repository root.
13
- python BioMod/drug_combination/train_drug_combination_performer_module.py \
14
  --checkpoint_path <ckpt> --hyper_params_path <pkl> \
15
- --module_data_dir model_dependencies/module_data_all \
16
  --data_dir data/downstream/final_data/drug_combination_synergy/data_summary
17
  """
18
 
@@ -54,35 +54,35 @@
54
 
55
  # ===================== Path setup =====================
56
  SCRIPT_DIR = Path(__file__).resolve().parent
57
- PERFORMER_MODULE_ROOT = SCRIPT_DIR.parent
58
- SCLONG_ROOT = PERFORMER_MODULE_ROOT.parent
59
 
60
  REPO_ROOT = Path(__file__).resolve().parents[2]
61
  ASSET_DIR = REPO_ROOT / "model_dependencies"
62
  DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
63
- WEIGHTS_DIR = REPO_ROOT / "weight" / "0810model"
64
 
65
- if str(PERFORMER_MODULE_ROOT) not in sys.path:
66
- sys.path.insert(0, str(PERFORMER_MODULE_ROOT))
67
  if str(SCLONG_ROOT) not in sys.path:
68
  sys.path.insert(0, str(SCLONG_ROOT))
69
 
70
  import importlib.util
71
 
72
- # Import PerformerModule model
73
- _models_path = os.path.join(PERFORMER_MODULE_ROOT, 'models_performer_module.py')
74
- _spec = importlib.util.spec_from_file_location("models_performer_module", _models_path)
75
  _models_mod = importlib.util.module_from_spec(_spec)
76
  _spec.loader.exec_module(_models_mod)
77
- PerformerEncoderOnly_Module = _models_mod.PerformerEncoderOnly_Module
78
- sanitize_performer_module_config = _models_mod.sanitize_performer_module_config
79
 
80
- # Import module_dataset
81
- _dataset_path = os.path.join(PERFORMER_MODULE_ROOT, 'module_dataset.py')
82
- _spec2 = importlib.util.spec_from_file_location("module_dataset", _dataset_path)
83
  _module_mod = importlib.util.module_from_spec(_spec2)
84
  _spec2.loader.exec_module(_module_mod)
85
- load_module_data = _module_mod.load_module_data
86
 
87
 
88
  # ===================== Utilities =====================
@@ -111,19 +111,19 @@ def reindex_tensor_universal(tensor, index_positions, dim, filler="0", device="c
111
  return reindexed
112
 
113
 
114
- # ===================== PerformerModule Loading =====================
115
 
116
- def load_performer_module_model(
117
  checkpoint_path, hyper_params_path, module_data_dir, module_data_path,
118
  gene_list_path, gene_mapping_path, gene2vec_path, device,
119
  shuffle_module_genes=False, shuffle_seed=2021,
120
  ):
121
- print_sys(f"Loading PerformerModule model from: {checkpoint_path}")
122
 
123
  with open(hyper_params_path, 'rb') as f:
124
  model_params = pickle.load(f)
125
  model_params.pop('module_embed_chunk_padded_gene_budget', None)
126
- model_params = sanitize_performer_module_config(
127
  model_params, source=hyper_params_path, log_fn=print_sys,
128
  )
129
 
@@ -133,7 +133,7 @@ def load_performer_module_model(
133
 
134
  if 'module_sets' not in model_params or model_params.get('module_sets') is None:
135
  if module_data_dir or module_data_path:
136
- module_sets = load_module_data(
137
  module_data_path=module_data_path,
138
  module_data_dir=module_data_dir,
139
  gene_list_path=gene_list_path,
@@ -143,8 +143,8 @@ def load_performer_module_model(
143
  model_params['module_sets'] = module_sets
144
  else:
145
  raise ValueError(
146
- "Model needs module_sets but neither --module_data_dir "
147
- "nor --module_data_path provided"
148
  )
149
 
150
  if shuffle_module_genes:
@@ -164,7 +164,7 @@ def load_performer_module_model(
164
  model_params['module_sets'] = shuffled_module_sets
165
  print_sys(f"Shuffled {len(all_gene_indices)} genes across {len(shuffled_module_sets)} modules")
166
 
167
- model = PerformerEncoderOnly_Module(**model_params).to(device)
168
 
169
  checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
170
  state_dict = checkpoint.get('model_state_dict', checkpoint)
@@ -173,7 +173,7 @@ def load_performer_module_model(
173
  if 'go_conv' not in k and 'G_go' not in k.lower()
174
  }
175
  model.load_state_dict(filtered, strict=True)
176
- print_sys("Loaded pretrained PerformerModule weights.")
177
 
178
  model.eval()
179
  for p in model.parameters():
@@ -182,7 +182,7 @@ def load_performer_module_model(
182
  return model, len(model_params['module_sets']), model_params.get('base_dim', 1024)
183
 
184
 
185
- def build_performer_module_gene_mapping(symbol2ens_path, gene_list_path, expr_genes):
186
  gene2ens = pd.read_csv(symbol2ens_path, index_col=0)
187
  symbol2ens = gene2ens["Ensembl gene ID"].to_dict()
188
 
@@ -191,15 +191,15 @@ def build_performer_module_gene_mapping(symbol2ens_path, gene_list_path, expr_ge
191
  input_mapping = {gid: i for i, gid in enumerate(input_genes)}
192
 
193
  with open(gene_list_path, "r") as f:
194
- performer_genes = [line.rstrip("\n") for line in f.readlines()]
195
- performer_genes_pad = performer_genes + ["PAD"]
196
 
197
- performer_index_positions = [
198
- input_mapping.get(gid, input_gene_num) for gid in performer_genes_pad
199
  ]
200
- intersect = np.intersect1d(input_genes, performer_genes)
201
- print_sys(f"Target and PerformerModule genes intersect: {len(intersect)}")
202
- return input_mapping, performer_index_positions
203
 
204
 
205
  def build_cell_embeddings(
@@ -208,14 +208,14 @@ def build_cell_embeddings(
208
  module_data_path, device, pool_method="mean",
209
  shuffle_module_genes=False, shuffle_seed=2021,
210
  ):
211
- model, num_modules, encoder_embed_dim = load_performer_module_model(
212
  checkpoint_path, hyper_params_path, module_data_dir, module_data_path,
213
  gene_list_path, gene_mapping_path, gene2vec_path, device,
214
  shuffle_module_genes=shuffle_module_genes, shuffle_seed=shuffle_seed,
215
  )
216
 
217
  gene_columns = list(cell_features_df.columns)
218
- _, performer_index_positions = build_performer_module_gene_mapping(
219
  symbol2ens_path, gene_list_path, gene_columns,
220
  )
221
 
@@ -225,10 +225,10 @@ def build_cell_embeddings(
225
  with torch.no_grad():
226
  for cell_line_id, row in cell_features_df.iterrows():
227
  x = torch.tensor(row.values[None, :], dtype=torch.float32, device=device)
228
- x_performer = reindex_tensor_universal(
229
- x, performer_index_positions, dim=1, filler="0", device=device,
230
  )
231
- out = model(x_performer, mask_ratio=0.0, return_encodings=True)
232
 
233
  if 'module_encodings' in out:
234
  module_enc = out['module_encodings']
@@ -251,7 +251,7 @@ def build_cell_embeddings(
251
  first_emb_dim = cell_emb_np.shape[0]
252
 
253
  print_sys(
254
- f"Built PerformerModule embeddings for {len(cell_emb_dict)} cell lines, "
255
  f"dim={first_emb_dim}, pool_method={pool_method}"
256
  )
257
  return cell_emb_dict, first_emb_dim
@@ -319,7 +319,7 @@ class TestbedDataset(InMemoryDataset):
319
  """
320
  PyG InMemoryDataset for drug combination prediction.
321
  Same pattern as scLong/drug_combination/utils_test.py::TestbedDataset,
322
- but uses PerformerModule cell embeddings instead of scfm/geneformer.
323
  """
324
 
325
  def __init__(
@@ -412,7 +412,7 @@ class GCNNet(torch.nn.Module):
412
 
413
  Drug branch: 3-layer GCNConv (78->78->156->312) + global_max_pool + 2 FC layers
414
  Drug1 and drug2 share weights (drug2 uses drug1_conv* layers, same as scLong).
415
- Cell branch: PerformerModule embedding -> MLP -> output_dim
416
  Fusion: concat(drug1, drug2, cell) -> fc1(256) -> fc2(128) -> out(2)
417
  """
418
 
@@ -432,7 +432,7 @@ def __init__(
432
  self.drug1_fc_g1 = nn.Linear(num_features_xd * 4, num_features_xd * 2)
433
  self.drug1_fc_g2 = nn.Linear(num_features_xd * 2, output_dim)
434
 
435
- # Cell branch (PerformerModule embedding -> MLP)
436
  self.reduction = nn.Sequential(
437
  nn.Linear(cell_emb_dim, 512),
438
  nn.ReLU(),
@@ -584,18 +584,18 @@ def load_cell_features(cell_file):
584
 
585
  def main():
586
  parser = argparse.ArgumentParser(
587
- description='Drug Combination Synergy Prediction with PerformerModule'
588
  )
589
 
590
- # PerformerModule model args
591
  parser.add_argument('--checkpoint_path', type=str, required=True,
592
- help='Path to PerformerModule pretrained checkpoint')
593
  parser.add_argument('--hyper_params_path', type=str, required=True,
594
- help='Path to PerformerModule hyper parameters pkl')
595
- parser.add_argument('--module_data_dir', type=str, default=None,
596
- help='Directory containing module data files')
597
- parser.add_argument('--module_data_path', type=str, default=None,
598
- help='Path to single module data file')
599
  parser.add_argument('--gene_list_path', type=str,
600
  default=str(ASSET_DIR / 'selected_genes_27k.txt'))
601
  parser.add_argument('--gene_mapping_path', type=str,
@@ -606,8 +606,8 @@ def main():
606
  default=str(ASSET_DIR / 'gene2ens.csv'))
607
  parser.add_argument('--pool_method', type=str, default='mean',
608
  choices=['mean', 'max', 'concat'])
609
- parser.add_argument('--shuffle_module_genes', action='store_true',
610
- help='Disrupt the gene allocation within the module (used for comparative experiments to verify the effectiveness of module division)')
611
  parser.add_argument('--shuffle_seed', type=int, default=2021,
612
  help='The random seed used when scrambling the module gene (needs to be consistent with the value used during pre-training)')
613
 
@@ -638,7 +638,7 @@ def main():
638
  default=os.path.join(SCRIPT_DIR, 'results'))
639
  parser.add_argument('--seed', type=int, default=42)
640
  parser.add_argument('--cache_emb', action='store_true', default=True,
641
- help='Cache PerformerModule cell embeddings to .npy')
642
  parser.add_argument('--no_cache_emb', dest='cache_emb', action='store_false')
643
  parser.add_argument('--force_preprocess', action='store_true', default=False,
644
  help='Force re-creation of PyG .pt datasets')
@@ -656,7 +656,7 @@ def main():
656
 
657
  os.makedirs(args.output_dir, exist_ok=True)
658
 
659
- # -------- Step 1: Build PerformerModule cell embeddings --------
660
  train_cell_file = os.path.join(args.data_dir, args.cell_file)
661
  train_cell_df = load_cell_features(train_cell_file)
662
  print_sys(f'Loaded train cell features: {train_cell_df.shape}')
@@ -737,12 +737,12 @@ def main():
737
  labels_train = train_labels['label'].tolist()
738
 
739
  train_d1_dataset = TestbedDataset(
740
- root=args.data_dir, dataset='train_drug1_performer_module',
741
  xd=drug1_train, xt=cells_train, y=labels_train,
742
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
743
  )
744
  train_d2_dataset = TestbedDataset(
745
- root=args.data_dir, dataset='train_drug2_performer_module',
746
  xd=drug2_train, xt=cells_train, y=labels_train,
747
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
748
  )
@@ -755,12 +755,12 @@ def main():
755
  labels_validation = validation_labels['label'].tolist()
756
 
757
  validation_d1_dataset = TestbedDataset(
758
- root=args.data_dir, dataset='test_drug1_performer_module',
759
  xd=drug1_validation, xt=cells_validation, y=labels_validation,
760
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
761
  )
762
  validation_d2_dataset = TestbedDataset(
763
- root=args.data_dir, dataset='test_drug2_performer_module',
764
  xd=drug2_validation, xt=cells_validation, y=labels_validation,
765
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
766
  )
 
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  """
4
+ Drug Combination Synergy Prediction with BioMod cell embeddings.
5
 
6
  Aligned with scLong/drug_combination (DeepDDs) architecture:
7
  - Drug branch: PyG GCNConv + global_max_pool (shared weights for drug1 & drug2)
8
+ - Cell branch: BioMod pretrained encoder -> MLP
9
  - Fusion: concat(drug1, drug2, cell) -> MLP -> 2-class classification
10
 
11
  Usage:
12
  # Run from the repository root.
13
+ python BioMod/drug_combination/train_drug_combination_biomod.py \
14
  --checkpoint_path <ckpt> --hyper_params_path <pkl> \
15
+ --biomod_data_dir model_dependencies/biomod_data_all \
16
  --data_dir data/downstream/final_data/drug_combination_synergy/data_summary
17
  """
18
 
 
54
 
55
  # ===================== Path setup =====================
56
  SCRIPT_DIR = Path(__file__).resolve().parent
57
+ BIOMOD_ROOT = SCRIPT_DIR.parent
58
+ SCLONG_ROOT = BIOMOD_ROOT.parent
59
 
60
  REPO_ROOT = Path(__file__).resolve().parents[2]
61
  ASSET_DIR = REPO_ROOT / "model_dependencies"
62
  DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
63
+ WEIGHTS_DIR = REPO_ROOT / "weight"
64
 
65
+ if str(BIOMOD_ROOT) not in sys.path:
66
+ sys.path.insert(0, str(BIOMOD_ROOT))
67
  if str(SCLONG_ROOT) not in sys.path:
68
  sys.path.insert(0, str(SCLONG_ROOT))
69
 
70
  import importlib.util
71
 
72
+ # Import BioMod model
73
+ _models_path = os.path.join(BIOMOD_ROOT, 'models_biomod.py')
74
+ _spec = importlib.util.spec_from_file_location("models_biomod", _models_path)
75
  _models_mod = importlib.util.module_from_spec(_spec)
76
  _spec.loader.exec_module(_models_mod)
77
+ BioMod = _models_mod.BioMod
78
+ sanitize_biomod_config = _models_mod.sanitize_biomod_config
79
 
80
+ # Import biomod_dataset
81
+ _dataset_path = os.path.join(BIOMOD_ROOT, 'biomod_dataset.py')
82
+ _spec2 = importlib.util.spec_from_file_location("biomod_dataset", _dataset_path)
83
  _module_mod = importlib.util.module_from_spec(_spec2)
84
  _spec2.loader.exec_module(_module_mod)
85
+ load_biomod_data = _module_mod.load_biomod_data
86
 
87
 
88
  # ===================== Utilities =====================
 
111
  return reindexed
112
 
113
 
114
+ # ===================== BioMod Loading =====================
115
 
116
+ def load_biomod_model(
117
  checkpoint_path, hyper_params_path, module_data_dir, module_data_path,
118
  gene_list_path, gene_mapping_path, gene2vec_path, device,
119
  shuffle_module_genes=False, shuffle_seed=2021,
120
  ):
121
+ print_sys(f"Loading BioMod model from: {checkpoint_path}")
122
 
123
  with open(hyper_params_path, 'rb') as f:
124
  model_params = pickle.load(f)
125
  model_params.pop('module_embed_chunk_padded_gene_budget', None)
126
+ model_params = sanitize_biomod_config(
127
  model_params, source=hyper_params_path, log_fn=print_sys,
128
  )
129
 
 
133
 
134
  if 'module_sets' not in model_params or model_params.get('module_sets') is None:
135
  if module_data_dir or module_data_path:
136
+ module_sets = load_biomod_data(
137
  module_data_path=module_data_path,
138
  module_data_dir=module_data_dir,
139
  gene_list_path=gene_list_path,
 
143
  model_params['module_sets'] = module_sets
144
  else:
145
  raise ValueError(
146
+ "Model needs module_sets but neither --biomod_data_dir "
147
+ "nor --biomod_data_path provided"
148
  )
149
 
150
  if shuffle_module_genes:
 
164
  model_params['module_sets'] = shuffled_module_sets
165
  print_sys(f"Shuffled {len(all_gene_indices)} genes across {len(shuffled_module_sets)} modules")
166
 
167
+ model = BioMod(**model_params).to(device)
168
 
169
  checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
170
  state_dict = checkpoint.get('model_state_dict', checkpoint)
 
173
  if 'go_conv' not in k and 'G_go' not in k.lower()
174
  }
175
  model.load_state_dict(filtered, strict=True)
176
+ print_sys("Loaded pretrained BioMod weights.")
177
 
178
  model.eval()
179
  for p in model.parameters():
 
182
  return model, len(model_params['module_sets']), model_params.get('base_dim', 1024)
183
 
184
 
185
+ def build_biomod_gene_mapping(symbol2ens_path, gene_list_path, expr_genes):
186
  gene2ens = pd.read_csv(symbol2ens_path, index_col=0)
187
  symbol2ens = gene2ens["Ensembl gene ID"].to_dict()
188
 
 
191
  input_mapping = {gid: i for i, gid in enumerate(input_genes)}
192
 
193
  with open(gene_list_path, "r") as f:
194
+ biomod_genes = [line.rstrip("\n") for line in f.readlines()]
195
+ biomod_genes_pad = biomod_genes + ["PAD"]
196
 
197
+ biomod_index_positions = [
198
+ input_mapping.get(gid, input_gene_num) for gid in biomod_genes_pad
199
  ]
200
+ intersect = np.intersect1d(input_genes, biomod_genes)
201
+ print_sys(f"Target and BioMod genes intersect: {len(intersect)}")
202
+ return input_mapping, biomod_index_positions
203
 
204
 
205
  def build_cell_embeddings(
 
208
  module_data_path, device, pool_method="mean",
209
  shuffle_module_genes=False, shuffle_seed=2021,
210
  ):
211
+ model, num_modules, encoder_embed_dim = load_biomod_model(
212
  checkpoint_path, hyper_params_path, module_data_dir, module_data_path,
213
  gene_list_path, gene_mapping_path, gene2vec_path, device,
214
  shuffle_module_genes=shuffle_module_genes, shuffle_seed=shuffle_seed,
215
  )
216
 
217
  gene_columns = list(cell_features_df.columns)
218
+ _, biomod_index_positions = build_biomod_gene_mapping(
219
  symbol2ens_path, gene_list_path, gene_columns,
220
  )
221
 
 
225
  with torch.no_grad():
226
  for cell_line_id, row in cell_features_df.iterrows():
227
  x = torch.tensor(row.values[None, :], dtype=torch.float32, device=device)
228
+ x_biomod = reindex_tensor_universal(
229
+ x, biomod_index_positions, dim=1, filler="0", device=device,
230
  )
231
+ out = model(x_biomod, mask_ratio=0.0, return_encodings=True)
232
 
233
  if 'module_encodings' in out:
234
  module_enc = out['module_encodings']
 
251
  first_emb_dim = cell_emb_np.shape[0]
252
 
253
  print_sys(
254
+ f"Built BioMod embeddings for {len(cell_emb_dict)} cell lines, "
255
  f"dim={first_emb_dim}, pool_method={pool_method}"
256
  )
257
  return cell_emb_dict, first_emb_dim
 
319
  """
320
  PyG InMemoryDataset for drug combination prediction.
321
  Same pattern as scLong/drug_combination/utils_test.py::TestbedDataset,
322
+ but uses BioMod cell embeddings instead of scfm/geneformer.
323
  """
324
 
325
  def __init__(
 
412
 
413
  Drug branch: 3-layer GCNConv (78->78->156->312) + global_max_pool + 2 FC layers
414
  Drug1 and drug2 share weights (drug2 uses drug1_conv* layers, same as scLong).
415
+ Cell branch: BioMod embedding -> MLP -> output_dim
416
  Fusion: concat(drug1, drug2, cell) -> fc1(256) -> fc2(128) -> out(2)
417
  """
418
 
 
432
  self.drug1_fc_g1 = nn.Linear(num_features_xd * 4, num_features_xd * 2)
433
  self.drug1_fc_g2 = nn.Linear(num_features_xd * 2, output_dim)
434
 
435
+ # Cell branch (BioMod embedding -> MLP)
436
  self.reduction = nn.Sequential(
437
  nn.Linear(cell_emb_dim, 512),
438
  nn.ReLU(),
 
584
 
585
  def main():
586
  parser = argparse.ArgumentParser(
587
+ description='Drug Combination Synergy Prediction with BioMod'
588
  )
589
 
590
+ # BioMod model args
591
  parser.add_argument('--checkpoint_path', type=str, required=True,
592
+ help='Path to BioMod pretrained checkpoint')
593
  parser.add_argument('--hyper_params_path', type=str, required=True,
594
+ help='Path to BioMod hyper parameters pkl')
595
+ parser.add_argument('--biomod_data_dir', '--module_data_dir', type=str, default=None,
596
+ help='Directory containing module data files', dest='module_data_dir')
597
+ parser.add_argument('--biomod_data_path', '--module_data_path', type=str, default=None,
598
+ help='Path to single module data file', dest='module_data_path')
599
  parser.add_argument('--gene_list_path', type=str,
600
  default=str(ASSET_DIR / 'selected_genes_27k.txt'))
601
  parser.add_argument('--gene_mapping_path', type=str,
 
606
  default=str(ASSET_DIR / 'gene2ens.csv'))
607
  parser.add_argument('--pool_method', type=str, default='mean',
608
  choices=['mean', 'max', 'concat'])
609
+ parser.add_argument('--shuffle_biomod_genes', '--shuffle_module_genes', action='store_true',
610
+ help='Disrupt the gene allocation within the module (used for comparative experiments to verify the effectiveness of module division)', dest='shuffle_module_genes')
611
  parser.add_argument('--shuffle_seed', type=int, default=2021,
612
  help='The random seed used when scrambling the module gene (needs to be consistent with the value used during pre-training)')
613
 
 
638
  default=os.path.join(SCRIPT_DIR, 'results'))
639
  parser.add_argument('--seed', type=int, default=42)
640
  parser.add_argument('--cache_emb', action='store_true', default=True,
641
+ help='Cache BioMod cell embeddings to .npy')
642
  parser.add_argument('--no_cache_emb', dest='cache_emb', action='store_false')
643
  parser.add_argument('--force_preprocess', action='store_true', default=False,
644
  help='Force re-creation of PyG .pt datasets')
 
656
 
657
  os.makedirs(args.output_dir, exist_ok=True)
658
 
659
+ # -------- Step 1: Build BioMod cell embeddings --------
660
  train_cell_file = os.path.join(args.data_dir, args.cell_file)
661
  train_cell_df = load_cell_features(train_cell_file)
662
  print_sys(f'Loaded train cell features: {train_cell_df.shape}')
 
737
  labels_train = train_labels['label'].tolist()
738
 
739
  train_d1_dataset = TestbedDataset(
740
+ root=args.data_dir, dataset='train_drug1_biomod',
741
  xd=drug1_train, xt=cells_train, y=labels_train,
742
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
743
  )
744
  train_d2_dataset = TestbedDataset(
745
+ root=args.data_dir, dataset='train_drug2_biomod',
746
  xd=drug2_train, xt=cells_train, y=labels_train,
747
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
748
  )
 
755
  labels_validation = validation_labels['label'].tolist()
756
 
757
  validation_d1_dataset = TestbedDataset(
758
+ root=args.data_dir, dataset='test_drug1_biomod',
759
  xd=drug1_validation, xt=cells_validation, y=labels_validation,
760
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
761
  )
762
  validation_d2_dataset = TestbedDataset(
763
+ root=args.data_dir, dataset='test_drug2_biomod',
764
  xd=drug2_validation, xt=cells_validation, y=labels_validation,
765
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
766
  )
BioMod/drug_response/cmd.txt CHANGED
@@ -1,10 +1,10 @@
1
  # Run from the repository root.
2
- python BioMod/drug_response/train_drug_performer_module.py \
3
  --gpu_id 0 \
4
  --data_path data/downstream/final_data/single_drug_response/data \
5
- --checkpoint_path weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
6
- --hyper_params_path weight/0810model/mae_tost_70M_75mask_ec2.pkl \
7
  --epochs 300 \
8
- --module_data_dir model_dependencies/module_data_all \
9
  --save_dir tutorial_runs/drug_response \
10
  --pool_method max
 
1
  # Run from the repository root.
2
+ python BioMod/drug_response/train_drug_biomod.py \
3
  --gpu_id 0 \
4
  --data_path data/downstream/final_data/single_drug_response/data \
5
+ --checkpoint_path weight/BioMod.pth \
6
+ --hyper_params_path weight/BioMod.pkl \
7
  --epochs 300 \
8
+ --biomod_data_dir model_dependencies/biomod_data_all \
9
  --save_dir tutorial_runs/drug_response \
10
  --pool_method max
BioMod/drug_response/{train_drug_performer_module.py → train_drug_biomod.py} RENAMED
@@ -1,20 +1,20 @@
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  """
4
- Drug response prediction: using drug molecular structure + PerformerModule cell embedding
5
 
6
  Architecture:
7
  - Drug branch: 3-layer GCN + residual connection + double pooling
8
- - Cell branch: extract module-level features from pre-trained PerformerModule
9
  - Fusion layer: attention mechanism + deep MLP
10
  - Optimization strategy: AdamW + cosine annealing + early stopping
11
 
12
  How to use:
13
  # Run from the repository root; see drug_response/cmd.txt for a full example.
14
- python BioMod/drug_response/train_drug_performer_module.py \
15
  --gpu_id 0 --epochs 300 \
16
- --checkpoint_path weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
17
- --hyper_params_path weight/0810model/mae_tost_70M_75mask_ec2.pkl
18
  """
19
 
20
  import argparse
@@ -27,7 +27,7 @@
27
  REPO_ROOT = Path(__file__).resolve().parents[2]
28
  ASSET_DIR = REPO_ROOT / "model_dependencies"
29
  DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
30
- WEIGHTS_DIR = REPO_ROOT / "weight" / "0810model"
31
  from typing import Dict, List, Tuple, Union, Optional
32
 
33
  import hickle as hkl
@@ -39,7 +39,7 @@
39
  from scipy.stats import pearsonr
40
  from torch.utils.data import DataLoader, Dataset
41
 
42
- # Import PerformerModule model
43
  project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
44
  if project_root not in sys.path:
45
  sys.path.insert(0, project_root)
@@ -47,26 +47,26 @@
47
 
48
  # Use direct path import to avoid conflicts with files with the same name
49
  import importlib.util
50
- performer_module_dir = project_root
51
 
52
- # import models_performer_module
53
- models_path = os.path.join(performer_module_dir, 'models_performer_module.py')
54
  if not os.path.exists(models_path):
55
  raise FileNotFoundError(f"Model file not found: {models_path}")
56
- spec = importlib.util.spec_from_file_location("models_performer_module", models_path)
57
- models_performer_module = importlib.util.module_from_spec(spec)
58
- spec.loader.exec_module(models_performer_module)
59
- PerformerEncoderOnly_Module = models_performer_module.PerformerEncoderOnly_Module
60
- sanitize_performer_module_config = models_performer_module.sanitize_performer_module_config
61
-
62
- # Import module_dataset
63
- dataset_path = os.path.join(performer_module_dir, 'module_dataset.py')
64
  if not os.path.exists(dataset_path):
65
  raise FileNotFoundError(f"Dataset file not found: {dataset_path}")
66
- spec = importlib.util.spec_from_file_location("module_dataset", dataset_path)
67
- module_dataset_module = importlib.util.module_from_spec(spec)
68
- spec.loader.exec_module(module_dataset_module)
69
- load_module_data = module_dataset_module.load_module_data
70
 
71
 
72
  # ===================== Utility functions =====================
@@ -105,9 +105,9 @@ def reindex_tensor_universal(
105
  return reindexed
106
 
107
 
108
- # ===================== PerformerModule related =====================
109
 
110
- def load_performer_module_model(
111
  checkpoint_path: str,
112
  hyper_params_path: str,
113
  module_data_dir: Optional[str],
@@ -118,16 +118,16 @@ def load_performer_module_model(
118
  device: torch.device,
119
  shuffle_module_genes: bool = False,
120
  seed: int = 0,
121
- ) -> PerformerEncoderOnly_Module:
122
- """Load a pretrained PerformerModule model"""
123
- print_sys(f"Loading PerformerModule model from: {checkpoint_path}")
124
 
125
  # Load model hyperparameters
126
  print_sys(f"Loading hyper parameters from: {hyper_params_path}")
127
  with open(hyper_params_path, 'rb') as f:
128
  model_params = pickle.load(f)
129
  model_params.pop('module_embed_chunk_padded_gene_budget', None)
130
- model_params = sanitize_performer_module_config(
131
  model_params,
132
  source=hyper_params_path,
133
  log_fn=print_sys,
@@ -150,7 +150,7 @@ def load_performer_module_model(
150
  if 'module_sets' not in model_params or model_params.get('module_sets') is None:
151
  if module_data_dir or module_data_path:
152
  print_sys("Loading module data...")
153
- module_sets = load_module_data(
154
  module_data_path=module_data_path,
155
  module_data_dir=module_data_dir,
156
  gene_list_path=gene_list_path,
@@ -159,7 +159,7 @@ def load_performer_module_model(
159
  )
160
  model_params['module_sets'] = module_sets
161
  else:
162
- raise ValueError("Model requires module_sets, but --module_data_dir or --module_data_path was not provided")
163
  else:
164
  module_sets = model_params['module_sets']
165
  print_sys(f"Using module_sets from hyper parameters ({len(module_sets)} modules)")
@@ -191,11 +191,11 @@ def load_performer_module_model(
191
  model_params['module_sets'] = module_sets
192
  print_sys(f"Shuffled {len(all_gene_indices)} genes across {len(module_sets)} modules")
193
 
194
- print_sys(f"PerformerModule config: base_dim={model_params.get('base_dim', 'N/A')}, "
195
  f"num_modules={len(module_sets)}")
196
 
197
  # Build model
198
- model = PerformerEncoderOnly_Module(**model_params).to(device)
199
 
200
  # Load weights
201
  print_sys(f"Loading checkpoint from: {checkpoint_path}")
@@ -216,7 +216,7 @@ def load_performer_module_model(
216
  # Use strict=True to ensure the weights match exactly (except for filtered GO parameters)
217
  model.load_state_dict(filtered_state_dict, strict=True)
218
 
219
- print_sys("Loaded pretrained PerformerModule weights.")
220
 
221
  # freeze model
222
  model.eval()
@@ -226,7 +226,7 @@ def load_performer_module_model(
226
  return model, len(module_sets), model_params.get('base_dim', 1024)
227
 
228
 
229
- def build_performer_module_gene_mapping(
230
  symbol2ens_path: str,
231
  gene_list_path: str,
232
  expr_genes: List[str],
@@ -240,18 +240,18 @@ def build_performer_module_gene_mapping(
240
  input_mapping = {gid: i for i, gid in enumerate(input_genes)}
241
 
242
  with open(gene_list_path, "r") as f:
243
- performer_genes = [line.rstrip("\n") for line in f.readlines()]
244
- performer_genes_pad = performer_genes + ["PAD"]
245
 
246
- performer_index_positions = [
247
- input_mapping.get(gid, input_gene_num) for gid in performer_genes_pad
248
  ]
249
- intersect = np.intersect1d(input_genes, performer_genes)
250
- print_sys(f"Target and PerformerModule genes intersect: {len(intersect)}")
251
- return input_mapping, performer_index_positions
252
 
253
 
254
- def build_cell_embeddings_with_performer_module(
255
  gexpr_feature: pd.DataFrame,
256
  symbol2ens_path: str,
257
  gene_list_path: str,
@@ -267,7 +267,7 @@ def build_cell_embeddings_with_performer_module(
267
  seed: int = 0,
268
  ) -> Tuple[Dict[str, np.ndarray], int]:
269
  """
270
- Use PerformerModule to construct embedding for each cell line
271
 
272
  Args:
273
  pool_method: pooling method, 'mean', 'max', 'cls', or 'concat'
@@ -277,7 +277,7 @@ def build_cell_embeddings_with_performer_module(
277
  - 'concat': concatenate all modules (the dimension will be very large)
278
  use_gene2vec: automatically restored by pre-trained PKL, no longer covered downstream
279
  """
280
- model, num_modules, encoder_embed_dim = load_performer_module_model(
281
  checkpoint_path=checkpoint_path,
282
  hyper_params_path=hyper_params_path,
283
  module_data_dir=module_data_dir,
@@ -290,7 +290,7 @@ def build_cell_embeddings_with_performer_module(
290
  seed=seed,
291
  )
292
 
293
- _, performer_index_positions = build_performer_module_gene_mapping(
294
  symbol2ens_path=symbol2ens_path,
295
  gene_list_path=gene_list_path,
296
  expr_genes=list(gexpr_feature.columns),
@@ -302,12 +302,12 @@ def build_cell_embeddings_with_performer_module(
302
  with torch.no_grad():
303
  for cell_line_id, row in gexpr_feature.iterrows():
304
  x = torch.tensor(row.values[None, :], dtype=torch.float32, device=device)
305
- x_performer = reindex_tensor_universal(
306
- x, performer_index_positions, dim=1, filler="0", device=device
307
  )
308
 
309
  # Extract features through encoder (without masking, mask_ratio=0.0)
310
- out = model(x_performer, mask_ratio=0.0, return_encodings=True)
311
 
312
  # Extract module encodings
313
  if 'module_encodings' in out:
@@ -318,7 +318,7 @@ def build_cell_embeddings_with_performer_module(
318
  if model.use_cls:
319
  # Use CLS token (in latent, but module_encodings does not contain CLS)
320
  # Need to get the complete latent from forward_encoder
321
- enc_out = model.forward_encoder(x_performer, mask_ratio=0.0)
322
  latent = enc_out[0]
323
  if model.use_cls:
324
  cell_emb = latent[:, 0, :] # (1, encoder_embed_dim)
@@ -345,7 +345,7 @@ def build_cell_embeddings_with_performer_module(
345
  first_emb_dim = cell_emb_np.shape[0]
346
 
347
  print_sys(
348
- f"Built PerformerModule embeddings for {len(cell_emb_dict)} cell lines, "
349
  f"dim={first_emb_dim}, pool_method={pool_method}"
350
  )
351
  return cell_emb_dict, first_emb_dim
@@ -525,8 +525,8 @@ def feature_extract(
525
 
526
  # ===================== Dataset =====================
527
 
528
- class DrugPerformerModuleDataset(Dataset):
529
- """Drug + PerformerModule Dataset"""
530
 
531
  def __init__(
532
  self,
@@ -550,7 +550,7 @@ def __getitem__(self, idx: int):
550
  drug_feat = torch.from_numpy(feat).float()
551
  drug_adj = torch.from_numpy(adj).float()
552
 
553
- # PerformerModule cell embedding
554
  cell_line_id = self.index_list[idx][0]
555
  cell_emb_np = self.cell_emb_dict[cell_line_id]
556
  cell_emb = torch.from_numpy(cell_emb_np).float()
@@ -595,13 +595,13 @@ def forward(self, x: torch.Tensor, adj: torch.Tensor) -> torch.Tensor:
595
  return out
596
 
597
 
598
- class DrugPerformerModuleModel(nn.Module):
599
  """
600
- Drugs + PerformerModule Predictive Model
601
 
602
  Architecture:
603
  - Drug branch: 3-layer GraphConv + residual connection + double pooling
604
- - Cell branch: 2-layer MLP (processing PerformerModule module embeddings)
605
  - Fusion layer: 4-layer MLP + attention mechanism
606
  """
607
 
@@ -624,7 +624,7 @@ def __init__(
624
  self.drug_bn2 = nn.BatchNorm1d(256)
625
  self.drug_bn3 = nn.BatchNorm1d(128)
626
 
627
- # Cell branch: 2-layer MLP (processing PerformerModule embeddings)
628
  self.cell_fc1 = nn.Linear(cell_emb_dim, 256)
629
  self.cell_bn1 = nn.BatchNorm1d(256)
630
  self.cell_fc2 = nn.Linear(256, 128)
@@ -689,7 +689,7 @@ def forward(
689
  x_drug_mean = x_drug.mean(dim=1) # (B, 128)
690
  x_drug = x_drug_max + x_drug_mean # Fusion
691
 
692
- # Cell branching (handling PerformerModule embeddings)
693
  x_cell = self.cell_fc1(cell_emb) # (B, 256)
694
  x_cell = self.cell_bn1(x_cell)
695
  x_cell = self.relu(x_cell)
@@ -783,7 +783,7 @@ def eval_epoch(model, loader, device):
783
 
784
  def main():
785
  parser = argparse.ArgumentParser(
786
- description="Drug response prediction using Drug + PerformerModule"
787
  )
788
  parser.add_argument("--gpu_id", type=str, default="0")
789
  parser.add_argument("--data_path", type=str, default=str(DOWNSTREAM_DATA_DIR / "single_drug_response" / "data"))
@@ -795,31 +795,31 @@ def main():
795
  parser.add_argument("--dropout", type=float, default=0.1)
796
  parser.add_argument("--patience", type=int, default=30)
797
 
798
- # PerformerModule related paths
799
  parser.add_argument(
800
  "--checkpoint_path",
801
  type=str,
802
  required=True,
803
- help="Path to pre-trained PerformerModule checkpoint"
804
  )
805
  parser.add_argument(
806
  "--hyper_params_path",
807
  type=str,
808
  required=True,
809
- help="Path to PerformerModule model hyper parameters pkl file"
810
  )
811
  parser.add_argument(
812
- "--module_data_dir",
813
  type=str,
814
  default=None,
815
  help="Directory containing module JSON files"
816
- )
817
  parser.add_argument(
818
- "--module_data_path",
819
  type=str,
820
  default=None,
821
  help="Path to module JSON file (single file)"
822
- )
823
  parser.add_argument(
824
  "--gene_list_path",
825
  type=str,
@@ -849,25 +849,25 @@ def main():
849
  type=str,
850
  default="mean",
851
  choices=["mean", "max", "cls", "concat"],
852
- help="Pooling method for PerformerModule module embeddings"
853
  )
854
  parser.add_argument(
855
- "--shuffle_module_genes",
856
  action="store_true",
857
  help="Optional downstream-only extra reshuffle on top of module_sets restored from PKL. "
858
  "Do not pass this when reproducing the pretrained backbone as-is."
859
- )
860
  parser.add_argument(
861
  "--shuffle_seed",
862
  type=int,
863
  default=2021,
864
- help="Random seed for the downstream extra reshuffle when --shuffle_module_genes is enabled."
865
  )
866
  # Output path
867
  parser.add_argument(
868
  "--save_dir",
869
  type=str,
870
- default="./models_drug_performer_module",
871
  help="Directory to save the model"
872
  )
873
 
@@ -877,7 +877,7 @@ def main():
877
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
878
 
879
  print_sys("=" * 70)
880
- print_sys("Drug Response Prediction: Drug + PerformerModule")
881
  print_sys("=" * 70)
882
  print_sys(f"Device: {device}")
883
  print_sys(f"Epochs: {args.epochs}")
@@ -916,9 +916,9 @@ def main():
916
  gene_expression_file,
917
  )
918
 
919
- # 2) Build PerformerModule embeddings
920
- print_sys("\n[Step 2/6] Building PerformerModule embeddings...")
921
- cell_emb_dict, cell_emb_dim = build_cell_embeddings_with_performer_module( # cell_emb_dict │ Dict[str, np.ndarray] │ {cell_line_id: embedding} mapping dictionary
922
  gexpr_feature=gexpr_feature,
923
  symbol2ens_path=args.symbol2ens_path,
924
  gene_list_path=args.gene_list_path,
@@ -950,10 +950,10 @@ def main():
950
 
951
  # 5) Build a data set
952
  print_sys("\n[Step 5/6] Creating datasets...")
953
- train_ds = DrugPerformerModuleDataset(
954
  X_drug_train, Y_train, train_index_list, cell_emb_dict
955
  )
956
- validation_ds = DrugPerformerModuleDataset(
957
  X_drug_validation, Y_validation, validation_index_list, cell_emb_dict
958
  )
959
 
@@ -977,7 +977,7 @@ def main():
977
 
978
  # 6) Build the model
979
  print_sys("\n[Step 6/6] Building model...")
980
- model = DrugPerformerModuleModel(
981
  drug_feat_dim=drug_dim,
982
  cell_emb_dim=cell_emb_dim,
983
  hidden_dim=args.hidden_dim,
 
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
  """
4
+ Drug response prediction: using drug molecular structure + BioMod cell embedding
5
 
6
  Architecture:
7
  - Drug branch: 3-layer GCN + residual connection + double pooling
8
+ - Cell branch: extract module-level features from pre-trained BioMod
9
  - Fusion layer: attention mechanism + deep MLP
10
  - Optimization strategy: AdamW + cosine annealing + early stopping
11
 
12
  How to use:
13
  # Run from the repository root; see drug_response/cmd.txt for a full example.
14
+ python BioMod/drug_response/train_drug_biomod.py \
15
  --gpu_id 0 --epochs 300 \
16
+ --checkpoint_path weight/BioMod.pth \
17
+ --hyper_params_path weight/BioMod.pkl
18
  """
19
 
20
  import argparse
 
27
  REPO_ROOT = Path(__file__).resolve().parents[2]
28
  ASSET_DIR = REPO_ROOT / "model_dependencies"
29
  DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
30
+ WEIGHTS_DIR = REPO_ROOT / "weight"
31
  from typing import Dict, List, Tuple, Union, Optional
32
 
33
  import hickle as hkl
 
39
  from scipy.stats import pearsonr
40
  from torch.utils.data import DataLoader, Dataset
41
 
42
+ # Import BioMod model
43
  project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
44
  if project_root not in sys.path:
45
  sys.path.insert(0, project_root)
 
47
 
48
  # Use direct path import to avoid conflicts with files with the same name
49
  import importlib.util
50
+ biomod_dir = project_root
51
 
52
+ # import models_biomod
53
+ models_path = os.path.join(biomod_dir, 'models_biomod.py')
54
  if not os.path.exists(models_path):
55
  raise FileNotFoundError(f"Model file not found: {models_path}")
56
+ spec = importlib.util.spec_from_file_location("models_biomod", models_path)
57
+ models_biomod = importlib.util.module_from_spec(spec)
58
+ spec.loader.exec_module(models_biomod)
59
+ BioMod = models_biomod.BioMod
60
+ sanitize_biomod_config = models_biomod.sanitize_biomod_config
61
+
62
+ # Import biomod_dataset
63
+ dataset_path = os.path.join(biomod_dir, 'biomod_dataset.py')
64
  if not os.path.exists(dataset_path):
65
  raise FileNotFoundError(f"Dataset file not found: {dataset_path}")
66
+ spec = importlib.util.spec_from_file_location("biomod_dataset", dataset_path)
67
+ biomod_dataset_module = importlib.util.module_from_spec(spec)
68
+ spec.loader.exec_module(biomod_dataset_module)
69
+ load_biomod_data = biomod_dataset_module.load_biomod_data
70
 
71
 
72
  # ===================== Utility functions =====================
 
105
  return reindexed
106
 
107
 
108
+ # ===================== BioMod related =====================
109
 
110
+ def load_biomod_model(
111
  checkpoint_path: str,
112
  hyper_params_path: str,
113
  module_data_dir: Optional[str],
 
118
  device: torch.device,
119
  shuffle_module_genes: bool = False,
120
  seed: int = 0,
121
+ ) -> BioMod:
122
+ """Load a pretrained BioMod model"""
123
+ print_sys(f"Loading BioMod model from: {checkpoint_path}")
124
 
125
  # Load model hyperparameters
126
  print_sys(f"Loading hyper parameters from: {hyper_params_path}")
127
  with open(hyper_params_path, 'rb') as f:
128
  model_params = pickle.load(f)
129
  model_params.pop('module_embed_chunk_padded_gene_budget', None)
130
+ model_params = sanitize_biomod_config(
131
  model_params,
132
  source=hyper_params_path,
133
  log_fn=print_sys,
 
150
  if 'module_sets' not in model_params or model_params.get('module_sets') is None:
151
  if module_data_dir or module_data_path:
152
  print_sys("Loading module data...")
153
+ module_sets = load_biomod_data(
154
  module_data_path=module_data_path,
155
  module_data_dir=module_data_dir,
156
  gene_list_path=gene_list_path,
 
159
  )
160
  model_params['module_sets'] = module_sets
161
  else:
162
+ raise ValueError("Model requires module_sets, but --biomod_data_dir or --biomod_data_path was not provided")
163
  else:
164
  module_sets = model_params['module_sets']
165
  print_sys(f"Using module_sets from hyper parameters ({len(module_sets)} modules)")
 
191
  model_params['module_sets'] = module_sets
192
  print_sys(f"Shuffled {len(all_gene_indices)} genes across {len(module_sets)} modules")
193
 
194
+ print_sys(f"BioMod config: base_dim={model_params.get('base_dim', 'N/A')}, "
195
  f"num_modules={len(module_sets)}")
196
 
197
  # Build model
198
+ model = BioMod(**model_params).to(device)
199
 
200
  # Load weights
201
  print_sys(f"Loading checkpoint from: {checkpoint_path}")
 
216
  # Use strict=True to ensure the weights match exactly (except for filtered GO parameters)
217
  model.load_state_dict(filtered_state_dict, strict=True)
218
 
219
+ print_sys("Loaded pretrained BioMod weights.")
220
 
221
  # freeze model
222
  model.eval()
 
226
  return model, len(module_sets), model_params.get('base_dim', 1024)
227
 
228
 
229
+ def build_biomod_gene_mapping(
230
  symbol2ens_path: str,
231
  gene_list_path: str,
232
  expr_genes: List[str],
 
240
  input_mapping = {gid: i for i, gid in enumerate(input_genes)}
241
 
242
  with open(gene_list_path, "r") as f:
243
+ biomod_genes = [line.rstrip("\n") for line in f.readlines()]
244
+ biomod_genes_pad = biomod_genes + ["PAD"]
245
 
246
+ biomod_index_positions = [
247
+ input_mapping.get(gid, input_gene_num) for gid in biomod_genes_pad
248
  ]
249
+ intersect = np.intersect1d(input_genes, biomod_genes)
250
+ print_sys(f"Target and BioMod genes intersect: {len(intersect)}")
251
+ return input_mapping, biomod_index_positions
252
 
253
 
254
+ def build_cell_embeddings_with_biomod(
255
  gexpr_feature: pd.DataFrame,
256
  symbol2ens_path: str,
257
  gene_list_path: str,
 
267
  seed: int = 0,
268
  ) -> Tuple[Dict[str, np.ndarray], int]:
269
  """
270
+ Use BioMod to construct embedding for each cell line
271
 
272
  Args:
273
  pool_method: pooling method, 'mean', 'max', 'cls', or 'concat'
 
277
  - 'concat': concatenate all modules (the dimension will be very large)
278
  use_gene2vec: automatically restored by pre-trained PKL, no longer covered downstream
279
  """
280
+ model, num_modules, encoder_embed_dim = load_biomod_model(
281
  checkpoint_path=checkpoint_path,
282
  hyper_params_path=hyper_params_path,
283
  module_data_dir=module_data_dir,
 
290
  seed=seed,
291
  )
292
 
293
+ _, biomod_index_positions = build_biomod_gene_mapping(
294
  symbol2ens_path=symbol2ens_path,
295
  gene_list_path=gene_list_path,
296
  expr_genes=list(gexpr_feature.columns),
 
302
  with torch.no_grad():
303
  for cell_line_id, row in gexpr_feature.iterrows():
304
  x = torch.tensor(row.values[None, :], dtype=torch.float32, device=device)
305
+ x_biomod = reindex_tensor_universal(
306
+ x, biomod_index_positions, dim=1, filler="0", device=device
307
  )
308
 
309
  # Extract features through encoder (without masking, mask_ratio=0.0)
310
+ out = model(x_biomod, mask_ratio=0.0, return_encodings=True)
311
 
312
  # Extract module encodings
313
  if 'module_encodings' in out:
 
318
  if model.use_cls:
319
  # Use CLS token (in latent, but module_encodings does not contain CLS)
320
  # Need to get the complete latent from forward_encoder
321
+ enc_out = model.forward_encoder(x_biomod, mask_ratio=0.0)
322
  latent = enc_out[0]
323
  if model.use_cls:
324
  cell_emb = latent[:, 0, :] # (1, encoder_embed_dim)
 
345
  first_emb_dim = cell_emb_np.shape[0]
346
 
347
  print_sys(
348
+ f"Built BioMod embeddings for {len(cell_emb_dict)} cell lines, "
349
  f"dim={first_emb_dim}, pool_method={pool_method}"
350
  )
351
  return cell_emb_dict, first_emb_dim
 
525
 
526
  # ===================== Dataset =====================
527
 
528
+ class DrugBioModDataset(Dataset):
529
+ """Drug + BioMod Dataset"""
530
 
531
  def __init__(
532
  self,
 
550
  drug_feat = torch.from_numpy(feat).float()
551
  drug_adj = torch.from_numpy(adj).float()
552
 
553
+ # BioMod cell embedding
554
  cell_line_id = self.index_list[idx][0]
555
  cell_emb_np = self.cell_emb_dict[cell_line_id]
556
  cell_emb = torch.from_numpy(cell_emb_np).float()
 
595
  return out
596
 
597
 
598
+ class DrugBioModModel(nn.Module):
599
  """
600
+ Drugs + BioMod Predictive Model
601
 
602
  Architecture:
603
  - Drug branch: 3-layer GraphConv + residual connection + double pooling
604
+ - Cell branch: 2-layer MLP (processing BioMod module embeddings)
605
  - Fusion layer: 4-layer MLP + attention mechanism
606
  """
607
 
 
624
  self.drug_bn2 = nn.BatchNorm1d(256)
625
  self.drug_bn3 = nn.BatchNorm1d(128)
626
 
627
+ # Cell branch: 2-layer MLP (processing BioMod embeddings)
628
  self.cell_fc1 = nn.Linear(cell_emb_dim, 256)
629
  self.cell_bn1 = nn.BatchNorm1d(256)
630
  self.cell_fc2 = nn.Linear(256, 128)
 
689
  x_drug_mean = x_drug.mean(dim=1) # (B, 128)
690
  x_drug = x_drug_max + x_drug_mean # Fusion
691
 
692
+ # Cell branching (handling BioMod embeddings)
693
  x_cell = self.cell_fc1(cell_emb) # (B, 256)
694
  x_cell = self.cell_bn1(x_cell)
695
  x_cell = self.relu(x_cell)
 
783
 
784
  def main():
785
  parser = argparse.ArgumentParser(
786
+ description="Drug response prediction using Drug + BioMod"
787
  )
788
  parser.add_argument("--gpu_id", type=str, default="0")
789
  parser.add_argument("--data_path", type=str, default=str(DOWNSTREAM_DATA_DIR / "single_drug_response" / "data"))
 
795
  parser.add_argument("--dropout", type=float, default=0.1)
796
  parser.add_argument("--patience", type=int, default=30)
797
 
798
+ # BioMod related paths
799
  parser.add_argument(
800
  "--checkpoint_path",
801
  type=str,
802
  required=True,
803
+ help="Path to pre-trained BioMod checkpoint"
804
  )
805
  parser.add_argument(
806
  "--hyper_params_path",
807
  type=str,
808
  required=True,
809
+ help="Path to BioMod model hyper parameters pkl file"
810
  )
811
  parser.add_argument(
812
+ "--biomod_data_dir", "--module_data_dir",
813
  type=str,
814
  default=None,
815
  help="Directory containing module JSON files"
816
+ , dest='module_data_dir')
817
  parser.add_argument(
818
+ "--biomod_data_path", "--module_data_path",
819
  type=str,
820
  default=None,
821
  help="Path to module JSON file (single file)"
822
+ , dest='module_data_path')
823
  parser.add_argument(
824
  "--gene_list_path",
825
  type=str,
 
849
  type=str,
850
  default="mean",
851
  choices=["mean", "max", "cls", "concat"],
852
+ help="Pooling method for BioMod module embeddings"
853
  )
854
  parser.add_argument(
855
+ "--shuffle_biomod_genes", "--shuffle_module_genes",
856
  action="store_true",
857
  help="Optional downstream-only extra reshuffle on top of module_sets restored from PKL. "
858
  "Do not pass this when reproducing the pretrained backbone as-is."
859
+ , dest='shuffle_module_genes')
860
  parser.add_argument(
861
  "--shuffle_seed",
862
  type=int,
863
  default=2021,
864
+ help="Random seed for the downstream extra reshuffle when --shuffle_biomod_genes is enabled."
865
  )
866
  # Output path
867
  parser.add_argument(
868
  "--save_dir",
869
  type=str,
870
+ default="./models_drug_biomod",
871
  help="Directory to save the model"
872
  )
873
 
 
877
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
878
 
879
  print_sys("=" * 70)
880
+ print_sys("Drug Response Prediction: Drug + BioMod")
881
  print_sys("=" * 70)
882
  print_sys(f"Device: {device}")
883
  print_sys(f"Epochs: {args.epochs}")
 
916
  gene_expression_file,
917
  )
918
 
919
+ # 2) Build BioMod embeddings
920
+ print_sys("\n[Step 2/6] Building BioMod embeddings...")
921
+ cell_emb_dict, cell_emb_dim = build_cell_embeddings_with_biomod( # cell_emb_dict │ Dict[str, np.ndarray] │ {cell_line_id: embedding} mapping dictionary
922
  gexpr_feature=gexpr_feature,
923
  symbol2ens_path=args.symbol2ens_path,
924
  gene_list_path=args.gene_list_path,
 
950
 
951
  # 5) Build a data set
952
  print_sys("\n[Step 5/6] Creating datasets...")
953
+ train_ds = DrugBioModDataset(
954
  X_drug_train, Y_train, train_index_list, cell_emb_dict
955
  )
956
+ validation_ds = DrugBioModDataset(
957
  X_drug_validation, Y_validation, validation_index_list, cell_emb_dict
958
  )
959
 
 
977
 
978
  # 6) Build the model
979
  print_sys("\n[Step 6/6] Building model...")
980
+ model = DrugBioModModel(
981
  drug_feat_dim=drug_dim,
982
  cell_emb_dim=cell_emb_dim,
983
  hidden_dim=args.hidden_dim,
BioMod/environment.yml CHANGED
@@ -1,4 +1,4 @@
1
- name: performer_module
2
  channels:
3
  - pytorch
4
  - nvidia
@@ -45,7 +45,7 @@ dependencies:
45
  - rdkit
46
 
47
  - pip:
48
- # PerformerModule model utilities
49
  - einops==0.8.1
50
  - local-attention==1.11.2
51
  - hickle>=5.0.0
 
1
+ name: biomod
2
  channels:
3
  - pytorch
4
  - nvidia
 
45
  - rdkit
46
 
47
  - pip:
48
+ # BioMod model utilities
49
  - einops==0.8.1
50
  - local-attention==1.11.2
51
  - hickle>=5.0.0
BioMod/gene_perturbation/README.md CHANGED
@@ -1,14 +1,14 @@
1
  # Gene Perturbation Prediction with BioMod
2
 
3
  This document describes the BioMod gene perturbation workflow (GEARS), using
4
- the `PerformerEncoderOnly_Module` encoder. The
5
  [BioMod model weights and matching configuration](../../README.md#biomod-model-weights)
6
  are shared with the other downstream tasks.
7
 
8
  ## Overview
9
 
10
  This adaptation replaces the original GEARS gene perturbation model based on
11
- `DualEncoderSCFM` with `PerformerEncoderOnly_Module`. The main adaptation points
12
  are:
13
 
14
  1. **Model architecture adaptation**: replace the original dual-encoder model with a module-based encoder-only model.
@@ -17,12 +17,12 @@ are:
17
 
18
  ## Main Adaptations
19
 
20
- ### 1. `SCFM_Module` (`model_module.py`)
21
 
22
- `SCFM_Module` wraps `PerformerEncoderOnly_Module` and provides these functions:
23
 
24
  - **Input handling**: reindex input gene expression values to the BioMod gene order.
25
- - **Module encoding**: obtain module-level encodings from `PerformerEncoderOnly_Module`.
26
  - **Gene embedding recovery**: recover gene-level embeddings from module encodings.
27
  - For each gene, find all modules that contain it.
28
  - Average the encodings of those modules to obtain the gene embedding.
@@ -42,22 +42,22 @@ for gene_idx in range(len(self.scfm_index_positions) - 1):
42
  gene_emb = module_encodings.mean(dim=1) # B, D (global average)
43
  ```
44
 
45
- ### 2. `GEARS_Model_Module` (`model_module.py`)
46
 
47
- `GEARS_Model_Module` inherits the original GEARS architecture. Main changes:
48
 
49
- - **Use `SCFM_Module`** instead of the original `SCFM_different_genes`.
50
  - **Module data loading** supports loading module data from hyper-parameters or file paths.
51
  - **Other components remain unchanged**, including the GNN layers and decoder.
52
 
53
- ### 3. Training Script (`train_module.py`)
54
 
55
  The training script is based on `GEARS/train_dual_ddp_reduce_dec.py`. Main changes:
56
 
57
- - **Model class**: use `GEARS_Model_Module` instead of `GEARS_Model_Acc`.
58
  - **New arguments**:
59
- - `--module_data_path`: path to one module JSON file.
60
- - `--module_data_dir`: directory containing module JSON files.
61
  - `--gene_mapping_path`: CSV file mapping gene symbols to Ensembl IDs.
62
 
63
  ## Usage
@@ -78,12 +78,12 @@ Run from the repository root in a Linux CUDA/NCCL environment. Built-in asset de
78
  ```bash
79
  # DDP training on multiple GPUs.
80
  python -m torch.distributed.run --standalone --nproc_per_node=4 \
81
- BioMod/gene_perturbation/train_module.py \
82
  --data_dir data/downstream/final_data \
83
  --data_name norman \
84
- --scfm_hyper_params_path weight/0810model/mae_tost_70M_75mask_ec2.pkl \
85
- --scfm_ckpt_path weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
86
- --module_data_dir model_dependencies/module_data_all \
87
  --gene_mapping_path model_dependencies/gene2ens.csv \
88
  --scfm_genes_list_path model_dependencies/selected_genes_27k.txt \
89
  --scfm_gene2vec_file model_dependencies/selected_gene2vec_27k.npy \
@@ -100,7 +100,7 @@ python -m torch.distributed.run --standalone --nproc_per_node=4 \
100
 
101
  - `--scfm_hyper_params_path`: path to the BioMod model configuration file.
102
  - `--scfm_ckpt_path`: path to the BioMod model weights.
103
- - `--module_data_path` or `--module_data_dir`: required only when the model configuration does not already contain module definitions.
104
 
105
  **Recommended arguments**:
106
 
@@ -110,7 +110,7 @@ python -m torch.distributed.run --standalone --nproc_per_node=4 \
110
 
111
  **Optional arguments**:
112
 
113
- - `--use_module_level`: use module-level features. This is experimental and is not fully supported at the moment.
114
  - Default: False, meaning gene-level features are used and recommended.
115
 
116
  ## Comparison with Original sclong
@@ -119,7 +119,7 @@ python -m torch.distributed.run --standalone --nproc_per_node=4 \
119
 
120
  1. **Model architecture**:
121
  - **Original sclong**: uses `DualEncoderSCFM` and directly processes gene sequences.
122
- - **Module version**: uses `PerformerEncoderOnly_Module` and processes module-based data.
123
 
124
  2. **Data representation**:
125
  - **Original sclong**: gene-level embeddings come directly from encoder outputs.
@@ -144,7 +144,7 @@ To compare model performance:
144
 
145
  ```bash
146
  # Run from the BioMod repository root.
147
- python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_perturbation/train_module.py [arguments...]
148
  ```
149
 
150
  3. **Compare metrics**:
@@ -161,7 +161,7 @@ module version.
161
 
162
  ### Module-to-Gene Mapping
163
 
164
- In `SCFM_Module`, `gene_to_modules` records which modules contain each gene:
165
 
166
  ```python
167
  self.gene_to_modules = {} # {gene_idx: [module_idx1, module_idx2, ...]}
@@ -193,11 +193,11 @@ BioMod/gene_perturbation/ # Perturbation response
193
  ├── data_utils_local.py
194
  ├── gears_reduce_local.py
195
  ├── inference_local.py
196
- ├── model_module.py
197
  ├── pertdata_local.py
198
  ├── README.md
199
  ├── scfm_utils_local.py
200
- ├── train_module.py
201
  └── utils.py
202
  ```
203
 
@@ -212,7 +212,7 @@ BioMod/gene_perturbation/ # Perturbation response
212
 
213
  ### Issue 1: Module Data Not Found
214
 
215
- - Check whether `--module_data_path` or `--module_data_dir` is correct.
216
  - Make sure the module data file exists and has the expected format.
217
 
218
  ### Issue 2: Gene Mapping Failed
@@ -228,13 +228,13 @@ BioMod/gene_perturbation/ # Perturbation response
228
  ## Gene-Based vs Module-Based Methods
229
 
230
  This implementation supports two feature extraction modes controlled by
231
- `--use_module_level`.
232
 
233
  ### Method 1: Gene-Based (Default, Recommended)
234
 
235
  ```bash
236
- # Default mode; do not pass --use_module_level or set it to False.
237
- python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_perturbation/train_module.py [other arguments...]
238
  ```
239
 
240
  **Characteristics**:
@@ -248,7 +248,7 @@ python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_pert
248
 
249
  ```bash
250
  # Enable module-level mode.
251
- python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_perturbation/train_module.py --use_module_level [other arguments...]
252
  ```
253
 
254
  **Characteristics**:
@@ -262,8 +262,8 @@ python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_pert
262
  ## References
263
 
264
  - Original GEARS code is an external reference and is not included in this source directory.
265
- - PerformerEncoderOnly_Module model: `BioMod/models_performer_module.py`
266
- - Module data processing: `BioMod/module_dataset.py`
267
 
268
  ## Optional external annotation resources
269
 
 
1
  # Gene Perturbation Prediction with BioMod
2
 
3
  This document describes the BioMod gene perturbation workflow (GEARS), using
4
+ the `BioMod` encoder. The
5
  [BioMod model weights and matching configuration](../../README.md#biomod-model-weights)
6
  are shared with the other downstream tasks.
7
 
8
  ## Overview
9
 
10
  This adaptation replaces the original GEARS gene perturbation model based on
11
+ `DualEncoderSCFM` with `BioMod`. The main adaptation points
12
  are:
13
 
14
  1. **Model architecture adaptation**: replace the original dual-encoder model with a module-based encoder-only model.
 
17
 
18
  ## Main Adaptations
19
 
20
+ ### 1. `SCFM_BioMod` (`model_biomod.py`)
21
 
22
+ `SCFM_BioMod` wraps `BioMod` and provides these functions:
23
 
24
  - **Input handling**: reindex input gene expression values to the BioMod gene order.
25
+ - **Module encoding**: obtain module-level encodings from `BioMod`.
26
  - **Gene embedding recovery**: recover gene-level embeddings from module encodings.
27
  - For each gene, find all modules that contain it.
28
  - Average the encodings of those modules to obtain the gene embedding.
 
42
  gene_emb = module_encodings.mean(dim=1) # B, D (global average)
43
  ```
44
 
45
+ ### 2. `GEARS_Model_BioMod` (`model_biomod.py`)
46
 
47
+ `GEARS_Model_BioMod` inherits the original GEARS architecture. Main changes:
48
 
49
+ - **Use `SCFM_BioMod`** instead of the original `SCFM_different_genes`.
50
  - **Module data loading** supports loading module data from hyper-parameters or file paths.
51
  - **Other components remain unchanged**, including the GNN layers and decoder.
52
 
53
+ ### 3. Training Script (`train_biomod.py`)
54
 
55
  The training script is based on `GEARS/train_dual_ddp_reduce_dec.py`. Main changes:
56
 
57
+ - **Model class**: use `GEARS_Model_BioMod` instead of `GEARS_Model_Acc`.
58
  - **New arguments**:
59
+ - `--biomod_data_path`: path to one module JSON file.
60
+ - `--biomod_data_dir`: directory containing module JSON files.
61
  - `--gene_mapping_path`: CSV file mapping gene symbols to Ensembl IDs.
62
 
63
  ## Usage
 
78
  ```bash
79
  # DDP training on multiple GPUs.
80
  python -m torch.distributed.run --standalone --nproc_per_node=4 \
81
+ BioMod/gene_perturbation/train_biomod.py \
82
  --data_dir data/downstream/final_data \
83
  --data_name norman \
84
+ --scfm_hyper_params_path weight/BioMod.pkl \
85
+ --scfm_ckpt_path weight/BioMod.pth \
86
+ --biomod_data_dir model_dependencies/biomod_data_all \
87
  --gene_mapping_path model_dependencies/gene2ens.csv \
88
  --scfm_genes_list_path model_dependencies/selected_genes_27k.txt \
89
  --scfm_gene2vec_file model_dependencies/selected_gene2vec_27k.npy \
 
100
 
101
  - `--scfm_hyper_params_path`: path to the BioMod model configuration file.
102
  - `--scfm_ckpt_path`: path to the BioMod model weights.
103
+ - `--biomod_data_path` or `--biomod_data_dir`: required only when the model configuration does not already contain module definitions.
104
 
105
  **Recommended arguments**:
106
 
 
110
 
111
  **Optional arguments**:
112
 
113
+ - `--use_biomod_level`: use module-level features. This is experimental and is not fully supported at the moment.
114
  - Default: False, meaning gene-level features are used and recommended.
115
 
116
  ## Comparison with Original sclong
 
119
 
120
  1. **Model architecture**:
121
  - **Original sclong**: uses `DualEncoderSCFM` and directly processes gene sequences.
122
+ - **Module version**: uses `BioMod` and processes module-based data.
123
 
124
  2. **Data representation**:
125
  - **Original sclong**: gene-level embeddings come directly from encoder outputs.
 
144
 
145
  ```bash
146
  # Run from the BioMod repository root.
147
+ python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_perturbation/train_biomod.py [arguments...]
148
  ```
149
 
150
  3. **Compare metrics**:
 
161
 
162
  ### Module-to-Gene Mapping
163
 
164
+ In `SCFM_BioMod`, `gene_to_modules` records which modules contain each gene:
165
 
166
  ```python
167
  self.gene_to_modules = {} # {gene_idx: [module_idx1, module_idx2, ...]}
 
193
  ├── data_utils_local.py
194
  ├── gears_reduce_local.py
195
  ├── inference_local.py
196
+ ├── model_biomod.py
197
  ├── pertdata_local.py
198
  ├── README.md
199
  ├── scfm_utils_local.py
200
+ ├── train_biomod.py
201
  └── utils.py
202
  ```
203
 
 
212
 
213
  ### Issue 1: Module Data Not Found
214
 
215
+ - Check whether `--biomod_data_path` or `--biomod_data_dir` is correct.
216
  - Make sure the module data file exists and has the expected format.
217
 
218
  ### Issue 2: Gene Mapping Failed
 
228
  ## Gene-Based vs Module-Based Methods
229
 
230
  This implementation supports two feature extraction modes controlled by
231
+ `--use_biomod_level`.
232
 
233
  ### Method 1: Gene-Based (Default, Recommended)
234
 
235
  ```bash
236
+ # Default mode; do not pass --use_biomod_level or set it to False.
237
+ python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_perturbation/train_biomod.py [other arguments...]
238
  ```
239
 
240
  **Characteristics**:
 
248
 
249
  ```bash
250
  # Enable module-level mode.
251
+ python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_perturbation/train_biomod.py --use_biomod_level [other arguments...]
252
  ```
253
 
254
  **Characteristics**:
 
262
  ## References
263
 
264
  - Original GEARS code is an external reference and is not included in this source directory.
265
+ - BioMod model: `BioMod/models_biomod.py`
266
+ - Module data processing: `BioMod/biomod_dataset.py`
267
 
268
  ## Optional external annotation resources
269
 
BioMod/gene_perturbation/gears_reduce_local.py CHANGED
@@ -46,7 +46,7 @@
46
  # ============================================================
47
  # Use native modules (optimized versions)
48
  # ============================================================
49
- from model_module import GEARS_Model_Module
50
  from inference_local import evaluate, compute_metrics, deeper_analysis, \
51
  non_dropout_analysis, compute_synergy_loss
52
  from utils import loss_fct, uncertainty_loss_fct, parse_any_pert, \
@@ -74,7 +74,7 @@ class GEARS:
74
  [Usage process]
75
  ```python
76
  gears = GEARS(pert_data, local_rank, is_master, world_size, ...)
77
- gears.model_initialize(hidden_size=64, model_class=GEARS_Model_Module, ...)
78
  gears.train(epochs=20, result_dir='./results', lr=1e-3)
79
  ```
80
  """
 
46
  # ============================================================
47
  # Use native modules (optimized versions)
48
  # ============================================================
49
+ from model_biomod import GEARS_Model_BioMod
50
  from inference_local import evaluate, compute_metrics, deeper_analysis, \
51
  non_dropout_analysis, compute_synergy_loss
52
  from utils import loss_fct, uncertainty_loss_fct, parse_any_pert, \
 
74
  [Usage process]
75
  ```python
76
  gears = GEARS(pert_data, local_rank, is_master, world_size, ...)
77
+ gears.model_initialize(hidden_size=64, model_class=GEARS_Model_BioMod, ...)
78
  gears.train(epochs=20, result_dir='./results', lr=1e-3)
79
  ```
80
  """
BioMod/gene_perturbation/{model_module.py → model_biomod.py} RENAMED
@@ -1,24 +1,24 @@
1
  """
2
  ================================================================================
3
- Genetic perturbation task model definition (model_module.py)
4
  ================================================================================
5
 
6
  [Model Architecture Overview]
7
  This document implements a deep learning model for genetic perturbation prediction. The core components include:
8
 
9
- 1. ModuleEmbedding: Convert gene-level expression values into module-level tokens
10
  - Supports multiple integration methods (multiply/add/gated_residual)
11
  - Use attention pooling to aggregate genetic information within the module
12
 
13
- 2. PerformerEncoderOnly_Module: Encoder-only Transformer model
14
  - Use Performer (Linear Complexity Attention) to process large numbers of module tokens
15
  - Support Masked Autoencoder (MAE) pre-training paradigm
16
 
17
- 3. SCFM_Module: wrapper for pre-trained models
18
  - Process the mapping of input genes to pre-trained model genes
19
  - Recover gene-level embeddings from module encoding
20
 
21
- 4. GEARS_Model_Module: Genetic perturbation prediction model
22
  - Integrate pre-trained encoder + GNN + decoder
23
  - Predict expression changes after gene perturbation
24
 
@@ -49,19 +49,19 @@
49
  # Performer is a linear complexity Transformer variant suitable for processing long sequences
50
  # ============================================================
51
  try:
52
- from BioMod.models_performer_module import (
53
- ModuleEmbedding,
54
- PerformerEncoderOnly_Module,
55
- sanitize_performer_module_config,
56
  )
57
  except ModuleNotFoundError:
58
  project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
59
  if project_root not in sys.path:
60
  sys.path.insert(0, project_root)
61
- from BioMod.models_performer_module import (
62
- ModuleEmbedding,
63
- PerformerEncoderOnly_Module,
64
- sanitize_performer_module_config,
65
  )
66
 
67
  # ============================================================
@@ -114,13 +114,13 @@ def reindex_tensor_universal(tensor, index_positions, dim, filler='0', device='c
114
  return reindexed
115
 
116
 
117
- class SCFM_Module(torch.nn.Module):
118
  """
119
  ================================================================================
120
- SCFM_Module: wrapper for pre-trained single cell base models
121
 
122
  [Core functions]
123
- Adapt the PerformerEncoderOnly_Module model to the genetic perturbation task to address the following key issues:
124
 
125
  1. Gene mapping: The gene order of the input data may be different from that of the pre-trained model, and re-indexing is required.
126
  2. Module-to-gene recovery: Recovering gene-level embeddings from module encodings
@@ -150,7 +150,7 @@ def __init__(self, input_genes, scfm_genes,
150
  device='cpu',
151
  output_dim=200,
152
  use_module_level=False):
153
- super(SCFM_Module, self).__init__()
154
  self.device = device
155
  self.use_module_level = use_module_level
156
 
@@ -242,12 +242,12 @@ def __init__(self, input_genes, scfm_genes,
242
  self.register_buffer('scatter_module_idx', torch.tensor(module_indices, dtype=torch.long))
243
  self.register_buffer('scatter_gene_idx', torch.tensor(gene_indices, dtype=torch.long))
244
 
245
- # Create PerformerEncoderOnly_Module model
246
  scfm_hyper_params['module_sets'] = module_sets
247
  scfm_hyper_params['num_genes'] = len(scfm_genes)
248
  scfm_hyper_params['max_seq_len'] = len(module_sets)
249
 
250
- self.scfm = PerformerEncoderOnly_Module(**scfm_hyper_params)
251
 
252
  # ============================================================
253
  # [Key] Load pre-trained weights
@@ -298,7 +298,7 @@ def forward(self, x):
298
 
299
  [Processing Process]
300
  1. Re-index the input gene expression into the gene order of the pre-trained model
301
- 2. Get the module encoding through PerformerEncoderOnly_Module
302
  3. Restore gene-level embedding from module encoding (or return module encoding directly)
303
  4. Reindex gene embeddings back to input gene order
304
 
@@ -395,17 +395,17 @@ def forward(self, x):
395
  return output_reindex
396
 
397
 
398
- class GEARS_Model_Module(torch.nn.Module):
399
  """
400
  ================================================================================
401
- GEARS_Model_Module: Genetic perturbation prediction model
402
 
403
  [Core functions]
404
  Predicting transcriptomic changes in cells following genetic perturbation. Enter a perturbation combination (such as knockout gene A),
405
  Output the expression value prediction of each gene after perturbation.
406
 
407
  [Model Architecture]
408
- 1. Pretrained encoder (SCFM_Module): Encode gene expression into embedding vectors
409
  2. Perturbation embedding layer: generate perturbation features for perturbed genes
410
  3. GO graph neural network (GNN): using gene ontology knowledge to enhance perturbation representation
411
  4. Decoder: predict gene expression values after perturbation
@@ -424,7 +424,7 @@ class GEARS_Model_Module(torch.nn.Module):
424
  ================================================================================
425
  """
426
  def __init__(self, args):
427
- super(GEARS_Model_Module, self).__init__()
428
  self.args = args
429
 
430
  # Forecast record related settings
@@ -462,7 +462,7 @@ def __init__(self, args):
462
  with open(args["scfm_hyper_params_path"], 'rb') as handle:
463
  self.scfm_hyper_params = pickle5.load(handle)
464
  self.scfm_hyper_params.pop('module_embed_chunk_padded_gene_budget', None)
465
- self.scfm_hyper_params = sanitize_performer_module_config(
466
  self.scfm_hyper_params,
467
  source=args["scfm_hyper_params_path"],
468
  log_fn=print,
@@ -480,8 +480,8 @@ def __init__(self, args):
480
  if 'module_sets' in self.scfm_hyper_params and self.scfm_hyper_params['module_sets'] is not None:
481
  module_sets = self.scfm_hyper_params['module_sets']
482
  elif args.get('module_data_path') or args.get('module_data_dir'):
483
- from BioMod.module_dataset import load_module_data
484
- module_sets = load_module_data(
485
  module_data_path=args.get('module_data_path'),
486
  module_data_dir=args.get('module_data_dir'),
487
  gene_list_path=args.get('scfm_genes_list_path'),
@@ -499,7 +499,7 @@ def __init__(self, args):
499
  if args.get('shuffle_module_genes', False):
500
  shuffle_seed = args.get('shuffle_seed', 2021)
501
  print(
502
- f"[GEARS_Model_Module] Applying downstream-only extra module reshuffle "
503
  f"(seed={shuffle_seed}). Pretraining-time module_sets from PKL will be reshuffled again."
504
  )
505
  all_gene_indices = []
@@ -518,7 +518,7 @@ def __init__(self, args):
518
  shuffled_module_sets.append(all_gene_indices[start_idx:start_idx + size])
519
  start_idx += size
520
  module_sets = shuffled_module_sets
521
- print(f"[GEARS_Model_Module] Shuffled {len(all_gene_indices)} genes across {len(module_sets)} modules")
522
 
523
  # ============================================================
524
  # [Perturbation Embedding Layer]
@@ -585,13 +585,13 @@ def __init__(self, args):
585
  self.uncertainty_w = MLP([hidden_size, hidden_size*2, hidden_size, 1], last_layer_act='linear')
586
 
587
  # ============================================================
588
- # [Core] Initialize pre-trained encoder (SCFM_Module)
589
  # requires_grad: whether to fine-tune pre-training parameters
590
  # - finetune_method='frozen': freeze pre-training parameters
591
  # - Others: Full parameter fine-tuning
592
  # ============================================================
593
  self.use_module_level = args.get('use_module_level', False)
594
- self.singlecell_model = SCFM_Module(
595
  self.input_genes_list,
596
  self.scfm_genes_list,
597
  self.scfm_hyper_params,
 
1
  """
2
  ================================================================================
3
+ Genetic perturbation task model definition (model_biomod.py)
4
  ================================================================================
5
 
6
  [Model Architecture Overview]
7
  This document implements a deep learning model for genetic perturbation prediction. The core components include:
8
 
9
+ 1. BioModEmbedding: Convert gene-level expression values into module-level tokens
10
  - Supports multiple integration methods (multiply/add/gated_residual)
11
  - Use attention pooling to aggregate genetic information within the module
12
 
13
+ 2. BioMod: Encoder-only Transformer model
14
  - Use Performer (Linear Complexity Attention) to process large numbers of module tokens
15
  - Support Masked Autoencoder (MAE) pre-training paradigm
16
 
17
+ 3. SCFM_BioMod: wrapper for pre-trained models
18
  - Process the mapping of input genes to pre-trained model genes
19
  - Recover gene-level embeddings from module encoding
20
 
21
+ 4. GEARS_Model_BioMod: Genetic perturbation prediction model
22
  - Integrate pre-trained encoder + GNN + decoder
23
  - Predict expression changes after gene perturbation
24
 
 
49
  # Performer is a linear complexity Transformer variant suitable for processing long sequences
50
  # ============================================================
51
  try:
52
+ from BioMod.models_biomod import (
53
+ BioModEmbedding,
54
+ BioMod,
55
+ sanitize_biomod_config,
56
  )
57
  except ModuleNotFoundError:
58
  project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
59
  if project_root not in sys.path:
60
  sys.path.insert(0, project_root)
61
+ from BioMod.models_biomod import (
62
+ BioModEmbedding,
63
+ BioMod,
64
+ sanitize_biomod_config,
65
  )
66
 
67
  # ============================================================
 
114
  return reindexed
115
 
116
 
117
+ class SCFM_BioMod(torch.nn.Module):
118
  """
119
  ================================================================================
120
+ SCFM_BioMod: wrapper for pre-trained single cell base models
121
 
122
  [Core functions]
123
+ Adapt the BioMod model to the genetic perturbation task to address the following key issues:
124
 
125
  1. Gene mapping: The gene order of the input data may be different from that of the pre-trained model, and re-indexing is required.
126
  2. Module-to-gene recovery: Recovering gene-level embeddings from module encodings
 
150
  device='cpu',
151
  output_dim=200,
152
  use_module_level=False):
153
+ super(SCFM_BioMod, self).__init__()
154
  self.device = device
155
  self.use_module_level = use_module_level
156
 
 
242
  self.register_buffer('scatter_module_idx', torch.tensor(module_indices, dtype=torch.long))
243
  self.register_buffer('scatter_gene_idx', torch.tensor(gene_indices, dtype=torch.long))
244
 
245
+ # Create BioMod model
246
  scfm_hyper_params['module_sets'] = module_sets
247
  scfm_hyper_params['num_genes'] = len(scfm_genes)
248
  scfm_hyper_params['max_seq_len'] = len(module_sets)
249
 
250
+ self.scfm = BioMod(**scfm_hyper_params)
251
 
252
  # ============================================================
253
  # [Key] Load pre-trained weights
 
298
 
299
  [Processing Process]
300
  1. Re-index the input gene expression into the gene order of the pre-trained model
301
+ 2. Get the module encoding through BioMod
302
  3. Restore gene-level embedding from module encoding (or return module encoding directly)
303
  4. Reindex gene embeddings back to input gene order
304
 
 
395
  return output_reindex
396
 
397
 
398
+ class GEARS_Model_BioMod(torch.nn.Module):
399
  """
400
  ================================================================================
401
+ GEARS_Model_BioMod: Genetic perturbation prediction model
402
 
403
  [Core functions]
404
  Predicting transcriptomic changes in cells following genetic perturbation. Enter a perturbation combination (such as knockout gene A),
405
  Output the expression value prediction of each gene after perturbation.
406
 
407
  [Model Architecture]
408
+ 1. Pretrained encoder (SCFM_BioMod): Encode gene expression into embedding vectors
409
  2. Perturbation embedding layer: generate perturbation features for perturbed genes
410
  3. GO graph neural network (GNN): using gene ontology knowledge to enhance perturbation representation
411
  4. Decoder: predict gene expression values after perturbation
 
424
  ================================================================================
425
  """
426
  def __init__(self, args):
427
+ super(GEARS_Model_BioMod, self).__init__()
428
  self.args = args
429
 
430
  # Forecast record related settings
 
462
  with open(args["scfm_hyper_params_path"], 'rb') as handle:
463
  self.scfm_hyper_params = pickle5.load(handle)
464
  self.scfm_hyper_params.pop('module_embed_chunk_padded_gene_budget', None)
465
+ self.scfm_hyper_params = sanitize_biomod_config(
466
  self.scfm_hyper_params,
467
  source=args["scfm_hyper_params_path"],
468
  log_fn=print,
 
480
  if 'module_sets' in self.scfm_hyper_params and self.scfm_hyper_params['module_sets'] is not None:
481
  module_sets = self.scfm_hyper_params['module_sets']
482
  elif args.get('module_data_path') or args.get('module_data_dir'):
483
+ from BioMod.biomod_dataset import load_biomod_data
484
+ module_sets = load_biomod_data(
485
  module_data_path=args.get('module_data_path'),
486
  module_data_dir=args.get('module_data_dir'),
487
  gene_list_path=args.get('scfm_genes_list_path'),
 
499
  if args.get('shuffle_module_genes', False):
500
  shuffle_seed = args.get('shuffle_seed', 2021)
501
  print(
502
+ f"[GEARS_Model_BioMod] Applying downstream-only extra module reshuffle "
503
  f"(seed={shuffle_seed}). Pretraining-time module_sets from PKL will be reshuffled again."
504
  )
505
  all_gene_indices = []
 
518
  shuffled_module_sets.append(all_gene_indices[start_idx:start_idx + size])
519
  start_idx += size
520
  module_sets = shuffled_module_sets
521
+ print(f"[GEARS_Model_BioMod] Shuffled {len(all_gene_indices)} genes across {len(module_sets)} modules")
522
 
523
  # ============================================================
524
  # [Perturbation Embedding Layer]
 
585
  self.uncertainty_w = MLP([hidden_size, hidden_size*2, hidden_size, 1], last_layer_act='linear')
586
 
587
  # ============================================================
588
+ # [Core] Initialize pre-trained encoder (SCFM_BioMod)
589
  # requires_grad: whether to fine-tune pre-training parameters
590
  # - finetune_method='frozen': freeze pre-training parameters
591
  # - Others: Full parameter fine-tuning
592
  # ============================================================
593
  self.use_module_level = args.get('use_module_level', False)
594
+ self.singlecell_model = SCFM_BioMod(
595
  self.input_genes_list,
596
  self.scfm_genes_list,
597
  self.scfm_hyper_params,
BioMod/gene_perturbation/{train_module.py → train_biomod.py} RENAMED
@@ -1,6 +1,6 @@
1
  """
2
  ================================================================================
3
- Genetic perturbation task training script (train_module.py)
4
  ================================================================================
5
 
6
  [Task Overview]
@@ -10,7 +10,7 @@
10
 
11
  [Core process]
12
  1. Load perturbation data sets (such as Norman, Adamson and other public data sets)
13
- 2. Initialize the pre-trained single-cell basic model (PerformerEncoderOnly_Module)
14
  3. Fine-tune the model on perturbed data and learn to predict the effect of perturbation
15
  4. Evaluate model performance (MSE, Pearson correlation coefficient and other indicators)
16
 
@@ -47,7 +47,7 @@
47
  REPO_ROOT = Path(__file__).resolve().parents[2]
48
  ASSET_DIR = REPO_ROOT / "model_dependencies"
49
  DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
50
- WEIGHTS_DIR = REPO_ROOT / "weight" / "0810model"
51
 
52
  # ============================================================
53
  # [Key] Correctly set the order of sys.path
@@ -58,7 +58,7 @@
58
  current_dir = os.path.dirname(os.path.abspath(__file__))
59
  project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
60
 
61
- # First add the project root directory (for performer_pytorch_cont)
62
  if project_root not in sys.path:
63
  sys.path.append(project_root)
64
 
@@ -87,7 +87,7 @@
87
  from pertdata_local import PertData # Local version, does not rely on external gears_reduce
88
 
89
  # Import module model
90
- from model_module import GEARS_Model_Module
91
 
92
  def main(parser):
93
  """
@@ -105,7 +105,7 @@ def main(parser):
105
 
106
  args = parser.parse_args()
107
  if args.module_data_path is None and args.module_data_dir is None:
108
- args.module_data_dir = str(ASSET_DIR / 'module_data_all')
109
 
110
  # ============================================================
111
  # [Distributed training initialization]
@@ -190,7 +190,7 @@ def main(parser):
190
 
191
  # ============================================================
192
  # [Model parameter configuration]
193
- # Use GEARS_Model_Module instead of GEARS_Model_Acc
194
  # Key parameter description:
195
  # - hidden_size: hidden layer dimension, affecting model capacity
196
  # - finetune_method: fine-tuning method ('frozen' freezes pre-training parameters, None means fine-tuning all parameters)
@@ -212,7 +212,7 @@ def main(parser):
212
  key_enc=None,
213
  scfm_gene2vec_file=args.scfm_gene2vec_file,
214
  record_pred=args.record_pred,
215
- model_class=GEARS_Model_Module, # Use module model class
216
  # ============================================================
217
  # [Module related parameters] The following parameters affect how the model processes module-based data
218
  # module_data_path/module_data_dir: module divides the data path
@@ -243,7 +243,7 @@ def main(parser):
243
  param_pd.to_csv(f'{args.result_dir}/params.csv')
244
 
245
  if __name__ == '__main__':
246
- parser = argparse.ArgumentParser(description='GEARS with PerformerEncoderOnly_Module')
247
 
248
  # ============================================================
249
  # [Distributed training parameters]
@@ -297,9 +297,9 @@ def main(parser):
297
  parser.add_argument('--scfm_genes_list_path', type=str,
298
  default=str(ASSET_DIR / "selected_genes_27k.txt"))
299
  parser.add_argument('--scfm_hyper_params_path', type=str,
300
- default=str(WEIGHTS_DIR / "mae_tost_70M_75mask_ec2.pkl"))
301
  parser.add_argument('--scfm_ckpt_path', type=str,
302
- default=str(WEIGHTS_DIR / "mae_tost_70M_75mask_ec2_epoch0.pth"))
303
  parser.add_argument('--run_name', type=str,
304
  default="default_run_module")
305
  parser.add_argument('--scfm_gene2vec_file', type=str,
@@ -314,20 +314,20 @@ def main(parser):
314
  # fusion_method: Read from pkl to ensure consistency with pre-training
315
  # shuffle_module_genes: [Ablation experiment] Disrupt the genes in the module to verify the importance of the module structure
316
  # ============================================================
317
- parser.add_argument('--module_data_path', type=str, default=None,
318
- help='Path to module JSON file (single file)')
319
- parser.add_argument('--module_data_dir', type=str, default=None,
320
- help='Module JSON directory; defaults to bundled module_data_all only when no module file or directory is supplied')
321
  parser.add_argument('--gene_mapping_path', type=str, default=str(ASSET_DIR / 'gene2ens.csv'),
322
  help='Path to gene symbol to Ensembl ID mapping CSV')
323
- parser.add_argument('--use_module_level', action='store_true',
324
  help='Use module-level features directly instead of recovering to gene level. '
325
- 'Note: This requires compatible downstream architecture (not fully implemented).')
326
- parser.add_argument('--shuffle_module_genes', action='store_true',
327
  help='[Ablation experiment] Additionally shuffle module_sets after restoring them from the pretrained PKL. '
328
- 'Do not pass this parameter when reproducing the experiment normally.')
329
  parser.add_argument('--shuffle_seed', type=int, default=2021,
330
- help='Only takes effect when --shuffle_module_genes is turned on and is used for additional downstream shuffling.')
331
  parser.add_argument('--lr', type=float, default=1e-3)
332
 
333
  main(parser)
 
1
  """
2
  ================================================================================
3
+ Genetic perturbation task training script (train_biomod.py)
4
  ================================================================================
5
 
6
  [Task Overview]
 
10
 
11
  [Core process]
12
  1. Load perturbation data sets (such as Norman, Adamson and other public data sets)
13
+ 2. Initialize the pre-trained single-cell basic model (BioMod)
14
  3. Fine-tune the model on perturbed data and learn to predict the effect of perturbation
15
  4. Evaluate model performance (MSE, Pearson correlation coefficient and other indicators)
16
 
 
47
  REPO_ROOT = Path(__file__).resolve().parents[2]
48
  ASSET_DIR = REPO_ROOT / "model_dependencies"
49
  DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
50
+ WEIGHTS_DIR = REPO_ROOT / "weight"
51
 
52
  # ============================================================
53
  # [Key] Correctly set the order of sys.path
 
58
  current_dir = os.path.dirname(os.path.abspath(__file__))
59
  project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
60
 
61
+ # First add the project root directory (for pytorch_cont)
62
  if project_root not in sys.path:
63
  sys.path.append(project_root)
64
 
 
87
  from pertdata_local import PertData # Local version, does not rely on external gears_reduce
88
 
89
  # Import module model
90
+ from model_biomod import GEARS_Model_BioMod
91
 
92
  def main(parser):
93
  """
 
105
 
106
  args = parser.parse_args()
107
  if args.module_data_path is None and args.module_data_dir is None:
108
+ args.module_data_dir = str(ASSET_DIR / 'biomod_data_all')
109
 
110
  # ============================================================
111
  # [Distributed training initialization]
 
190
 
191
  # ============================================================
192
  # [Model parameter configuration]
193
+ # Use GEARS_Model_BioMod instead of GEARS_Model_Acc
194
  # Key parameter description:
195
  # - hidden_size: hidden layer dimension, affecting model capacity
196
  # - finetune_method: fine-tuning method ('frozen' freezes pre-training parameters, None means fine-tuning all parameters)
 
212
  key_enc=None,
213
  scfm_gene2vec_file=args.scfm_gene2vec_file,
214
  record_pred=args.record_pred,
215
+ model_class=GEARS_Model_BioMod, # Use module model class
216
  # ============================================================
217
  # [Module related parameters] The following parameters affect how the model processes module-based data
218
  # module_data_path/module_data_dir: module divides the data path
 
243
  param_pd.to_csv(f'{args.result_dir}/params.csv')
244
 
245
  if __name__ == '__main__':
246
+ parser = argparse.ArgumentParser(description='GEARS with BioMod')
247
 
248
  # ============================================================
249
  # [Distributed training parameters]
 
297
  parser.add_argument('--scfm_genes_list_path', type=str,
298
  default=str(ASSET_DIR / "selected_genes_27k.txt"))
299
  parser.add_argument('--scfm_hyper_params_path', type=str,
300
+ default=str(WEIGHTS_DIR / "BioMod.pkl"))
301
  parser.add_argument('--scfm_ckpt_path', type=str,
302
+ default=str(WEIGHTS_DIR / "BioMod.pth"))
303
  parser.add_argument('--run_name', type=str,
304
  default="default_run_module")
305
  parser.add_argument('--scfm_gene2vec_file', type=str,
 
314
  # fusion_method: Read from pkl to ensure consistency with pre-training
315
  # shuffle_module_genes: [Ablation experiment] Disrupt the genes in the module to verify the importance of the module structure
316
  # ============================================================
317
+ parser.add_argument('--biomod_data_path', '--module_data_path', type=str, default=None,
318
+ help='Path to module JSON file (single file)', dest='module_data_path')
319
+ parser.add_argument('--biomod_data_dir', '--module_data_dir', type=str, default=None,
320
+ help='Module JSON directory; defaults to bundled biomod_data_all only when no module file or directory is supplied', dest='module_data_dir')
321
  parser.add_argument('--gene_mapping_path', type=str, default=str(ASSET_DIR / 'gene2ens.csv'),
322
  help='Path to gene symbol to Ensembl ID mapping CSV')
323
+ parser.add_argument('--use_biomod_level', '--use_module_level', action='store_true',
324
  help='Use module-level features directly instead of recovering to gene level. '
325
+ 'Note: This requires compatible downstream architecture (not fully implemented).', dest='use_module_level')
326
+ parser.add_argument('--shuffle_biomod_genes', '--shuffle_module_genes', action='store_true',
327
  help='[Ablation experiment] Additionally shuffle module_sets after restoring them from the pretrained PKL. '
328
+ 'Do not pass this parameter when reproducing the experiment normally.', dest='shuffle_module_genes')
329
  parser.add_argument('--shuffle_seed', type=int, default=2021,
330
+ help='Only takes effect when --shuffle_biomod_genes is turned on and is used for additional downstream shuffling.')
331
  parser.add_argument('--lr', type=float, default=1e-3)
332
 
333
  main(parser)
BioMod/interpretability/README.md CHANGED
@@ -11,11 +11,11 @@ when loading the pretrained backbone.
11
 
12
  | # | Script | Core question | Status |
13
  | --- | --- | --- | --- |
14
- | 1 | `module_geometry.py` | Does the module latent space self-organize by pathway family, | Completed |
15
- | 2 | `module_text_alignment/module_text_alignment.py` | Do module embeddings align with natural-language pathway descriptions, | Implemented |
16
- | 3 | `module_importance/module_importance.py` | Does task-driven module importance match known pathway-cell-type or pathway-disease associations, | Implemented |
17
  | 4 | `module_vs_gene.py` | Is BioMod better than GenePT/scGPT for pathway-level interpretability, | TODO |
18
- | 6 | `attention/module_crosstalk.py` | Do module interactions match Reactome/KEGG pathway relationships, | Implemented |
19
 
20
  Experiments 1 and 2 are the core differentiators because gene-level baselines
21
  cannot structurally reproduce them. Experiment 3 provides task-level functional
@@ -24,7 +24,7 @@ experiments, such as module crosstalk vs Reactome, module dynamics along
24
  developmental trajectories, and retraining-based module-granularity ablation, can
25
  be pursued after the core experiments are complete.
26
 
27
- ## Experiment 1: Intrinsic Geometry of Module Embeddings (`module_geometry.py`)
28
 
29
  **Hypothesis:** the BioMod encoder is trained with a masked-module
30
  reconstruction task, but it should still encode semantically related modules,
@@ -36,7 +36,7 @@ never seen during training.
36
 
37
  1. Load the pretrained backbone and run inference with `mask_ratio=0.0` and `return_encodings=True` to obtain module encodings shaped `[B, num_modules, base_dim]`.
38
  2. Average over the cell population to obtain `[num_modules, base_dim]`, using streaming accumulation rather than storing the full table in memory.
39
- 3. Parse family assignments from the keys in `merged_modules.json`: `Hallmarks` (50), `Reactome` (491), `Hotspot` (73), and `Processed` (46). Hallmark modules are also annotated with one of eight MSigDB biological-process categories: immune, metabolic, signaling, proliferation, development, dna_damage, pathway, and cellular_component.
40
  4. Quantify family separation:
41
  - silhouette coefficient, using both cosine and Euclidean metrics.
42
  - kNN purity at `k=10`, using both cosine and Euclidean metrics.
@@ -56,36 +56,36 @@ aggregation end to end during pretraining.
56
  BioMod/interpretability/ # Module representations and biological interpretability
57
  ├── attention/ # Module crosstalk and within-module gene attention
58
  │ ├── __init__.py
59
- │ ├── module_crosstalk.py
60
- │ ├── module_gene_attention.py
61
  │ ├── README.md
62
  │ └── README_gene_attention.md
63
- ├── module_importance/ # Module importance, ablation studies and marker-gene validation
64
  │ ├── __init__.py
65
  │ ├── cross_class_similarity.py
66
  │ ├── family_ablation.py
67
  │ ├── family_importance_analysis.py
68
  │ ├── marker_gene_verification.py
69
  │ ├── masking_ablation.py
70
- │ ├── module_importance.py
71
- │ ├── module_importance_publication.py
72
  │ ├── pub_utils.py
73
  │ ├── random_baseline.py
74
  │ ├── README.md
75
  │ ├── RESULTS.md
76
  │ ├── tcell_reactome_analysis.py
77
  │ └── TODO.md
78
- ├── module_text_alignment/ # Semantic alignment of module representations and pathway text
79
  │ ├── __init__.py
80
  │ ├── DESIGN.md
81
  │ ├── fetch_reactome_descriptions.py
82
  │ ├── MANUSCRIPT_SECTION.md
83
- │ ├��─ module_text_alignment.py
84
- │ ├── module_text_alignment_extended.py
85
- │ ├── module_text_alignment_publication.py
86
  │ └── reactome_descriptions.json
87
  ├── __init__.py
88
- ├── module_geometry.py
89
  ├── README.md
90
  └── utils.py
91
  ```
@@ -104,10 +104,10 @@ bundled; run the corresponding upstream analysis before using those paths.
104
 
105
  ```bash
106
  # General h5ad input: cell annotation, zero-shot, or any annotated dataset.
107
- python BioMod/interpretability/module_geometry.py \
108
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
109
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
110
- --module_data_dir model_dependencies/module_data_all \
111
  --gene_list_path model_dependencies/selected_genes_27k.txt \
112
  --gene_mapping_path model_dependencies/gene2ens.csv \
113
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -123,10 +123,10 @@ For pretraining data, where each cell is an `.npz` file already in the 27,874
123
  model-gene space:
124
 
125
  ```bash
126
- python BioMod/interpretability/module_geometry.py \
127
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
128
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
129
- --module_data_dir model_dependencies/module_data_all \
130
  --gene_list_path model_dependencies/selected_genes_27k.txt \
131
  --gene_mapping_path model_dependencies/gene2ens.csv \
132
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -144,10 +144,10 @@ processes a subset of cells, embeddings are aggregated with `all_reduce`, and
144
  rank 0 computes metrics and figures. Single-GPU and CPU behavior is unchanged.
145
 
146
  ```bash
147
- torchrun --nproc_per_node=2 BioMod/interpretability/module_geometry.py \
148
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
149
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
150
- --module_data_dir model_dependencies/module_data_all \
151
  --gene_list_path model_dependencies/selected_genes_27k.txt \
152
  --gene_mapping_path model_dependencies/gene2ens.csv \
153
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -167,7 +167,7 @@ torchrun --nproc_per_node=2 BioMod/interpretability/module_geometry.py \
167
  | `--knn_k` | 10 | `k` for kNN purity. |
168
  | `--null_permutations` | 200 | Number of label permutations for the empirical null distribution. |
169
  | `--include_other_in_metrics` | Off | By default, modules from the `Other` family are excluded. |
170
- | `--shuffle_module_genes` | Off | Randomly shuffle gene-to-module assignments as a negative control or ablation. Uses the same flag as pretraining and downstream finetuning. |
171
  | `--umap_n_neighbors` | 15 | UMAP graph-construction parameter. |
172
  | `--umap_min_dist` | 0.3 | UMAP layout parameter. |
173
 
@@ -190,9 +190,9 @@ Files are written to `<output_dir>/`:
190
 
191
  Two control experiments make Experiment 1 more rigorous:
192
 
193
- 1. **Random module assignment**: rerun with `--shuffle_module_genes`. This flag preserves module sizes but randomly assigns genes to modules, matching the same ablation flag used for pretraining and finetuning. If geometry is pathway-driven rather than a generic property of the encoder, family separation should drop close to the null distribution.
194
 
195
- 2. **Untrained backbone**: set `--performer_checkpoint` to the `*_init.pth` checkpoint generated during pretraining with `--save_init_ckpt_and_exit`. Family separation should be close to the null distribution. The gap between trained and untrained models quantifies the contribution of pretraining.
196
 
197
  Together, these controls support the claim that the family structure of the module
198
  latent space is produced by pathway-based module design plus pretraining, rather
@@ -200,16 +200,16 @@ than by module design alone or by a generic encoder architecture.
200
 
201
  ### Control 1: Random Module Assignment (`shuffle_module_genes`)
202
 
203
- Add `--shuffle_module_genes` to the original command and write to a control output
204
  directory.
205
 
206
  For h5ad data, using `pancreas_scib` as an example:
207
 
208
  ```bash
209
- python BioMod/interpretability/module_geometry.py \
210
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
211
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
212
- --module_data_dir model_dependencies/module_data_all \
213
  --gene_list_path model_dependencies/selected_genes_27k.txt \
214
  --gene_mapping_path model_dependencies/gene2ens.csv \
215
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -218,17 +218,17 @@ python BioMod/interpretability/module_geometry.py \
218
  --data_mode h5ad \
219
  --gene_name_column auto \
220
  --batch_size 640 \
221
- --shuffle_module_genes \
222
  --output_dir outputs/interpretability/geometry/pancreas_scib_shuffle
223
  ```
224
 
225
  For pretraining npz data:
226
 
227
  ```bash
228
- python BioMod/interpretability/module_geometry.py \
229
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
230
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
231
- --module_data_dir model_dependencies/module_data_all \
232
  --gene_list_path model_dependencies/selected_genes_27k.txt \
233
  --gene_mapping_path model_dependencies/gene2ens.csv \
234
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -237,11 +237,11 @@ python BioMod/interpretability/module_geometry.py \
237
  --data_mode pretrain_npz \
238
  --max_cells 50000 \
239
  --batch_size 640 \
240
- --shuffle_module_genes \
241
  --output_dir outputs/interpretability/geometry/pretrain_subset_shuffle
242
  ```
243
 
244
- How `--shuffle_module_genes` works: inside `load_backbone()`, module sizes are
245
  kept unchanged, all genes are collected into one list, the list is randomly
246
  shuffled, and genes are reassigned to modules. The seed is controlled by
247
  `--shuffle_seed`, defaulting to 2021. Model weights are unchanged; only the
@@ -253,7 +253,7 @@ records `"shuffle_module_genes": true`.
253
 
254
  ### Control 2: Untrained Backbone with Random Initialization
255
 
256
- First generate a randomly initialized checkpoint, then run `module_geometry.py`
257
  with it.
258
 
259
  A matching randomly initialized checkpoint and configuration are not bundled.
@@ -263,8 +263,8 @@ Keep the generated control files in a separate output directory. Then pass their
263
  paths explicitly; do not use the pretrained BioMod weights for this control.
264
 
265
  ```bash
266
- python BioMod/interpretability/module_geometry.py \
267
- --performer_checkpoint /absolute/path/to/random_init.pth \
268
  --config_pkl /absolute/path/to/random_init.pkl \
269
  --data_path data/downstream/final_data/human_pancreas_norm_complexBatch_with_gene_name.h5ad \
270
  --data_mode h5ad \
@@ -292,7 +292,7 @@ The three experiments jointly show that the family structure of the module laten
292
  space is generated by both pathway-based module design and pretraining; both are
293
  necessary.
294
 
295
- ## Experiment 2: Module Embedding to Pathway Text Alignment (`module_text_alignment/`)
296
 
297
  **Hypothesis:** the structure of the BioMod module embedding space significantly
298
  aligns with natural-language pathway semantics. Semantically similar pathways
@@ -301,7 +301,7 @@ experiment because individual gene names do not carry phrase-level semantics.
301
 
302
  **Method:**
303
 
304
- 1. Parse 541 semantically named modules from `merged_modules.json`: 50 Hallmark and 491 Reactome.
305
  2. Encode pathway names with S-PubMedBert-MS-MARCO, a sentence-level fine-tuned PubMedBERT model.
306
  3. Load `module_embeddings.npy` from Experiment 1, containing cell-averaged module embeddings.
307
  4. Measure alignment:
@@ -322,19 +322,19 @@ raw expression data are not required.
322
  # Network access or an existing local model cache is required.
323
 
324
  # Main experiment: RSA + CKA + LOO retrieval.
325
- python BioMod/interpretability/module_text_alignment/module_text_alignment.py \
326
- --module_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
327
- --module_json model_dependencies/module_data_all/merged_modules.json \
328
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
329
  --n_permutations 1000 \
330
  --output_dir outputs/interpretability/text_alignment/pretrain_subset_v2
331
 
332
  # Extended analysis: broad Reactome categories and Hallmark deep dive.
333
- python BioMod/interpretability/module_text_alignment/module_text_alignment_extended.py \
334
- --module_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
335
- --module_json model_dependencies/module_data_all/merged_modules.json \
336
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
337
- --descriptions_file BioMod/interpretability/module_text_alignment/reactome_descriptions.json \
338
  --n_permutations 1000 \
339
  --output_dir outputs/interpretability/text_alignment/pretrain_subset_v2_extended
340
  ```
 
11
 
12
  | # | Script | Core question | Status |
13
  | --- | --- | --- | --- |
14
+ | 1 | `biomod_geometry.py` | Does the module latent space self-organize by pathway family, | Completed |
15
+ | 2 | `biomod_text_alignment/biomod_text_alignment.py` | Do module embeddings align with natural-language pathway descriptions, | Implemented |
16
+ | 3 | `biomod_importance/biomod_importance.py` | Does task-driven module importance match known pathway-cell-type or pathway-disease associations, | Implemented |
17
  | 4 | `module_vs_gene.py` | Is BioMod better than GenePT/scGPT for pathway-level interpretability, | TODO |
18
+ | 6 | `attention/biomod_crosstalk.py` | Do module interactions match Reactome/KEGG pathway relationships, | Implemented |
19
 
20
  Experiments 1 and 2 are the core differentiators because gene-level baselines
21
  cannot structurally reproduce them. Experiment 3 provides task-level functional
 
24
  developmental trajectories, and retraining-based module-granularity ablation, can
25
  be pursued after the core experiments are complete.
26
 
27
+ ## Experiment 1: Intrinsic Geometry of Module Embeddings (`biomod_geometry.py`)
28
 
29
  **Hypothesis:** the BioMod encoder is trained with a masked-module
30
  reconstruction task, but it should still encode semantically related modules,
 
36
 
37
  1. Load the pretrained backbone and run inference with `mask_ratio=0.0` and `return_encodings=True` to obtain module encodings shaped `[B, num_modules, base_dim]`.
38
  2. Average over the cell population to obtain `[num_modules, base_dim]`, using streaming accumulation rather than storing the full table in memory.
39
+ 3. Parse family assignments from the keys in `merged_biomods.json`: `Hallmarks` (50), `Reactome` (491), `Hotspot` (73), and `Processed` (46). Hallmark modules are also annotated with one of eight MSigDB biological-process categories: immune, metabolic, signaling, proliferation, development, dna_damage, pathway, and cellular_component.
40
  4. Quantify family separation:
41
  - silhouette coefficient, using both cosine and Euclidean metrics.
42
  - kNN purity at `k=10`, using both cosine and Euclidean metrics.
 
56
  BioMod/interpretability/ # Module representations and biological interpretability
57
  ├── attention/ # Module crosstalk and within-module gene attention
58
  │ ├── __init__.py
59
+ │ ├── biomod_crosstalk.py
60
+ │ ├── biomod_gene_attention.py
61
  │ ├── README.md
62
  │ └── README_gene_attention.md
63
+ ├── biomod_importance/ # Module importance, ablation studies and marker-gene validation
64
  │ ├── __init__.py
65
  │ ├── cross_class_similarity.py
66
  │ ├── family_ablation.py
67
  │ ├── family_importance_analysis.py
68
  │ ├── marker_gene_verification.py
69
  │ ├── masking_ablation.py
70
+ │ ├── biomod_importance.py
71
+ │ ├── biomod_importance_publication.py
72
  │ ├── pub_utils.py
73
  │ ├── random_baseline.py
74
  │ ├── README.md
75
  │ ├── RESULTS.md
76
  │ ├── tcell_reactome_analysis.py
77
  │ └── TODO.md
78
+ ├── biomod_text_alignment/ # Semantic alignment of module representations and pathway text
79
  │ ├── __init__.py
80
  │ ├── DESIGN.md
81
  │ ├── fetch_reactome_descriptions.py
82
  │ ├── MANUSCRIPT_SECTION.md
83
+ │ ├── biomod_text_alignment.py
84
+ │ ├── biomod_text_alignment_extended.py
85
+ │ ├── biomod_text_alignment_publication.py
86
  │ └── reactome_descriptions.json
87
  ├── __init__.py
88
+ ├── biomod_geometry.py
89
  ├── README.md
90
  └── utils.py
91
  ```
 
104
 
105
  ```bash
106
  # General h5ad input: cell annotation, zero-shot, or any annotated dataset.
107
+ python BioMod/interpretability/biomod_geometry.py \
108
+ --biomod_checkpoint weight/BioMod.pth \
109
+ --config_pkl weight/BioMod.pkl \
110
+ --biomod_data_dir model_dependencies/biomod_data_all \
111
  --gene_list_path model_dependencies/selected_genes_27k.txt \
112
  --gene_mapping_path model_dependencies/gene2ens.csv \
113
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
123
  model-gene space:
124
 
125
  ```bash
126
+ python BioMod/interpretability/biomod_geometry.py \
127
+ --biomod_checkpoint weight/BioMod.pth \
128
+ --config_pkl weight/BioMod.pkl \
129
+ --biomod_data_dir model_dependencies/biomod_data_all \
130
  --gene_list_path model_dependencies/selected_genes_27k.txt \
131
  --gene_mapping_path model_dependencies/gene2ens.csv \
132
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
144
  rank 0 computes metrics and figures. Single-GPU and CPU behavior is unchanged.
145
 
146
  ```bash
147
+ torchrun --nproc_per_node=2 BioMod/interpretability/biomod_geometry.py \
148
+ --biomod_checkpoint weight/BioMod.pth \
149
+ --config_pkl weight/BioMod.pkl \
150
+ --biomod_data_dir model_dependencies/biomod_data_all \
151
  --gene_list_path model_dependencies/selected_genes_27k.txt \
152
  --gene_mapping_path model_dependencies/gene2ens.csv \
153
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
167
  | `--knn_k` | 10 | `k` for kNN purity. |
168
  | `--null_permutations` | 200 | Number of label permutations for the empirical null distribution. |
169
  | `--include_other_in_metrics` | Off | By default, modules from the `Other` family are excluded. |
170
+ | `--shuffle_biomod_genes` | Off | Randomly shuffle gene-to-module assignments as a negative control or ablation. Uses the same flag as pretraining and downstream finetuning. |
171
  | `--umap_n_neighbors` | 15 | UMAP graph-construction parameter. |
172
  | `--umap_min_dist` | 0.3 | UMAP layout parameter. |
173
 
 
190
 
191
  Two control experiments make Experiment 1 more rigorous:
192
 
193
+ 1. **Random module assignment**: rerun with `--shuffle_biomod_genes`. This flag preserves module sizes but randomly assigns genes to modules, matching the same ablation flag used for pretraining and finetuning. If geometry is pathway-driven rather than a generic property of the encoder, family separation should drop close to the null distribution.
194
 
195
+ 2. **Untrained backbone**: set `--biomod_checkpoint` to the `*_init.pth` checkpoint generated during pretraining with `--save_init_ckpt_and_exit`. Family separation should be close to the null distribution. The gap between trained and untrained models quantifies the contribution of pretraining.
196
 
197
  Together, these controls support the claim that the family structure of the module
198
  latent space is produced by pathway-based module design plus pretraining, rather
 
200
 
201
  ### Control 1: Random Module Assignment (`shuffle_module_genes`)
202
 
203
+ Add `--shuffle_biomod_genes` to the original command and write to a control output
204
  directory.
205
 
206
  For h5ad data, using `pancreas_scib` as an example:
207
 
208
  ```bash
209
+ python BioMod/interpretability/biomod_geometry.py \
210
+ --biomod_checkpoint weight/BioMod.pth \
211
+ --config_pkl weight/BioMod.pkl \
212
+ --biomod_data_dir model_dependencies/biomod_data_all \
213
  --gene_list_path model_dependencies/selected_genes_27k.txt \
214
  --gene_mapping_path model_dependencies/gene2ens.csv \
215
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
218
  --data_mode h5ad \
219
  --gene_name_column auto \
220
  --batch_size 640 \
221
+ --shuffle_biomod_genes \
222
  --output_dir outputs/interpretability/geometry/pancreas_scib_shuffle
223
  ```
224
 
225
  For pretraining npz data:
226
 
227
  ```bash
228
+ python BioMod/interpretability/biomod_geometry.py \
229
+ --biomod_checkpoint weight/BioMod.pth \
230
+ --config_pkl weight/BioMod.pkl \
231
+ --biomod_data_dir model_dependencies/biomod_data_all \
232
  --gene_list_path model_dependencies/selected_genes_27k.txt \
233
  --gene_mapping_path model_dependencies/gene2ens.csv \
234
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
237
  --data_mode pretrain_npz \
238
  --max_cells 50000 \
239
  --batch_size 640 \
240
+ --shuffle_biomod_genes \
241
  --output_dir outputs/interpretability/geometry/pretrain_subset_shuffle
242
  ```
243
 
244
+ How `--shuffle_biomod_genes` works: inside `load_backbone()`, module sizes are
245
  kept unchanged, all genes are collected into one list, the list is randomly
246
  shuffled, and genes are reassigned to modules. The seed is controlled by
247
  `--shuffle_seed`, defaulting to 2021. Model weights are unchanged; only the
 
253
 
254
  ### Control 2: Untrained Backbone with Random Initialization
255
 
256
+ First generate a randomly initialized checkpoint, then run `biomod_geometry.py`
257
  with it.
258
 
259
  A matching randomly initialized checkpoint and configuration are not bundled.
 
263
  paths explicitly; do not use the pretrained BioMod weights for this control.
264
 
265
  ```bash
266
+ python BioMod/interpretability/biomod_geometry.py \
267
+ --biomod_checkpoint /absolute/path/to/random_init.pth \
268
  --config_pkl /absolute/path/to/random_init.pkl \
269
  --data_path data/downstream/final_data/human_pancreas_norm_complexBatch_with_gene_name.h5ad \
270
  --data_mode h5ad \
 
292
  space is generated by both pathway-based module design and pretraining; both are
293
  necessary.
294
 
295
+ ## Experiment 2: Module Embedding to Pathway Text Alignment (`biomod_text_alignment/`)
296
 
297
  **Hypothesis:** the structure of the BioMod module embedding space significantly
298
  aligns with natural-language pathway semantics. Semantically similar pathways
 
301
 
302
  **Method:**
303
 
304
+ 1. Parse 541 semantically named modules from `merged_biomods.json`: 50 Hallmark and 491 Reactome.
305
  2. Encode pathway names with S-PubMedBert-MS-MARCO, a sentence-level fine-tuned PubMedBERT model.
306
  3. Load `module_embeddings.npy` from Experiment 1, containing cell-averaged module embeddings.
307
  4. Measure alignment:
 
322
  # Network access or an existing local model cache is required.
323
 
324
  # Main experiment: RSA + CKA + LOO retrieval.
325
+ python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment.py \
326
+ --biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
327
+ --biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
328
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
329
  --n_permutations 1000 \
330
  --output_dir outputs/interpretability/text_alignment/pretrain_subset_v2
331
 
332
  # Extended analysis: broad Reactome categories and Hallmark deep dive.
333
+ python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment_extended.py \
334
+ --biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
335
+ --biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
336
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
337
+ --descriptions_file BioMod/interpretability/biomod_text_alignment/reactome_descriptions.json \
338
  --n_permutations 1000 \
339
  --output_dir outputs/interpretability/text_alignment/pretrain_subset_v2_extended
340
  ```
BioMod/interpretability/__init__.py CHANGED
@@ -1,8 +1,8 @@
1
- """Module interpretability experiments for scLong (performer_module).
2
 
3
  Roadmap (priority order):
4
- 1. module_geometry.py — intrinsic module-embedding geometry (current).
5
- 2. module_text_alignment.py — module ↔ pathway natural-language alignment.
6
- 3. module_importance.py — task-driven ablation + literature enrichment.
7
  4. module_vs_gene.py — comparison with gene-level baselines (GenePT, scGPT, ...).
8
  """
 
1
+ """Module interpretability experiments for scLong (biomod).
2
 
3
  Roadmap (priority order):
4
+ 1. biomod_geometry.py — intrinsic module-embedding geometry (current).
5
+ 2. biomod_text_alignment.py — module ↔ pathway natural-language alignment.
6
+ 3. biomod_importance.py — task-driven ablation + literature enrichment.
7
  4. module_vs_gene.py — comparison with gene-level baselines (GenePT, scGPT, ...).
8
  """
BioMod/interpretability/attention/README.md CHANGED
@@ -30,10 +30,10 @@ included; replace `/absolute/path/to/pretraining_cell_npz` with your own directo
30
  # Run from the BioMod repository root.
31
 
32
  # Full run, methods A + C.
33
- python BioMod/interpretability/attention/module_crosstalk.py \
34
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
35
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
36
- --module_data_dir model_dependencies/module_data_all \
37
  --gene_list_path model_dependencies/selected_genes_27k.txt \
38
  --gene_mapping_path model_dependencies/gene2ens.csv \
39
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -46,10 +46,10 @@ python BioMod/interpretability/attention/module_crosstalk.py \
46
  --pool_mode max
47
 
48
  # Method A only, for quick validation.
49
- python BioMod/interpretability/attention/module_crosstalk.py \
50
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
51
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
52
- --module_data_dir model_dependencies/module_data_all \
53
  --gene_list_path model_dependencies/selected_genes_27k.txt \
54
  --gene_mapping_path model_dependencies/gene2ens.csv \
55
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -61,10 +61,10 @@ python BioMod/interpretability/attention/module_crosstalk.py \
61
  --output_dir outputs/interpretability/crosstalk/pancreas_cosine_only
62
 
63
  # Pretraining npz data.
64
- python BioMod/interpretability/attention/module_crosstalk.py \
65
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
66
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
67
- --module_data_dir model_dependencies/module_data_all \
68
  --gene_list_path model_dependencies/selected_genes_27k.txt \
69
  --gene_mapping_path model_dependencies/gene2ens.csv \
70
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
30
  # Run from the BioMod repository root.
31
 
32
  # Full run, methods A + C.
33
+ python BioMod/interpretability/attention/biomod_crosstalk.py \
34
+ --biomod_checkpoint weight/BioMod.pth \
35
+ --config_pkl weight/BioMod.pkl \
36
+ --biomod_data_dir model_dependencies/biomod_data_all \
37
  --gene_list_path model_dependencies/selected_genes_27k.txt \
38
  --gene_mapping_path model_dependencies/gene2ens.csv \
39
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
46
  --pool_mode max
47
 
48
  # Method A only, for quick validation.
49
+ python BioMod/interpretability/attention/biomod_crosstalk.py \
50
+ --biomod_checkpoint weight/BioMod.pth \
51
+ --config_pkl weight/BioMod.pkl \
52
+ --biomod_data_dir model_dependencies/biomod_data_all \
53
  --gene_list_path model_dependencies/selected_genes_27k.txt \
54
  --gene_mapping_path model_dependencies/gene2ens.csv \
55
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
61
  --output_dir outputs/interpretability/crosstalk/pancreas_cosine_only
62
 
63
  # Pretraining npz data.
64
+ python BioMod/interpretability/attention/biomod_crosstalk.py \
65
+ --biomod_checkpoint weight/BioMod.pth \
66
+ --config_pkl weight/BioMod.pkl \
67
+ --biomod_data_dir model_dependencies/biomod_data_all \
68
  --gene_list_path model_dependencies/selected_genes_27k.txt \
69
  --gene_mapping_path model_dependencies/gene2ens.csv \
70
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
BioMod/interpretability/attention/README_gene_attention.md CHANGED
@@ -1,7 +1,7 @@
1
- # Gene Attention Interpretability: ModuleEmbedding
2
 
3
  Analyze gene contributions within modules using the content-based attention
4
- pooling in ModuleEmbedding.
5
 
6
  ## Analyses
7
 
@@ -32,10 +32,10 @@ the column name has not been assumed for the bundled pancreas file.
32
  ```bash
33
  # Run from the BioMod repository root.
34
 
35
- python BioMod/interpretability/attention/module_gene_attention.py \
36
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
37
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
38
- --module_data_dir model_dependencies/module_data_all \
39
  --gene_list_path model_dependencies/selected_genes_27k.txt \
40
  --gene_mapping_path model_dependencies/gene2ens.csv \
41
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -50,10 +50,10 @@ python BioMod/interpretability/attention/module_gene_attention.py \
50
  ### With DEG Comparison
51
 
52
  ```bash
53
- python BioMod/interpretability/attention/module_gene_attention.py \
54
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
55
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
56
- --module_data_dir model_dependencies/module_data_all \
57
  --gene_list_path model_dependencies/selected_genes_27k.txt \
58
  --gene_mapping_path model_dependencies/gene2ens.csv \
59
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -89,7 +89,7 @@ Add `--max_cells 5000` to the complete basic command above and change
89
  | `--marker_db` | None | Custom marker CSV; built-in pancreas markers are used when omitted |
90
  | `--deg_csv` | None | DEG result CSV; DEG correlation analysis is skipped when omitted |
91
  | `--celltype_column` | celltype | Cell-type column name in h5ad |
92
- | `--shuffle_module_genes` | off | Randomly shuffle gene-to-module assignment as a negative control |
93
 
94
  ## Output Files
95
 
 
1
+ # Gene Attention Interpretability: BioModEmbedding
2
 
3
  Analyze gene contributions within modules using the content-based attention
4
+ pooling in BioModEmbedding.
5
 
6
  ## Analyses
7
 
 
32
  ```bash
33
  # Run from the BioMod repository root.
34
 
35
+ python BioMod/interpretability/attention/biomod_gene_attention.py \
36
+ --biomod_checkpoint weight/BioMod.pth \
37
+ --config_pkl weight/BioMod.pkl \
38
+ --biomod_data_dir model_dependencies/biomod_data_all \
39
  --gene_list_path model_dependencies/selected_genes_27k.txt \
40
  --gene_mapping_path model_dependencies/gene2ens.csv \
41
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
50
  ### With DEG Comparison
51
 
52
  ```bash
53
+ python BioMod/interpretability/attention/biomod_gene_attention.py \
54
+ --biomod_checkpoint weight/BioMod.pth \
55
+ --config_pkl weight/BioMod.pkl \
56
+ --biomod_data_dir model_dependencies/biomod_data_all \
57
  --gene_list_path model_dependencies/selected_genes_27k.txt \
58
  --gene_mapping_path model_dependencies/gene2ens.csv \
59
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
89
  | `--marker_db` | None | Custom marker CSV; built-in pancreas markers are used when omitted |
90
  | `--deg_csv` | None | DEG result CSV; DEG correlation analysis is skipped when omitted |
91
  | `--celltype_column` | celltype | Cell-type column name in h5ad |
92
+ | `--shuffle_biomod_genes` | off | Randomly shuffle gene-to-module assignment as a negative control |
93
 
94
  ## Output Files
95
 
BioMod/interpretability/attention/{module_crosstalk.py → biomod_crosstalk.py} RENAMED
@@ -19,10 +19,10 @@
19
  - Permutation null p-values for all metrics
20
 
21
  Usage (run from the BioMod repository root):
22
- python BioMod/interpretability/attention/module_crosstalk.py \
23
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
24
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
25
- --module_data_dir model_dependencies/module_data_all \
26
  --gene_list_path model_dependencies/selected_genes_27k.txt \
27
  --gene_mapping_path model_dependencies/gene2ens.csv \
28
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -53,14 +53,14 @@
53
  from sklearn.metrics import roc_auc_score
54
 
55
  HERE = Path(__file__).resolve()
56
- PERFORMER_MODULE = HERE.parents[2]
57
  REPO_ROOT = HERE.parents[3]
58
  ASSET_ROOT = REPO_ROOT / "model_dependencies"
59
- for p in (str(PERFORMER_MODULE), str(REPO_ROOT)):
60
  if p not in sys.path:
61
  sys.path.insert(0, p)
62
 
63
- from cell_annotation.performer_module_finetune import (
64
  DataPreprocessor,
65
  load_backbone,
66
  seed_all,
@@ -69,9 +69,9 @@
69
  from interpretability.utils import (
70
  MatrixRowDataset,
71
  NpzCellDataset,
72
- build_module_metadata,
73
  ensure_dir,
74
- load_module_names,
75
  )
76
 
77
  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
@@ -84,16 +84,16 @@
84
  def parse_args() -> argparse.Namespace:
85
  p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
86
 
87
- p.add_argument("--performer_checkpoint", type=str, required=True)
88
  p.add_argument("--config_pkl", type=str, required=True)
89
- p.add_argument("--module_data_dir", type=str, default=str(ASSET_ROOT / "module_data_all"))
90
- p.add_argument("--module_json", type=str, default=None)
91
  p.add_argument("--gene_list_path", type=str, default=str(ASSET_ROOT / "selected_genes_27k.txt"))
92
  p.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
93
  p.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / "selected_gene2vec_27k.npy"))
94
  p.add_argument("--gene2ens_file", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
95
  p.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
96
- p.add_argument("--shuffle_module_genes", action="store_true")
97
  p.add_argument("--shuffle_seed", type=int, default=2021)
98
 
99
  p.add_argument("--data_path", type=str, required=True)
@@ -129,7 +129,7 @@ def parse_args() -> argparse.Namespace:
129
 
130
 
131
  # ---------------------------------------------------------------------------
132
- # Data loading (reuses patterns from module_geometry.py)
133
  # ---------------------------------------------------------------------------
134
 
135
  def build_dataset(args, preprocessor):
@@ -273,9 +273,9 @@ def _compute_single_cell_jacobian(backbone, cell, num_modules, base_dim, device)
273
 
274
  with torch.enable_grad():
275
  if backbone.mask_style == "replace":
276
- x = backbone._add_module_position_embedding(x_modules)
277
  else:
278
- x = backbone._add_module_position_embedding(x_modules)
279
 
280
  if backbone.use_cls and backbone.cls_token is not None:
281
  cls_token = backbone.cls_token + backbone.pos_emb[:, :1, :]
@@ -477,16 +477,16 @@ def main():
477
  setattr(args, attr, str(Path(val).resolve()))
478
 
479
  logger.info("Loading backbone...")
480
- backbone, module_data, model_config = load_backbone(args, device)
481
  backbone.eval()
482
 
483
- num_modules = len(module_data)
484
  base_dim = int(model_config["base_dim"])
485
  expected_genes = int(model_config.get("num_genes", 27874))
486
  logger.info("Model: %d modules, dim=%d, genes=%d", num_modules, base_dim, expected_genes)
487
 
488
- module_json_path = args.module_json or str(Path(args.module_data_dir) / "merged_modules.json")
489
- module_names = load_module_names(module_json_path)
490
  with open(module_json_path, "r", encoding="utf-8") as f:
491
  module_gene_dict = json.load(f)
492
 
@@ -571,7 +571,7 @@ def main():
571
  json.dump(results, f, indent=2, ensure_ascii=False)
572
 
573
  # --- Plots ---
574
- meta = build_module_metadata(module_names)
575
  families = [row["family"] for row in meta]
576
 
577
  plot_interaction_matrix(cos_matrix, "Method A: Cosine Similarity",
 
19
  - Permutation null p-values for all metrics
20
 
21
  Usage (run from the BioMod repository root):
22
+ python BioMod/interpretability/attention/biomod_crosstalk.py \
23
+ --biomod_checkpoint weight/BioMod.pth \
24
+ --config_pkl weight/BioMod.pkl \
25
+ --biomod_data_dir model_dependencies/biomod_data_all \
26
  --gene_list_path model_dependencies/selected_genes_27k.txt \
27
  --gene_mapping_path model_dependencies/gene2ens.csv \
28
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
53
  from sklearn.metrics import roc_auc_score
54
 
55
  HERE = Path(__file__).resolve()
56
+ BIOMOD_SOURCE = HERE.parents[2]
57
  REPO_ROOT = HERE.parents[3]
58
  ASSET_ROOT = REPO_ROOT / "model_dependencies"
59
+ for p in (str(BIOMOD_SOURCE), str(REPO_ROOT)):
60
  if p not in sys.path:
61
  sys.path.insert(0, p)
62
 
63
+ from cell_annotation.biomod_finetune import (
64
  DataPreprocessor,
65
  load_backbone,
66
  seed_all,
 
69
  from interpretability.utils import (
70
  MatrixRowDataset,
71
  NpzCellDataset,
72
+ build_biomod_metadata,
73
  ensure_dir,
74
+ load_biomod_names,
75
  )
76
 
77
  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
 
84
  def parse_args() -> argparse.Namespace:
85
  p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
86
 
87
+ p.add_argument("--biomod_checkpoint", "--performer_checkpoint", type=str, required=True, dest='performer_checkpoint')
88
  p.add_argument("--config_pkl", type=str, required=True)
89
+ p.add_argument("--biomod_data_dir", "--module_data_dir", type=str, default=str(ASSET_ROOT / "biomod_data_all"), dest='module_data_dir')
90
+ p.add_argument("--biomod_json", "--module_json", type=str, default=None, dest='module_json')
91
  p.add_argument("--gene_list_path", type=str, default=str(ASSET_ROOT / "selected_genes_27k.txt"))
92
  p.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
93
  p.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / "selected_gene2vec_27k.npy"))
94
  p.add_argument("--gene2ens_file", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
95
  p.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
96
+ p.add_argument("--shuffle_biomod_genes", "--shuffle_module_genes", action="store_true", dest='shuffle_module_genes')
97
  p.add_argument("--shuffle_seed", type=int, default=2021)
98
 
99
  p.add_argument("--data_path", type=str, required=True)
 
129
 
130
 
131
  # ---------------------------------------------------------------------------
132
+ # Data loading (reuses patterns from biomod_geometry.py)
133
  # ---------------------------------------------------------------------------
134
 
135
  def build_dataset(args, preprocessor):
 
273
 
274
  with torch.enable_grad():
275
  if backbone.mask_style == "replace":
276
+ x = backbone._add_biomod_position_embedding(x_modules)
277
  else:
278
+ x = backbone._add_biomod_position_embedding(x_modules)
279
 
280
  if backbone.use_cls and backbone.cls_token is not None:
281
  cls_token = backbone.cls_token + backbone.pos_emb[:, :1, :]
 
477
  setattr(args, attr, str(Path(val).resolve()))
478
 
479
  logger.info("Loading backbone...")
480
+ backbone, biomod_data, model_config = load_backbone(args, device)
481
  backbone.eval()
482
 
483
+ num_modules = len(biomod_data)
484
  base_dim = int(model_config["base_dim"])
485
  expected_genes = int(model_config.get("num_genes", 27874))
486
  logger.info("Model: %d modules, dim=%d, genes=%d", num_modules, base_dim, expected_genes)
487
 
488
+ module_json_path = args.module_json or str(Path(args.module_data_dir) / "merged_biomods.json")
489
+ module_names = load_biomod_names(module_json_path)
490
  with open(module_json_path, "r", encoding="utf-8") as f:
491
  module_gene_dict = json.load(f)
492
 
 
571
  json.dump(results, f, indent=2, ensure_ascii=False)
572
 
573
  # --- Plots ---
574
+ meta = build_biomod_metadata(module_names)
575
  families = [row["family"] for row in meta]
576
 
577
  plot_interaction_matrix(cos_matrix, "Method A: Cosine Similarity",
BioMod/interpretability/attention/{module_gene_attention.py → biomod_gene_attention.py} RENAMED
@@ -1,6 +1,6 @@
1
- """ModuleEmbedding Attention-based Gene Interpretability Analysis (Streaming).
2
 
3
- Interpretability analysis using single-head content-based attention pooling in ModuleEmbedding.
4
  Streaming computing mode: memory usage O(n_celltypes × n_genes).
5
 
6
  Analysis content:
@@ -14,10 +14,10 @@
14
  8. DEG comparison
15
 
16
  Usage (run from the BioMod repository root):
17
- python BioMod/interpretability/attention/module_gene_attention.py \
18
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
19
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
20
- --module_data_dir model_dependencies/module_data_all \
21
  --gene_list_path model_dependencies/selected_genes_27k.txt \
22
  --gene_mapping_path model_dependencies/gene2ens.csv \
23
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -51,30 +51,30 @@
51
  from scipy.stats import spearmanr, t as t_dist
52
 
53
  HERE = Path(__file__).resolve()
54
- PERFORMER_MODULE = HERE.parents[2]
55
  REPO_ROOT = HERE.parents[3]
56
  ASSET_ROOT = REPO_ROOT / "model_dependencies"
57
- for p in (str(PERFORMER_MODULE), str(REPO_ROOT)):
58
  if p not in sys.path:
59
  sys.path.insert(0, p)
60
 
61
  # Publication utilities
62
- MODULE_IMPORTANCE_DIR = PERFORMER_MODULE / "interpretability" / "module_importance"
63
- if str(MODULE_IMPORTANCE_DIR) not in sys.path:
64
- sys.path.insert(0, str(MODULE_IMPORTANCE_DIR))
65
 
66
  from pub_utils import setup_publication_style, savefig as pub_savefig
67
 
68
- from cell_annotation.performer_module_finetune import (
69
  DataPreprocessor,
70
  load_backbone,
71
  seed_all,
72
  )
73
  from interpretability.utils import (
74
  MatrixRowDataset,
75
- build_module_metadata,
76
  ensure_dir,
77
- load_module_names,
78
  )
79
 
80
  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
@@ -88,16 +88,16 @@
88
  def parse_args() -> argparse.Namespace:
89
  p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
90
 
91
- p.add_argument("--performer_checkpoint", type=str, required=True)
92
  p.add_argument("--config_pkl", type=str, required=True)
93
- p.add_argument("--module_data_dir", type=str, default=str(ASSET_ROOT / "module_data_all"))
94
- p.add_argument("--module_json", type=str, default=None)
95
  p.add_argument("--gene_list_path", type=str, default=str(ASSET_ROOT / "selected_genes_27k.txt"))
96
  p.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
97
  p.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / "selected_gene2vec_27k.npy"))
98
  p.add_argument("--gene2ens_file", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
99
  p.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
100
- p.add_argument("--shuffle_module_genes", action="store_true")
101
  p.add_argument("--shuffle_seed", type=int, default=2021)
102
 
103
  p.add_argument("--data_path", type=str, required=True)
@@ -915,7 +915,7 @@ def main():
915
 
916
  # --- Load model ---
917
  logger.info("Loading backbone from %s", args.performer_checkpoint)
918
- backbone, module_data, model_config = load_backbone(args, device)
919
  backbone.eval()
920
 
921
  # --- Build module gene name mapping ---
 
1
+ """BioModEmbedding Attention-based Gene Interpretability Analysis (Streaming).
2
 
3
+ Interpretability analysis using single-head content-based attention pooling in BioModEmbedding.
4
  Streaming computing mode: memory usage O(n_celltypes × n_genes).
5
 
6
  Analysis content:
 
14
  8. DEG comparison
15
 
16
  Usage (run from the BioMod repository root):
17
+ python BioMod/interpretability/attention/biomod_gene_attention.py \
18
+ --biomod_checkpoint weight/BioMod.pth \
19
+ --config_pkl weight/BioMod.pkl \
20
+ --biomod_data_dir model_dependencies/biomod_data_all \
21
  --gene_list_path model_dependencies/selected_genes_27k.txt \
22
  --gene_mapping_path model_dependencies/gene2ens.csv \
23
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
51
  from scipy.stats import spearmanr, t as t_dist
52
 
53
  HERE = Path(__file__).resolve()
54
+ BIOMOD_SOURCE = HERE.parents[2]
55
  REPO_ROOT = HERE.parents[3]
56
  ASSET_ROOT = REPO_ROOT / "model_dependencies"
57
+ for p in (str(BIOMOD_SOURCE), str(REPO_ROOT)):
58
  if p not in sys.path:
59
  sys.path.insert(0, p)
60
 
61
  # Publication utilities
62
+ BIOMOD_IMPORTANCE_DIR = BIOMOD_SOURCE / "interpretability" / "biomod_importance"
63
+ if str(BIOMOD_IMPORTANCE_DIR) not in sys.path:
64
+ sys.path.insert(0, str(BIOMOD_IMPORTANCE_DIR))
65
 
66
  from pub_utils import setup_publication_style, savefig as pub_savefig
67
 
68
+ from cell_annotation.biomod_finetune import (
69
  DataPreprocessor,
70
  load_backbone,
71
  seed_all,
72
  )
73
  from interpretability.utils import (
74
  MatrixRowDataset,
75
+ build_biomod_metadata,
76
  ensure_dir,
77
+ load_biomod_names,
78
  )
79
 
80
  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
 
88
  def parse_args() -> argparse.Namespace:
89
  p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
90
 
91
+ p.add_argument("--biomod_checkpoint", "--performer_checkpoint", type=str, required=True, dest='performer_checkpoint')
92
  p.add_argument("--config_pkl", type=str, required=True)
93
+ p.add_argument("--biomod_data_dir", "--module_data_dir", type=str, default=str(ASSET_ROOT / "biomod_data_all"), dest='module_data_dir')
94
+ p.add_argument("--biomod_json", "--module_json", type=str, default=None, dest='module_json')
95
  p.add_argument("--gene_list_path", type=str, default=str(ASSET_ROOT / "selected_genes_27k.txt"))
96
  p.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
97
  p.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / "selected_gene2vec_27k.npy"))
98
  p.add_argument("--gene2ens_file", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
99
  p.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
100
+ p.add_argument("--shuffle_biomod_genes", "--shuffle_module_genes", action="store_true", dest='shuffle_module_genes')
101
  p.add_argument("--shuffle_seed", type=int, default=2021)
102
 
103
  p.add_argument("--data_path", type=str, required=True)
 
915
 
916
  # --- Load model ---
917
  logger.info("Loading backbone from %s", args.performer_checkpoint)
918
+ backbone, biomod_data, model_config = load_backbone(args, device)
919
  backbone.eval()
920
 
921
  # --- Build module gene name mapping ---
BioMod/interpretability/{module_geometry.py → biomod_geometry.py} RENAMED
@@ -12,7 +12,7 @@
12
  1. Load pretrained backbone (forward with mask_ratio=0).
13
  2. Iterate over a population of cells, accumulate the per-module encoder output
14
  in a streaming average -> [num_modules, base_dim].
15
- 3. Parse each module's family from merged_modules.json keys
16
  (Hallmarks / Reactome / Hotspot / Processed) and, where applicable, the
17
  MSigDB hallmark process subcategory (immune / metabolic / signaling / ...).
18
  4. Quantify family separation:
@@ -44,8 +44,8 @@
44
 
45
  CLI
46
  ---
47
- python -m performer_module.interpretability.module_geometry \\
48
- --performer_checkpoint <ckpt.pth> \\
49
  --config_pkl <config.pkl> \\
50
  --data_path <path.h5ad> \\
51
  --data_mode h5ad \\
@@ -79,14 +79,14 @@
79
 
80
  # Make sibling modules importable when launched as a script
81
  HERE = Path(__file__).resolve()
82
- PERFORMER_MODULE = HERE.parents[1]
83
  REPO_ROOT = HERE.parents[2]
84
  ASSET_ROOT = REPO_ROOT / "model_dependencies"
85
- for p in (str(PERFORMER_MODULE), str(REPO_ROOT)):
86
  if p not in sys.path:
87
  sys.path.insert(0, p)
88
 
89
- from cell_annotation.performer_module_finetune import ( # noqa: E402
90
  DataPreprocessor,
91
  get_rank,
92
  get_world_size,
@@ -102,9 +102,9 @@
102
  from interpretability.utils import ( # noqa: E402
103
  MatrixRowDataset,
104
  NpzCellDataset,
105
- build_module_metadata,
106
  ensure_dir,
107
- load_module_names,
108
  )
109
 
110
  logger = logging.getLogger(__name__)
@@ -115,19 +115,19 @@
115
  def parse_args() -> argparse.Namespace:
116
  parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
117
 
118
- # Pretrained backbone (mirrors cell_annotation.performer_module_finetune)
119
- parser.add_argument("--performer_checkpoint", type=str, required=True)
120
  parser.add_argument("--config_pkl", type=str, required=True)
121
- parser.add_argument("--module_data_dir", type=str, default=str(ASSET_ROOT / "module_data_all"))
122
- parser.add_argument("--module_json", type=str, default=None,
123
- help="Path to merged_modules.json. Defaults to <module_data_dir>/merged_modules.json.")
124
  parser.add_argument("--gene_list_path", type=str, default=str(ASSET_ROOT / "selected_genes_27k.txt"))
125
  parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
126
  parser.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / "selected_gene2vec_27k.npy"))
127
  parser.add_argument("--gene2ens_file", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
128
  parser.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
129
- parser.add_argument("--shuffle_module_genes", action="store_true",
130
- help="Randomly reshuffle gene-to-module assignment (control / ablation).")
131
  parser.add_argument("--shuffle_seed", type=int, default=2021)
132
 
133
  # Data
@@ -245,7 +245,7 @@ def build_inference_loader(
245
  # ---------- inference: cell-averaged module embeddings ----------
246
 
247
  @torch.no_grad()
248
- def extract_cell_averaged_module_embeddings(
249
  backbone,
250
  loader: DataLoader,
251
  preprocessor: Optional[DataPreprocessor],
@@ -285,7 +285,7 @@ def extract_cell_averaged_module_embeddings(
285
  raise RuntimeError(
286
  f"Encoder output shape mismatch: got {tuple(enc.shape)}, "
287
  f"expected (*, {num_modules}, {base_dim}). "
288
- "Module order between merged_modules.json and the loaded backbone may have drifted."
289
  )
290
  sum_emb += enc.to(torch.float64).sum(dim=0)
291
  n_cells += enc.shape[0]
@@ -497,13 +497,13 @@ def setup_logging(output_dir: Path) -> None:
497
  )
498
 
499
 
500
- def resolve_module_json(args: argparse.Namespace) -> str:
501
- """Resolve relative paths against performer_module/cell_annotation/ to stay
502
  consistent with how `load_backbone` reads `module_data_dir`.
503
  """
504
  if args.module_json is not None:
505
  return str(ca_resolve_path(args.module_json))
506
- return str(ca_resolve_path(args.module_data_dir) / "merged_modules.json")
507
 
508
 
509
  def main() -> None:
@@ -526,15 +526,15 @@ def main() -> None:
526
  logger.info("DDP: use_ddp=%s, rank=%d/%d, device=%s", use_ddp, rank, world_size, device)
527
 
528
  # 1. Load backbone
529
- backbone, module_data, model_config = load_backbone(args, device)
530
  backbone.eval()
531
  base_dim = int(model_config["base_dim"])
532
- num_modules = len(module_data)
533
  logger.info("Loaded backbone: num_modules=%d, base_dim=%d", num_modules, base_dim)
534
 
535
- # 2. Load module names (must align with module_data order)
536
- module_json_path = resolve_module_json(args)
537
- module_names = load_module_names(module_json_path)
538
  if len(module_names) != num_modules:
539
  raise RuntimeError(
540
  f"Module name count ({len(module_names)} from {module_json_path}) does not match "
@@ -542,7 +542,7 @@ def main() -> None:
542
  "model and the one given here must be identical, with no modules dropped during "
543
  "gene-symbol resolution."
544
  )
545
- metadata = build_module_metadata(module_names)
546
  metadata_df = pd.DataFrame(metadata)
547
  if is_main_process():
548
  logger.info("Family distribution:\n%s", metadata_df["family"].value_counts().to_string())
@@ -569,7 +569,7 @@ def main() -> None:
569
 
570
  # 4. Inference
571
  logger.info("Extracting cell-averaged module embeddings over %d cells...", n_cells)
572
- mean_emb, processed = extract_cell_averaged_module_embeddings(
573
  backbone, loader, preprocessor, reindex_state, device,
574
  num_modules=num_modules, base_dim=base_dim,
575
  expected_input_genes=expected_input_genes,
 
12
  1. Load pretrained backbone (forward with mask_ratio=0).
13
  2. Iterate over a population of cells, accumulate the per-module encoder output
14
  in a streaming average -> [num_modules, base_dim].
15
+ 3. Parse each module's family from merged_biomods.json keys
16
  (Hallmarks / Reactome / Hotspot / Processed) and, where applicable, the
17
  MSigDB hallmark process subcategory (immune / metabolic / signaling / ...).
18
  4. Quantify family separation:
 
44
 
45
  CLI
46
  ---
47
+ python BioMod/interpretability/biomod_geometry.py \\
48
+ --biomod_checkpoint <ckpt.pth> \\
49
  --config_pkl <config.pkl> \\
50
  --data_path <path.h5ad> \\
51
  --data_mode h5ad \\
 
79
 
80
  # Make sibling modules importable when launched as a script
81
  HERE = Path(__file__).resolve()
82
+ BIOMOD_SOURCE = HERE.parents[1]
83
  REPO_ROOT = HERE.parents[2]
84
  ASSET_ROOT = REPO_ROOT / "model_dependencies"
85
+ for p in (str(BIOMOD_SOURCE), str(REPO_ROOT)):
86
  if p not in sys.path:
87
  sys.path.insert(0, p)
88
 
89
+ from cell_annotation.biomod_finetune import ( # noqa: E402
90
  DataPreprocessor,
91
  get_rank,
92
  get_world_size,
 
102
  from interpretability.utils import ( # noqa: E402
103
  MatrixRowDataset,
104
  NpzCellDataset,
105
+ build_biomod_metadata,
106
  ensure_dir,
107
+ load_biomod_names,
108
  )
109
 
110
  logger = logging.getLogger(__name__)
 
115
  def parse_args() -> argparse.Namespace:
116
  parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
117
 
118
+ # Pretrained backbone (mirrors cell_annotation.biomod_finetune)
119
+ parser.add_argument("--biomod_checkpoint", "--performer_checkpoint", type=str, required=True, dest='performer_checkpoint')
120
  parser.add_argument("--config_pkl", type=str, required=True)
121
+ parser.add_argument("--biomod_data_dir", "--module_data_dir", type=str, default=str(ASSET_ROOT / "biomod_data_all"), dest='module_data_dir')
122
+ parser.add_argument("--biomod_json", "--module_json", type=str, default=None,
123
+ help="Path to merged_biomods.json. Defaults to <module_data_dir>/merged_biomods.json.", dest='module_json')
124
  parser.add_argument("--gene_list_path", type=str, default=str(ASSET_ROOT / "selected_genes_27k.txt"))
125
  parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
126
  parser.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / "selected_gene2vec_27k.npy"))
127
  parser.add_argument("--gene2ens_file", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
128
  parser.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
129
+ parser.add_argument("--shuffle_biomod_genes", "--shuffle_module_genes", action="store_true",
130
+ help="Randomly reshuffle gene-to-module assignment (control / ablation).", dest='shuffle_module_genes')
131
  parser.add_argument("--shuffle_seed", type=int, default=2021)
132
 
133
  # Data
 
245
  # ---------- inference: cell-averaged module embeddings ----------
246
 
247
  @torch.no_grad()
248
+ def extract_cell_averaged_biomod_embeddings(
249
  backbone,
250
  loader: DataLoader,
251
  preprocessor: Optional[DataPreprocessor],
 
285
  raise RuntimeError(
286
  f"Encoder output shape mismatch: got {tuple(enc.shape)}, "
287
  f"expected (*, {num_modules}, {base_dim}). "
288
+ "Module order between merged_biomods.json and the loaded backbone may have drifted."
289
  )
290
  sum_emb += enc.to(torch.float64).sum(dim=0)
291
  n_cells += enc.shape[0]
 
497
  )
498
 
499
 
500
+ def resolve_biomod_json(args: argparse.Namespace) -> str:
501
+ """Resolve relative paths against biomod/cell_annotation/ to stay
502
  consistent with how `load_backbone` reads `module_data_dir`.
503
  """
504
  if args.module_json is not None:
505
  return str(ca_resolve_path(args.module_json))
506
+ return str(ca_resolve_path(args.module_data_dir) / "merged_biomods.json")
507
 
508
 
509
  def main() -> None:
 
526
  logger.info("DDP: use_ddp=%s, rank=%d/%d, device=%s", use_ddp, rank, world_size, device)
527
 
528
  # 1. Load backbone
529
+ backbone, biomod_data, model_config = load_backbone(args, device)
530
  backbone.eval()
531
  base_dim = int(model_config["base_dim"])
532
+ num_modules = len(biomod_data)
533
  logger.info("Loaded backbone: num_modules=%d, base_dim=%d", num_modules, base_dim)
534
 
535
+ # 2. Load module names (must align with biomod_data order)
536
+ module_json_path = resolve_biomod_json(args)
537
+ module_names = load_biomod_names(module_json_path)
538
  if len(module_names) != num_modules:
539
  raise RuntimeError(
540
  f"Module name count ({len(module_names)} from {module_json_path}) does not match "
 
542
  "model and the one given here must be identical, with no modules dropped during "
543
  "gene-symbol resolution."
544
  )
545
+ metadata = build_biomod_metadata(module_names)
546
  metadata_df = pd.DataFrame(metadata)
547
  if is_main_process():
548
  logger.info("Family distribution:\n%s", metadata_df["family"].value_counts().to_string())
 
569
 
570
  # 4. Inference
571
  logger.info("Extracting cell-averaged module embeddings over %d cells...", n_cells)
572
+ mean_emb, processed = extract_cell_averaged_biomod_embeddings(
573
  backbone, loader, preprocessor, reindex_state, device,
574
  num_modules=num_modules, base_dim=base_dim,
575
  expected_input_genes=expected_input_genes,
BioMod/interpretability/{module_importance → biomod_importance}/README.md RENAMED
@@ -33,10 +33,10 @@ preceding steps; they are not bundled inputs.
33
 
34
  ```bash
35
  # Run from the BioMod repository root.
36
- python BioMod/interpretability/module_importance/module_importance.py \
37
  --finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
38
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
39
- --module_data_dir model_dependencies/module_data_all \
40
  --gene_list_path model_dependencies/selected_genes_27k.txt \
41
  --gene_mapping_path model_dependencies/gene2ens.csv \
42
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -52,10 +52,10 @@ python BioMod/interpretability/module_importance/module_importance.py \
52
  Pre-encoder mode, more accurate but slower:
53
 
54
  ```bash
55
- python BioMod/interpretability/module_importance/module_importance.py \
56
  --finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
57
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
58
- --module_data_dir model_dependencies/module_data_all \
59
  --gene_list_path model_dependencies/selected_genes_27k.txt \
60
  --gene_mapping_path model_dependencies/gene2ens.csv \
61
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -72,10 +72,10 @@ python BioMod/interpretability/module_importance/module_importance.py \
72
  Pre-encoder mode on all cells with DDP multi-GPU acceleration:
73
 
74
  ```bash
75
- torchrun --nproc_per_node=4 BioMod/interpretability/module_importance/module_importance.py \
76
  --finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
77
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
78
- --module_data_dir model_dependencies/module_data_all \
79
  --gene_list_path model_dependencies/selected_genes_27k.txt \
80
  --gene_mapping_path model_dependencies/gene2ens.csv \
81
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -132,10 +132,10 @@ If the model learns meaningful module importance:
132
  Progressively mask top, bottom, or random modules and measure the accuracy drop.
133
 
134
  ```bash
135
- python BioMod/interpretability/module_importance/masking_ablation.py \
136
  --finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
137
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
138
- --module_data_dir model_dependencies/module_data_all \
139
  --gene_list_path model_dependencies/selected_genes_27k.txt \
140
  --gene_mapping_path model_dependencies/gene2ens.csv \
141
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -154,11 +154,11 @@ python BioMod/interpretability/module_importance/masking_ablation.py \
154
 
155
  ```bash
156
  # Mode A: shuffle modules, GPU and slow.
157
- python BioMod/interpretability/module_importance/random_baseline.py \
158
  --mode shuffle_modules \
159
  --finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
160
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
161
- --module_data_dir model_dependencies/module_data_all \
162
  --gene_list_path model_dependencies/selected_genes_27k.txt \
163
  --gene_mapping_path model_dependencies/gene2ens.csv \
164
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -170,7 +170,7 @@ python BioMod/interpretability/module_importance/random_baseline.py \
170
  --output_dir outputs/interpretability/importance/permutation/
171
 
172
  # Mode B: shuffle labels, CPU and fast, requires per_cell_deltas from ablation.
173
- python BioMod/interpretability/module_importance/random_baseline.py \
174
  --mode shuffle_labels \
175
  --importance_matrix outputs/interpretability/importance/zheng68k_pre_13690/importance_matrix.npy \
176
  --per_cell_deltas outputs/interpretability/importance/ablation/per_cell_deltas.npy \
@@ -182,21 +182,21 @@ python BioMod/interpretability/module_importance/random_baseline.py \
182
  ### 3-C: Marker Gene Verification, CPU Only
183
 
184
  ```bash
185
- python BioMod/interpretability/module_importance/marker_gene_verification.py \
186
  --importance_matrix outputs/interpretability/importance/zheng68k_pre_13690/importance_matrix.npy \
187
  --importance_csv <importance_matrix.csv> \
188
- --module_data_dir model_dependencies/module_data_all \
189
  --output_dir outputs/interpretability/importance/marker_verification/
190
  ```
191
 
192
  ### Publication Figures
193
 
194
  ```bash
195
- python BioMod/interpretability/module_importance/module_importance_publication.py \
196
  --results_dir outputs/interpretability/importance/zheng68k_pre_13690/ \
197
  --ablation_dir outputs/interpretability/importance/ablation \
198
  --permutation_dir outputs/interpretability/importance/permutation/ \
199
- --module_data_dir model_dependencies/module_data_all \
200
  --output_dir outputs/interpretability/importance/publication_figures/
201
  ```
202
 
@@ -206,7 +206,7 @@ Analyze importance distributions, contribution fractions, and enrichment tests b
206
  module source family: Hallmarks, Reactome, Hotspot, and WGCNA.
207
 
208
  ```bash
209
- python BioMod/interpretability/module_importance/family_importance_analysis.py \
210
  --results_dir outputs/interpretability/importance/zheng68k_pre \
211
  --output_dir outputs/interpretability/importance/family_analysis \
212
  --top_k 10 20 50 \
@@ -231,10 +231,10 @@ Ablate whole families by removing or keeping one module family and measuring
231
  classification-accuracy changes.
232
 
233
  ```bash
234
- python BioMod/interpretability/module_importance/family_ablation.py \
235
  --finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
236
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
237
- --module_data_dir model_dependencies/module_data_all \
238
  --gene_list_path model_dependencies/selected_genes_27k.txt \
239
  --gene_mapping_path model_dependencies/gene2ens.csv \
240
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -257,7 +257,7 @@ Analyze correlations and similarity between important modules across the 11 cell
257
  classes.
258
 
259
  ```bash
260
- python BioMod/interpretability/module_importance/cross_class_similarity.py \
261
  --results_dir outputs/interpretability/importance/zheng68k_pre \
262
  --output_dir outputs/interpretability/importance/publication_figures \
263
  --top_k 50
@@ -275,10 +275,10 @@ parameters are trained from zero initialization.
275
 
276
  ```bash
277
  # Baseline, no new modules.
278
- python BioMod/cell_annotation/performer_module_finetune.py \
279
- --config_pkl weight/0810model/mae_tost_70M_75mask_ec2.pkl \
280
- --performer_checkpoint weight/0810model/mae_tost_70M_75mask_ec2_epoch0.pth \
281
- --module_data_dir model_dependencies/module_data_all \
282
  --gene_list_path model_dependencies/selected_genes_27k.txt \
283
  --gene_mapping_path model_dependencies/gene2ens.csv \
284
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
@@ -296,7 +296,7 @@ python BioMod/cell_annotation/performer_module_finetune.py \
296
  The same command can be extended with:
297
 
298
  - `--use_family_emb` to enable family-type embedding, adding `4 x 200 = 800` parameters.
299
- - `--use_module_gate` to enable sparse module gating, adding LayerNorm + Linear, 601 parameters.
300
  - Both flags can be used together.
301
 
302
  The new module parameters use `lr_head`, a higher learning rate, and are
@@ -306,11 +306,11 @@ independent of frozen backbone parameters.
306
 
307
  | File | Description |
308
  | --- | --- |
309
- | `module_importance.py` | Core importance computation with DDP support |
310
  | `masking_ablation.py` | Ablation-curve experiment, GPU |
311
  | `random_baseline.py` | Permutation tests, GPU/CPU |
312
  | `marker_gene_verification.py` | Marker verification, CPU |
313
- | `module_importance_publication.py` | Publication figures |
314
  | `family_importance_analysis.py` | Family-level importance analysis, CPU |
315
  | `family_ablation.py` | Family-level ablation, GPU |
316
  | `cross_class_similarity.py` | Cross-class similarity analysis, CPU |
 
33
 
34
  ```bash
35
  # Run from the BioMod repository root.
36
+ python BioMod/interpretability/biomod_importance/biomod_importance.py \
37
  --finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
38
+ --config_pkl weight/BioMod.pkl \
39
+ --biomod_data_dir model_dependencies/biomod_data_all \
40
  --gene_list_path model_dependencies/selected_genes_27k.txt \
41
  --gene_mapping_path model_dependencies/gene2ens.csv \
42
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
52
  Pre-encoder mode, more accurate but slower:
53
 
54
  ```bash
55
+ python BioMod/interpretability/biomod_importance/biomod_importance.py \
56
  --finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
57
+ --config_pkl weight/BioMod.pkl \
58
+ --biomod_data_dir model_dependencies/biomod_data_all \
59
  --gene_list_path model_dependencies/selected_genes_27k.txt \
60
  --gene_mapping_path model_dependencies/gene2ens.csv \
61
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
72
  Pre-encoder mode on all cells with DDP multi-GPU acceleration:
73
 
74
  ```bash
75
+ torchrun --nproc_per_node=4 BioMod/interpretability/biomod_importance/biomod_importance.py \
76
  --finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
77
+ --config_pkl weight/BioMod.pkl \
78
+ --biomod_data_dir model_dependencies/biomod_data_all \
79
  --gene_list_path model_dependencies/selected_genes_27k.txt \
80
  --gene_mapping_path model_dependencies/gene2ens.csv \
81
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
132
  Progressively mask top, bottom, or random modules and measure the accuracy drop.
133
 
134
  ```bash
135
+ python BioMod/interpretability/biomod_importance/masking_ablation.py \
136
  --finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
137
+ --config_pkl weight/BioMod.pkl \
138
+ --biomod_data_dir model_dependencies/biomod_data_all \
139
  --gene_list_path model_dependencies/selected_genes_27k.txt \
140
  --gene_mapping_path model_dependencies/gene2ens.csv \
141
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
154
 
155
  ```bash
156
  # Mode A: shuffle modules, GPU and slow.
157
+ python BioMod/interpretability/biomod_importance/random_baseline.py \
158
  --mode shuffle_modules \
159
  --finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
160
+ --config_pkl weight/BioMod.pkl \
161
+ --biomod_data_dir model_dependencies/biomod_data_all \
162
  --gene_list_path model_dependencies/selected_genes_27k.txt \
163
  --gene_mapping_path model_dependencies/gene2ens.csv \
164
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
170
  --output_dir outputs/interpretability/importance/permutation/
171
 
172
  # Mode B: shuffle labels, CPU and fast, requires per_cell_deltas from ablation.
173
+ python BioMod/interpretability/biomod_importance/random_baseline.py \
174
  --mode shuffle_labels \
175
  --importance_matrix outputs/interpretability/importance/zheng68k_pre_13690/importance_matrix.npy \
176
  --per_cell_deltas outputs/interpretability/importance/ablation/per_cell_deltas.npy \
 
182
  ### 3-C: Marker Gene Verification, CPU Only
183
 
184
  ```bash
185
+ python BioMod/interpretability/biomod_importance/marker_gene_verification.py \
186
  --importance_matrix outputs/interpretability/importance/zheng68k_pre_13690/importance_matrix.npy \
187
  --importance_csv <importance_matrix.csv> \
188
+ --biomod_data_dir model_dependencies/biomod_data_all \
189
  --output_dir outputs/interpretability/importance/marker_verification/
190
  ```
191
 
192
  ### Publication Figures
193
 
194
  ```bash
195
+ python BioMod/interpretability/biomod_importance/biomod_importance_publication.py \
196
  --results_dir outputs/interpretability/importance/zheng68k_pre_13690/ \
197
  --ablation_dir outputs/interpretability/importance/ablation \
198
  --permutation_dir outputs/interpretability/importance/permutation/ \
199
+ --biomod_data_dir model_dependencies/biomod_data_all \
200
  --output_dir outputs/interpretability/importance/publication_figures/
201
  ```
202
 
 
206
  module source family: Hallmarks, Reactome, Hotspot, and WGCNA.
207
 
208
  ```bash
209
+ python BioMod/interpretability/biomod_importance/family_importance_analysis.py \
210
  --results_dir outputs/interpretability/importance/zheng68k_pre \
211
  --output_dir outputs/interpretability/importance/family_analysis \
212
  --top_k 10 20 50 \
 
231
  classification-accuracy changes.
232
 
233
  ```bash
234
+ python BioMod/interpretability/biomod_importance/family_ablation.py \
235
  --finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
236
+ --config_pkl weight/BioMod.pkl \
237
+ --biomod_data_dir model_dependencies/biomod_data_all \
238
  --gene_list_path model_dependencies/selected_genes_27k.txt \
239
  --gene_mapping_path model_dependencies/gene2ens.csv \
240
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
257
  classes.
258
 
259
  ```bash
260
+ python BioMod/interpretability/biomod_importance/cross_class_similarity.py \
261
  --results_dir outputs/interpretability/importance/zheng68k_pre \
262
  --output_dir outputs/interpretability/importance/publication_figures \
263
  --top_k 50
 
275
 
276
  ```bash
277
  # Baseline, no new modules.
278
+ python BioMod/cell_annotation/biomod_finetune.py \
279
+ --config_pkl weight/BioMod.pkl \
280
+ --biomod_checkpoint weight/BioMod.pth \
281
+ --biomod_data_dir model_dependencies/biomod_data_all \
282
  --gene_list_path model_dependencies/selected_genes_27k.txt \
283
  --gene_mapping_path model_dependencies/gene2ens.csv \
284
  --gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
 
296
  The same command can be extended with:
297
 
298
  - `--use_family_emb` to enable family-type embedding, adding `4 x 200 = 800` parameters.
299
+ - `--use_biomod_gate` to enable sparse module gating, adding LayerNorm + Linear, 601 parameters.
300
  - Both flags can be used together.
301
 
302
  The new module parameters use `lr_head`, a higher learning rate, and are
 
306
 
307
  | File | Description |
308
  | --- | --- |
309
+ | `biomod_importance.py` | Core importance computation with DDP support |
310
  | `masking_ablation.py` | Ablation-curve experiment, GPU |
311
  | `random_baseline.py` | Permutation tests, GPU/CPU |
312
  | `marker_gene_verification.py` | Marker verification, CPU |
313
+ | `biomod_importance_publication.py` | Publication figures |
314
  | `family_importance_analysis.py` | Family-level importance analysis, CPU |
315
  | `family_ablation.py` | Family-level ablation, GPU |
316
  | `cross_class_similarity.py` | Cross-class similarity analysis, CPU |
BioMod/interpretability/{module_importance → biomod_importance}/RESULTS.md RENAMED
@@ -5,7 +5,7 @@
5
  ## Dataset
6
 
7
  - **Zheng68K**: 11 cell types, 68,450 cells, with 13,690 test cells.
8
- - Model: PerformerModuleCellClassifier with attention-pooling head and 48M backbone.
9
  - Baseline test accuracy: **83.1%**.
10
 
11
  ## Data Sources
@@ -13,9 +13,9 @@
13
  These are historical experiment results. The analysis output files below are not
14
  included in the repository; recreate them using the commands in `README.md` or
15
  point the analysis scripts to the original run directory. The shared module JSON
16
- is bundled under `model_dependencies/module_data_all/` at the repository root.
17
 
18
- All analyses are based on the following core files, generated by `module_importance.py`
19
  with `pre_encoder` over all 13,690 test cells:
20
 
21
  | File | Path | Description |
@@ -24,10 +24,10 @@ with `pre_encoder` over all 13,690 test cells:
24
  | `importance_matrix.csv` | `outputs/interpretability/importance/zheng68k_pre_13690/importance_matrix.csv` | Same matrix with row and column labels |
25
  | `enrichment_results.csv` | `outputs/interpretability/importance/zheng68k_pre_13690/enrichment_results.csv` | Enrichment-test results |
26
  | `top_modules_per_celltype.json` | `outputs/interpretability/importance/zheng68k_pre_13690/top_modules_per_celltype.json` | Top-module details |
27
- | `merged_modules.json` | `model_dependencies/module_data_all/merged_modules.json` | Gene composition of 660 modules |
28
 
29
  Marker verification uses `importance_matrix.npy`, `importance_matrix.csv`, and
30
- `merged_modules.json`.
31
 
32
  Publication figures use all files above plus ablation and permutation results.
33
 
 
5
  ## Dataset
6
 
7
  - **Zheng68K**: 11 cell types, 68,450 cells, with 13,690 test cells.
8
+ - Model: BioModCellClassifier with attention-pooling head and 48M backbone.
9
  - Baseline test accuracy: **83.1%**.
10
 
11
  ## Data Sources
 
13
  These are historical experiment results. The analysis output files below are not
14
  included in the repository; recreate them using the commands in `README.md` or
15
  point the analysis scripts to the original run directory. The shared module JSON
16
+ is bundled under `model_dependencies/biomod_data_all/` at the repository root.
17
 
18
+ All analyses are based on the following core files, generated by `biomod_importance.py`
19
  with `pre_encoder` over all 13,690 test cells:
20
 
21
  | File | Path | Description |
 
24
  | `importance_matrix.csv` | `outputs/interpretability/importance/zheng68k_pre_13690/importance_matrix.csv` | Same matrix with row and column labels |
25
  | `enrichment_results.csv` | `outputs/interpretability/importance/zheng68k_pre_13690/enrichment_results.csv` | Enrichment-test results |
26
  | `top_modules_per_celltype.json` | `outputs/interpretability/importance/zheng68k_pre_13690/top_modules_per_celltype.json` | Top-module details |
27
+ | `merged_biomods.json` | `model_dependencies/biomod_data_all/merged_biomods.json` | Gene composition of 660 modules |
28
 
29
  Marker verification uses `importance_matrix.npy`, `importance_matrix.csv`, and
30
+ `merged_biomods.json`.
31
 
32
  Publication figures use all files above plus ablation and permutation results.
33
 
BioMod/interpretability/{module_importance → biomod_importance}/TODO.md RENAMED
@@ -14,7 +14,7 @@
14
  - [x] `marker_gene_verification.py`: canonical marker verification.
15
  - Result: 80% of markers are in the top 5%, median percentile = 0.9%.
16
  - Figures: `fig_marker_ranks.pdf`, `fig_marker_summary_bar.pdf`.
17
- - [x] `module_importance_publication.py`: publication-figure generator.
18
  - [x] `RESULTS.md`: result documentation.
19
  - [x] `TODO.md`: this file.
20
 
 
14
  - [x] `marker_gene_verification.py`: canonical marker verification.
15
  - Result: 80% of markers are in the top 5%, median percentile = 0.9%.
16
  - Figures: `fig_marker_ranks.pdf`, `fig_marker_summary_bar.pdf`.
17
+ - [x] `biomod_importance_publication.py`: publication-figure generator.
18
  - [x] `RESULTS.md`: result documentation.
19
  - [x] `TODO.md`: this file.
20
 
BioMod/interpretability/{module_importance → biomod_importance}/__init__.py RENAMED
File without changes
BioMod/interpretability/{module_importance/module_importance.py → biomod_importance/biomod_importance.py} RENAMED
@@ -33,10 +33,10 @@
33
 
34
  SCRIPT_DIR = Path(__file__).resolve().parent
35
  INTERP_DIR = SCRIPT_DIR.parent
36
- PERFORMER_MODULE_ROOT = INTERP_DIR.parent
37
- SCLONG_ROOT = PERFORMER_MODULE_ROOT.parent
38
 
39
- for p in (str(PERFORMER_MODULE_ROOT), str(SCLONG_ROOT)):
40
  if p not in sys.path:
41
  sys.path.insert(0, p)
42
 
@@ -89,8 +89,8 @@ def __getitem__(self, idx):
89
 
90
  def load_finetuned_model(args, device: torch.device):
91
  """Load the full finetuned classifier (backbone + pooler + head)."""
92
- from models_performer_module import PerformerEncoderOnly_Module
93
- from module_dataset import load_module_data
94
 
95
  ckpt_path = Path(args.finetune_checkpoint)
96
  checkpoint = torch.load(ckpt_path, map_location=device)
@@ -106,13 +106,13 @@ def load_finetuned_model(args, device: torch.device):
106
  with open(config_pkl_path, "rb") as f:
107
  config_pkl = pickle.load(f)
108
 
109
- module_data = None
110
  if isinstance(config_pkl, dict) and "module_sets" in config_pkl:
111
- module_data = config_pkl["module_sets"]
112
  elif "module_sets" in backbone_config:
113
- module_data = backbone_config["module_sets"]
114
  else:
115
- module_data = load_module_data(
116
  module_data_dir=args.module_data_dir,
117
  gene_list_path=args.gene_list_path,
118
  gene_mapping_path=args.gene_mapping_path,
@@ -141,10 +141,10 @@ def load_finetuned_model(args, device: torch.device):
141
  dec_heads = backbone_config.get("dec_heads", 8)
142
  dec_dim_head = backbone_config.get("dec_dim_head", 64)
143
 
144
- backbone = PerformerEncoderOnly_Module(
145
- module_sets=module_data,
146
  num_genes=num_genes,
147
- max_seq_len=len(module_data),
148
  base_dim=base_dim,
149
  enc_depth=enc_depth,
150
  enc_heads=enc_heads,
@@ -171,10 +171,10 @@ def load_finetuned_model(args, device: torch.device):
171
  dec_dim_head=dec_dim_head,
172
  )
173
 
174
- sys.path.insert(0, str(PERFORMER_MODULE_ROOT / "cell_annotation"))
175
- from performer_module_finetune import (
176
  GatedAttentionPool,
177
- PerformerModuleCellClassifier,
178
  ScGPTClsDecoder,
179
  )
180
 
@@ -183,9 +183,9 @@ def load_finetuned_model(args, device: torch.device):
183
  pool_hidden_dim = saved_args.get("pool_hidden_dim", 0)
184
  cls_decoder_layers = saved_args.get("cls_decoder_layers", 3)
185
 
186
- model = PerformerModuleCellClassifier(
187
  backbone=backbone,
188
- module_sets=module_data,
189
  num_classes=num_classes,
190
  head_type=head_type,
191
  pool_hidden_dim=pool_hidden_dim,
@@ -199,9 +199,9 @@ def load_finetuned_model(args, device: torch.device):
199
 
200
  logger.info(
201
  "Loaded finetuned model: head=%s, classes=%d, modules=%d",
202
- head_type, num_classes, len(module_data),
203
  )
204
- return model, module_data, label_classes, head_type
205
 
206
 
207
  # ─── Data Loading ───────────────────────────────────────────────────────────
@@ -210,8 +210,8 @@ def load_test_data(args, label_classes: List[str], device: torch.device):
210
  """Load Zheng68K and reproduce the test split."""
211
  import anndata as ad
212
 
213
- sys.path.insert(0, str(PERFORMER_MODULE_ROOT / "cell_annotation"))
214
- from performer_module_finetune import DataPreprocessor
215
 
216
  preprocessor = DataPreprocessor(
217
  gene2ens_file=args.gene2ens_file,
@@ -276,7 +276,7 @@ def compute_importance_post_encoder(
276
  if model.head_type == "cls":
277
  module_tokens = latent[:, 1:, :]
278
  else:
279
- module_tokens = model._restore_module_order(latent, ids_restore)
280
 
281
  baseline_logits = _classify_from_tokens(model, module_tokens)
282
  y_tensor = torch.from_numpy(y_batch).to(device)
@@ -340,7 +340,7 @@ def _classify_from_tokens(model: nn.Module, module_tokens: torch.Tensor) -> torc
340
  elif model.head_type == "attn_pool":
341
  cell_emb, _ = model.pooler(module_tokens)
342
  elif model.head_type == "gene_mean":
343
- gene_tokens = model._module_to_gene(module_tokens)
344
  cell_emb = gene_tokens.mean(dim=1)
345
  else:
346
  raise ValueError(f"Unsupported head_type: {model.head_type}")
@@ -533,12 +533,12 @@ def run_enrichment(
533
 
534
  # ─── Module Gene Mapping ─────────────────────────────────────────────────────
535
 
536
- def build_module_gene_mapping(
537
  module_data_dir: str,
538
  gene_list_path: str,
539
  ) -> Tuple[List[str], Dict[str, List[str]]]:
540
  """Return (module_names, {module_name: [gene_symbols]})."""
541
- merged_path = Path(module_data_dir) / "merged_modules.json"
542
  with open(merged_path, "r") as f:
543
  merged = json.load(f)
544
 
@@ -582,7 +582,7 @@ def plot_importance_heatmap(
582
  logger.info("Saved heatmap: %s", output_path)
583
 
584
 
585
- def plot_top_modules_bar(
586
  importance: np.ndarray,
587
  label_classes: List[str],
588
  module_names: List[str],
@@ -628,8 +628,8 @@ def parse_args():
628
  help="Path to best_model.pt from cell annotation finetuning")
629
  p.add_argument("--config_pkl", required=True,
630
  help="Path to pretrained model .pkl config")
631
- p.add_argument("--module_data_dir", required=True,
632
- help="Directory containing merged_modules.json")
633
  p.add_argument("--gene_list_path", required=True,
634
  help="Path to selected_genes_27k.txt")
635
  p.add_argument("--gene_mapping_path", required=True)
@@ -677,8 +677,8 @@ def main():
677
  setattr(args, attr, str(Path(val).resolve()))
678
 
679
  logger.info("Loading finetuned model...")
680
- model, module_data, label_classes, head_type = load_finetuned_model(args, device)
681
- num_modules = len(module_data)
682
  num_classes = len(label_classes)
683
 
684
  logger.info("Loading test data...")
@@ -752,7 +752,7 @@ def main():
752
  # Only rank 0 does enrichment, plotting, saving
753
  if rank == 0:
754
  logger.info("Building module-gene mapping...")
755
- module_names, module_gene_symbols = build_module_gene_mapping(
756
  args.module_data_dir, args.gene_list_path
757
  )
758
 
@@ -810,7 +810,7 @@ def main():
810
  plot_importance_heatmap(
811
  importance, label_classes, output_dir / "heatmap_importance.png"
812
  )
813
- plot_top_modules_bar(
814
  importance, label_classes, module_names, output_dir
815
  )
816
 
 
33
 
34
  SCRIPT_DIR = Path(__file__).resolve().parent
35
  INTERP_DIR = SCRIPT_DIR.parent
36
+ BIOMOD_ROOT = INTERP_DIR.parent
37
+ SCLONG_ROOT = BIOMOD_ROOT.parent
38
 
39
+ for p in (str(BIOMOD_ROOT), str(SCLONG_ROOT)):
40
  if p not in sys.path:
41
  sys.path.insert(0, p)
42
 
 
89
 
90
  def load_finetuned_model(args, device: torch.device):
91
  """Load the full finetuned classifier (backbone + pooler + head)."""
92
+ from models_biomod import BioMod
93
+ from biomod_dataset import load_biomod_data
94
 
95
  ckpt_path = Path(args.finetune_checkpoint)
96
  checkpoint = torch.load(ckpt_path, map_location=device)
 
106
  with open(config_pkl_path, "rb") as f:
107
  config_pkl = pickle.load(f)
108
 
109
+ biomod_data = None
110
  if isinstance(config_pkl, dict) and "module_sets" in config_pkl:
111
+ biomod_data = config_pkl["module_sets"]
112
  elif "module_sets" in backbone_config:
113
+ biomod_data = backbone_config["module_sets"]
114
  else:
115
+ biomod_data = load_biomod_data(
116
  module_data_dir=args.module_data_dir,
117
  gene_list_path=args.gene_list_path,
118
  gene_mapping_path=args.gene_mapping_path,
 
141
  dec_heads = backbone_config.get("dec_heads", 8)
142
  dec_dim_head = backbone_config.get("dec_dim_head", 64)
143
 
144
+ backbone = BioMod(
145
+ module_sets=biomod_data,
146
  num_genes=num_genes,
147
+ max_seq_len=len(biomod_data),
148
  base_dim=base_dim,
149
  enc_depth=enc_depth,
150
  enc_heads=enc_heads,
 
171
  dec_dim_head=dec_dim_head,
172
  )
173
 
174
+ sys.path.insert(0, str(BIOMOD_ROOT / "cell_annotation"))
175
+ from biomod_finetune import (
176
  GatedAttentionPool,
177
+ BioModCellClassifier,
178
  ScGPTClsDecoder,
179
  )
180
 
 
183
  pool_hidden_dim = saved_args.get("pool_hidden_dim", 0)
184
  cls_decoder_layers = saved_args.get("cls_decoder_layers", 3)
185
 
186
+ model = BioModCellClassifier(
187
  backbone=backbone,
188
+ module_sets=biomod_data,
189
  num_classes=num_classes,
190
  head_type=head_type,
191
  pool_hidden_dim=pool_hidden_dim,
 
199
 
200
  logger.info(
201
  "Loaded finetuned model: head=%s, classes=%d, modules=%d",
202
+ head_type, num_classes, len(biomod_data),
203
  )
204
+ return model, biomod_data, label_classes, head_type
205
 
206
 
207
  # ─── Data Loading ───────────────────────────────────────────────────────────
 
210
  """Load Zheng68K and reproduce the test split."""
211
  import anndata as ad
212
 
213
+ sys.path.insert(0, str(BIOMOD_ROOT / "cell_annotation"))
214
+ from biomod_finetune import DataPreprocessor
215
 
216
  preprocessor = DataPreprocessor(
217
  gene2ens_file=args.gene2ens_file,
 
276
  if model.head_type == "cls":
277
  module_tokens = latent[:, 1:, :]
278
  else:
279
+ module_tokens = model._restore_biomod_order(latent, ids_restore)
280
 
281
  baseline_logits = _classify_from_tokens(model, module_tokens)
282
  y_tensor = torch.from_numpy(y_batch).to(device)
 
340
  elif model.head_type == "attn_pool":
341
  cell_emb, _ = model.pooler(module_tokens)
342
  elif model.head_type == "gene_mean":
343
+ gene_tokens = model._biomod_to_gene(module_tokens)
344
  cell_emb = gene_tokens.mean(dim=1)
345
  else:
346
  raise ValueError(f"Unsupported head_type: {model.head_type}")
 
533
 
534
  # ─── Module Gene Mapping ─────────────────────────────────────────────────────
535
 
536
+ def build_biomod_gene_mapping(
537
  module_data_dir: str,
538
  gene_list_path: str,
539
  ) -> Tuple[List[str], Dict[str, List[str]]]:
540
  """Return (module_names, {module_name: [gene_symbols]})."""
541
+ merged_path = Path(module_data_dir) / "merged_biomods.json"
542
  with open(merged_path, "r") as f:
543
  merged = json.load(f)
544
 
 
582
  logger.info("Saved heatmap: %s", output_path)
583
 
584
 
585
+ def plot_top_biomods_bar(
586
  importance: np.ndarray,
587
  label_classes: List[str],
588
  module_names: List[str],
 
628
  help="Path to best_model.pt from cell annotation finetuning")
629
  p.add_argument("--config_pkl", required=True,
630
  help="Path to pretrained model .pkl config")
631
+ p.add_argument("--biomod_data_dir", "--module_data_dir", required=True,
632
+ help="Directory containing merged_biomods.json", dest='module_data_dir')
633
  p.add_argument("--gene_list_path", required=True,
634
  help="Path to selected_genes_27k.txt")
635
  p.add_argument("--gene_mapping_path", required=True)
 
677
  setattr(args, attr, str(Path(val).resolve()))
678
 
679
  logger.info("Loading finetuned model...")
680
+ model, biomod_data, label_classes, head_type = load_finetuned_model(args, device)
681
+ num_modules = len(biomod_data)
682
  num_classes = len(label_classes)
683
 
684
  logger.info("Loading test data...")
 
752
  # Only rank 0 does enrichment, plotting, saving
753
  if rank == 0:
754
  logger.info("Building module-gene mapping...")
755
+ module_names, module_gene_symbols = build_biomod_gene_mapping(
756
  args.module_data_dir, args.gene_list_path
757
  )
758
 
 
810
  plot_importance_heatmap(
811
  importance, label_classes, output_dir / "heatmap_importance.png"
812
  )
813
+ plot_top_biomods_bar(
814
  importance, label_classes, module_names, output_dir
815
  )
816
 
BioMod/interpretability/{module_importance/module_importance_publication.py → biomod_importance/biomod_importance_publication.py} RENAMED
@@ -31,9 +31,9 @@
31
  from pub_utils import (
32
  CELL_TYPE_COLORS,
33
  MODULE_FAMILY_COLORS,
34
- get_module_families,
35
  load_importance_results,
36
- load_module_gene_mapping,
37
  savefig,
38
  setup_publication_style,
39
  )
@@ -204,7 +204,7 @@ def plot_importance_heatmap_clustered(results_dir, module_data_dir, output_dir):
204
  label_classes = data["label_classes"]
205
  module_names = data["module_names"]
206
 
207
- families = get_module_families(module_names)
208
  family_colors = [MODULE_FAMILY_COLORS.get(f, "#CCCCCC") for f in families]
209
 
210
  # Cluster modules
@@ -329,8 +329,8 @@ def parse_args():
329
  help="Ablation results directory")
330
  p.add_argument("--permutation_dir",
331
  help="Permutation results directory")
332
- p.add_argument("--module_data_dir", required=True,
333
- help="Module data directory (merged_modules.json)")
334
  p.add_argument("--output_dir", required=True)
335
  return p.parse_args()
336
 
 
31
  from pub_utils import (
32
  CELL_TYPE_COLORS,
33
  MODULE_FAMILY_COLORS,
34
+ get_biomod_families,
35
  load_importance_results,
36
+ load_biomod_gene_mapping,
37
  savefig,
38
  setup_publication_style,
39
  )
 
204
  label_classes = data["label_classes"]
205
  module_names = data["module_names"]
206
 
207
+ families = get_biomod_families(module_names)
208
  family_colors = [MODULE_FAMILY_COLORS.get(f, "#CCCCCC") for f in families]
209
 
210
  # Cluster modules
 
329
  help="Ablation results directory")
330
  p.add_argument("--permutation_dir",
331
  help="Permutation results directory")
332
+ p.add_argument("--biomod_data_dir", "--module_data_dir", required=True,
333
+ help="Module data directory (merged_biomods.json)", dest='module_data_dir')
334
  p.add_argument("--output_dir", required=True)
335
  return p.parse_args()
336
 
BioMod/interpretability/{module_importance → biomod_importance}/cross_class_similarity.py RENAMED
File without changes
BioMod/interpretability/{module_importance → biomod_importance}/family_ablation.py RENAMED
@@ -29,10 +29,10 @@
29
 
30
  SCRIPT_DIR = Path(__file__).resolve().parent
31
  INTERP_DIR = SCRIPT_DIR.parent
32
- PERFORMER_MODULE_ROOT = INTERP_DIR.parent
33
- SCLONG_ROOT = PERFORMER_MODULE_ROOT.parent
34
 
35
- for p in (str(SCRIPT_DIR), str(PERFORMER_MODULE_ROOT), str(SCLONG_ROOT)):
36
  if p not in sys.path:
37
  sys.path.insert(0, p)
38
 
@@ -40,10 +40,10 @@
40
  compute_masked_accuracy_post_encoder,
41
  encode_all_cells,
42
  )
43
- from module_importance import (
44
  CellDataset,
45
  _classify_from_tokens,
46
- build_module_gene_mapping,
47
  load_finetuned_model,
48
  load_test_data,
49
  )
@@ -296,7 +296,7 @@ def parse_args():
296
  p = argparse.ArgumentParser(description="Family-level ablation experiment")
297
  p.add_argument("--finetune_checkpoint", required=True)
298
  p.add_argument("--config_pkl", required=True)
299
- p.add_argument("--module_data_dir", required=True)
300
  p.add_argument("--gene_list_path", required=True)
301
  p.add_argument("--gene_mapping_path", required=True)
302
  p.add_argument("--gene2vec_data_path", required=True)
@@ -329,8 +329,8 @@ def main():
329
  setattr(args, attr, str(Path(val).resolve()))
330
 
331
  logger.info("Loading model...")
332
- model, module_data, label_classes, head_type = load_finetuned_model(args, device)
333
- module_names, _ = build_module_gene_mapping(args.module_data_dir, args.gene_list_path)
334
  family_indices = get_family_indices(module_names)
335
  for fam in FAMILY_ORDER:
336
  logger.info(" %s: %d modules", fam, len(family_indices[fam]))
 
29
 
30
  SCRIPT_DIR = Path(__file__).resolve().parent
31
  INTERP_DIR = SCRIPT_DIR.parent
32
+ BIOMOD_ROOT = INTERP_DIR.parent
33
+ SCLONG_ROOT = BIOMOD_ROOT.parent
34
 
35
+ for p in (str(SCRIPT_DIR), str(BIOMOD_ROOT), str(SCLONG_ROOT)):
36
  if p not in sys.path:
37
  sys.path.insert(0, p)
38
 
 
40
  compute_masked_accuracy_post_encoder,
41
  encode_all_cells,
42
  )
43
+ from biomod_importance import (
44
  CellDataset,
45
  _classify_from_tokens,
46
+ build_biomod_gene_mapping,
47
  load_finetuned_model,
48
  load_test_data,
49
  )
 
296
  p = argparse.ArgumentParser(description="Family-level ablation experiment")
297
  p.add_argument("--finetune_checkpoint", required=True)
298
  p.add_argument("--config_pkl", required=True)
299
+ p.add_argument("--biomod_data_dir", "--module_data_dir", required=True, dest='module_data_dir')
300
  p.add_argument("--gene_list_path", required=True)
301
  p.add_argument("--gene_mapping_path", required=True)
302
  p.add_argument("--gene2vec_data_path", required=True)
 
329
  setattr(args, attr, str(Path(val).resolve()))
330
 
331
  logger.info("Loading model...")
332
+ model, biomod_data, label_classes, head_type = load_finetuned_model(args, device)
333
+ module_names, _ = build_biomod_gene_mapping(args.module_data_dir, args.gene_list_path)
334
  family_indices = get_family_indices(module_names)
335
  for fam in FAMILY_ORDER:
336
  logger.info(" %s: %d modules", fam, len(family_indices[fam]))
BioMod/interpretability/{module_importance → biomod_importance}/family_importance_analysis.py RENAMED
File without changes
BioMod/interpretability/{module_importance → biomod_importance}/marker_gene_verification.py RENAMED
@@ -28,11 +28,11 @@
28
  from pub_utils import (
29
  CANONICAL_MARKERS,
30
  CELL_TYPE_COLORS,
31
- find_gene_modules,
32
- get_module_family,
33
  importance_rank_percentile,
34
  load_importance_results,
35
- load_module_gene_mapping,
36
  savefig,
37
  setup_publication_style,
38
  )
@@ -55,7 +55,7 @@ def compute_marker_ranks(
55
  ranked_indices = np.argsort(-imp_row)
56
 
57
  for marker in markers:
58
- containing_modules = find_gene_modules(marker, module_gene_symbols)
59
  if not containing_modules:
60
  rows.append({
61
  "cell_type": cls_name,
@@ -89,7 +89,7 @@ def compute_marker_ranks(
89
  "found_in_modules": True,
90
  "num_modules_containing": len(containing_modules),
91
  "best_module": module_names[best_pi],
92
- "best_module_family": get_module_family(module_names[best_pi]),
93
  "best_rank": best_rank + 1,
94
  "rank_percentile": 100.0 * best_rank / num_modules,
95
  "best_importance": float(imp_row[best_pi]),
@@ -252,8 +252,8 @@ def parse_args():
252
  help="Path to importance_matrix.npy")
253
  p.add_argument("--importance_csv", required=True,
254
  help="Path to importance_matrix.csv (for labels)")
255
- p.add_argument("--module_data_dir", required=True,
256
- help="Directory containing merged_modules.json")
257
  p.add_argument("--output_dir", required=True)
258
  return p.parse_args()
259
 
@@ -268,7 +268,7 @@ def main():
268
  label_classes = list(imp_csv.index)
269
  module_names = list(imp_csv.columns)
270
 
271
- _, module_gene_symbols = load_module_gene_mapping(args.module_data_dir)
272
 
273
  print(f"Importance matrix: {importance.shape}")
274
  print(f"Cell types: {len(label_classes)}")
 
28
  from pub_utils import (
29
  CANONICAL_MARKERS,
30
  CELL_TYPE_COLORS,
31
+ find_gene_biomods,
32
+ get_biomod_family,
33
  importance_rank_percentile,
34
  load_importance_results,
35
+ load_biomod_gene_mapping,
36
  savefig,
37
  setup_publication_style,
38
  )
 
55
  ranked_indices = np.argsort(-imp_row)
56
 
57
  for marker in markers:
58
+ containing_modules = find_gene_biomods(marker, module_gene_symbols)
59
  if not containing_modules:
60
  rows.append({
61
  "cell_type": cls_name,
 
89
  "found_in_modules": True,
90
  "num_modules_containing": len(containing_modules),
91
  "best_module": module_names[best_pi],
92
+ "best_module_family": get_biomod_family(module_names[best_pi]),
93
  "best_rank": best_rank + 1,
94
  "rank_percentile": 100.0 * best_rank / num_modules,
95
  "best_importance": float(imp_row[best_pi]),
 
252
  help="Path to importance_matrix.npy")
253
  p.add_argument("--importance_csv", required=True,
254
  help="Path to importance_matrix.csv (for labels)")
255
+ p.add_argument("--biomod_data_dir", "--module_data_dir", required=True,
256
+ help="Directory containing merged_biomods.json", dest='module_data_dir')
257
  p.add_argument("--output_dir", required=True)
258
  return p.parse_args()
259
 
 
268
  label_classes = list(imp_csv.index)
269
  module_names = list(imp_csv.columns)
270
 
271
+ _, module_gene_symbols = load_biomod_gene_mapping(args.module_data_dir)
272
 
273
  print(f"Importance matrix: {importance.shape}")
274
  print(f"Cell types: {len(label_classes)}")
BioMod/interpretability/{module_importance → biomod_importance}/masking_ablation.py RENAMED
@@ -29,14 +29,14 @@
29
 
30
  SCRIPT_DIR = Path(__file__).resolve().parent
31
  INTERP_DIR = SCRIPT_DIR.parent
32
- PERFORMER_MODULE_ROOT = INTERP_DIR.parent
33
- SCLONG_ROOT = PERFORMER_MODULE_ROOT.parent
34
 
35
- for p in (str(SCRIPT_DIR), str(PERFORMER_MODULE_ROOT), str(SCLONG_ROOT)):
36
  if p not in sys.path:
37
  sys.path.insert(0, p)
38
 
39
- from module_importance import (
40
  CellDataset,
41
  _classify_from_tokens,
42
  load_finetuned_model,
@@ -63,7 +63,7 @@ def encode_all_cells(model, dataloader, preprocessor, reindex_state, device):
63
  if model.head_type == "cls":
64
  module_tokens = latent[:, 1:, :]
65
  else:
66
- module_tokens = model._restore_module_order(latent, ids_restore)
67
  all_tokens.append(module_tokens.cpu())
68
  all_labels.append(y_batch.numpy())
69
 
@@ -248,7 +248,7 @@ def parse_args():
248
  p = argparse.ArgumentParser(description="Masking ablation curves")
249
  p.add_argument("--finetune_checkpoint", required=True)
250
  p.add_argument("--config_pkl", required=True)
251
- p.add_argument("--module_data_dir", required=True)
252
  p.add_argument("--gene_list_path", required=True)
253
  p.add_argument("--gene_mapping_path", required=True)
254
  p.add_argument("--gene2vec_data_path", required=True)
@@ -290,8 +290,8 @@ def main():
290
  setattr(args, attr, str(Path(val).resolve()))
291
 
292
  logger.info("Loading model...")
293
- model, module_data, label_classes, head_type = load_finetuned_model(args, device)
294
- num_modules = len(module_data)
295
 
296
  logger.info("Loading test data...")
297
  X_test, y_test, le, preprocessor, reindex_state = load_test_data(
 
29
 
30
  SCRIPT_DIR = Path(__file__).resolve().parent
31
  INTERP_DIR = SCRIPT_DIR.parent
32
+ BIOMOD_ROOT = INTERP_DIR.parent
33
+ SCLONG_ROOT = BIOMOD_ROOT.parent
34
 
35
+ for p in (str(SCRIPT_DIR), str(BIOMOD_ROOT), str(SCLONG_ROOT)):
36
  if p not in sys.path:
37
  sys.path.insert(0, p)
38
 
39
+ from biomod_importance import (
40
  CellDataset,
41
  _classify_from_tokens,
42
  load_finetuned_model,
 
63
  if model.head_type == "cls":
64
  module_tokens = latent[:, 1:, :]
65
  else:
66
+ module_tokens = model._restore_biomod_order(latent, ids_restore)
67
  all_tokens.append(module_tokens.cpu())
68
  all_labels.append(y_batch.numpy())
69
 
 
248
  p = argparse.ArgumentParser(description="Masking ablation curves")
249
  p.add_argument("--finetune_checkpoint", required=True)
250
  p.add_argument("--config_pkl", required=True)
251
+ p.add_argument("--biomod_data_dir", "--module_data_dir", required=True, dest='module_data_dir')
252
  p.add_argument("--gene_list_path", required=True)
253
  p.add_argument("--gene_mapping_path", required=True)
254
  p.add_argument("--gene2vec_data_path", required=True)
 
290
  setattr(args, attr, str(Path(val).resolve()))
291
 
292
  logger.info("Loading model...")
293
+ model, biomod_data, label_classes, head_type = load_finetuned_model(args, device)
294
+ num_modules = len(biomod_data)
295
 
296
  logger.info("Loading test data...")
297
  X_test, y_test, le, preprocessor, reindex_state = load_test_data(
BioMod/interpretability/{module_importance → biomod_importance}/pub_utils.py RENAMED
@@ -140,15 +140,15 @@ def load_importance_results(results_dir) -> dict:
140
  return data
141
 
142
 
143
- def load_module_gene_mapping(module_data_dir) -> Tuple[List[str], Dict[str, List[str]]]:
144
- """Load merged_modules.json and return (module_names, module_gene_symbols).
145
 
146
  Returns:
147
  module_names: ordered list of module names
148
  module_gene_symbols: dict mapping module_name -> list of gene symbols
149
  """
150
  module_data_dir = Path(module_data_dir)
151
- with open(module_data_dir / "merged_modules.json") as f:
152
  modules = json.load(f)
153
  module_names = list(modules.keys())
154
  module_gene_symbols = {}
@@ -160,7 +160,7 @@ def load_module_gene_mapping(module_data_dir) -> Tuple[List[str], Dict[str, List
160
  return module_names, module_gene_symbols
161
 
162
 
163
- def get_module_family(module_name: str) -> str:
164
  """Determine which family a module belongs to based on its name prefix."""
165
  if module_name.startswith("module1_hallmarks"):
166
  return "Hallmarks"
@@ -173,14 +173,14 @@ def get_module_family(module_name: str) -> str:
173
  return "Unknown"
174
 
175
 
176
- def get_module_families(module_names: List[str]) -> List[str]:
177
  """Get family labels for a list of module names."""
178
- return [get_module_family(p) for p in module_names]
179
 
180
 
181
  # ─── Analysis Helpers ────────────────────────────────────────────────────────
182
 
183
- def find_gene_modules(
184
  gene_symbol: str, module_gene_symbols: Dict[str, List[str]]
185
  ) -> List[str]:
186
  """Find all modules containing a given gene symbol."""
 
140
  return data
141
 
142
 
143
+ def load_biomod_gene_mapping(module_data_dir) -> Tuple[List[str], Dict[str, List[str]]]:
144
+ """Load merged_biomods.json and return (module_names, module_gene_symbols).
145
 
146
  Returns:
147
  module_names: ordered list of module names
148
  module_gene_symbols: dict mapping module_name -> list of gene symbols
149
  """
150
  module_data_dir = Path(module_data_dir)
151
+ with open(module_data_dir / "merged_biomods.json") as f:
152
  modules = json.load(f)
153
  module_names = list(modules.keys())
154
  module_gene_symbols = {}
 
160
  return module_names, module_gene_symbols
161
 
162
 
163
+ def get_biomod_family(module_name: str) -> str:
164
  """Determine which family a module belongs to based on its name prefix."""
165
  if module_name.startswith("module1_hallmarks"):
166
  return "Hallmarks"
 
173
  return "Unknown"
174
 
175
 
176
+ def get_biomod_families(module_names: List[str]) -> List[str]:
177
  """Get family labels for a list of module names."""
178
+ return [get_biomod_family(p) for p in module_names]
179
 
180
 
181
  # ─── Analysis Helpers ────────────────────────────────────────────────────────
182
 
183
+ def find_gene_biomods(
184
  gene_symbol: str, module_gene_symbols: Dict[str, List[str]]
185
  ) -> List[str]:
186
  """Find all modules containing a given gene symbol."""
BioMod/interpretability/{module_importance → biomod_importance}/random_baseline.py RENAMED
@@ -27,14 +27,14 @@
27
 
28
  SCRIPT_DIR = Path(__file__).resolve().parent
29
  INTERP_DIR = SCRIPT_DIR.parent
30
- PERFORMER_MODULE_ROOT = INTERP_DIR.parent
31
- SCLONG_ROOT = PERFORMER_MODULE_ROOT.parent
32
 
33
- for p in (str(SCRIPT_DIR), str(PERFORMER_MODULE_ROOT), str(SCLONG_ROOT)):
34
  if p not in sys.path:
35
  sys.path.insert(0, p)
36
 
37
- from module_importance import (
38
  CellDataset,
39
  _classify_from_tokens,
40
  load_finetuned_model,
@@ -48,7 +48,7 @@
48
  # ─── Mode A: Shuffle Modules (GPU) ──────────────────────────────────────────
49
 
50
  @torch.no_grad()
51
- def compute_importance_shuffled_modules(
52
  model, dataloader, preprocessor, reindex_state,
53
  num_modules, num_classes, device, seed,
54
  ):
@@ -77,7 +77,7 @@ def compute_importance_shuffled_modules(
77
  if model.head_type == "cls":
78
  module_tokens = latent[:, 1:, :]
79
  else:
80
- module_tokens = model._restore_module_order(latent, ids_restore)
81
 
82
  # Permute module token order (breaks module identity)
83
  module_tokens_shuffled = module_tokens[:, perm, :]
@@ -206,7 +206,7 @@ def parse_args():
206
  # Model args (for shuffle_modules mode)
207
  p.add_argument("--finetune_checkpoint")
208
  p.add_argument("--config_pkl")
209
- p.add_argument("--module_data_dir")
210
  p.add_argument("--gene_list_path")
211
  p.add_argument("--gene_mapping_path")
212
  p.add_argument("--gene2vec_data_path")
@@ -256,7 +256,7 @@ def main():
256
  setattr(args, attr, str(Path(val).resolve()))
257
 
258
  logger.info("Loading model for shuffle_modules mode...")
259
- model, module_data, label_classes, _ = load_finetuned_model(args, device)
260
 
261
  logger.info("Loading test data...")
262
  X_test, y_test, le, preprocessor, reindex_state = load_test_data(
@@ -273,7 +273,7 @@ def main():
273
  )
274
  for pi in range(n_perms_a):
275
  logger.info(" Permutation %d/%d...", pi + 1, n_perms_a)
276
- null_module_shuffle[pi] = compute_importance_shuffled_modules(
277
  model, dataloader, preprocessor, reindex_state,
278
  num_modules, num_classes, device,
279
  seed=args.seed + pi + 1000,
 
27
 
28
  SCRIPT_DIR = Path(__file__).resolve().parent
29
  INTERP_DIR = SCRIPT_DIR.parent
30
+ BIOMOD_ROOT = INTERP_DIR.parent
31
+ SCLONG_ROOT = BIOMOD_ROOT.parent
32
 
33
+ for p in (str(SCRIPT_DIR), str(BIOMOD_ROOT), str(SCLONG_ROOT)):
34
  if p not in sys.path:
35
  sys.path.insert(0, p)
36
 
37
+ from biomod_importance import (
38
  CellDataset,
39
  _classify_from_tokens,
40
  load_finetuned_model,
 
48
  # ─── Mode A: Shuffle Modules (GPU) ──────────────────────────────────────────
49
 
50
  @torch.no_grad()
51
+ def compute_importance_shuffled_biomods(
52
  model, dataloader, preprocessor, reindex_state,
53
  num_modules, num_classes, device, seed,
54
  ):
 
77
  if model.head_type == "cls":
78
  module_tokens = latent[:, 1:, :]
79
  else:
80
+ module_tokens = model._restore_biomod_order(latent, ids_restore)
81
 
82
  # Permute module token order (breaks module identity)
83
  module_tokens_shuffled = module_tokens[:, perm, :]
 
206
  # Model args (for shuffle_modules mode)
207
  p.add_argument("--finetune_checkpoint")
208
  p.add_argument("--config_pkl")
209
+ p.add_argument("--biomod_data_dir", "--module_data_dir", dest='module_data_dir')
210
  p.add_argument("--gene_list_path")
211
  p.add_argument("--gene_mapping_path")
212
  p.add_argument("--gene2vec_data_path")
 
256
  setattr(args, attr, str(Path(val).resolve()))
257
 
258
  logger.info("Loading model for shuffle_modules mode...")
259
+ model, biomod_data, label_classes, _ = load_finetuned_model(args, device)
260
 
261
  logger.info("Loading test data...")
262
  X_test, y_test, le, preprocessor, reindex_state = load_test_data(
 
273
  )
274
  for pi in range(n_perms_a):
275
  logger.info(" Permutation %d/%d...", pi + 1, n_perms_a)
276
+ null_module_shuffle[pi] = compute_importance_shuffled_biomods(
277
  model, dataloader, preprocessor, reindex_state,
278
  num_modules, num_classes, device,
279
  seed=args.seed + pi + 1000,
BioMod/interpretability/{module_importance → biomod_importance}/tcell_reactome_analysis.py RENAMED
File without changes
BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/DESIGN.md RENAMED
@@ -11,15 +11,15 @@ gene names do not carry phrase-level semantics.
11
 
12
  | Script | Function |
13
  | --- | --- |
14
- | `module_text_alignment.py` | Main script: RSA/Mantel test, CKA, and leave-one-out Ridge retrieval |
15
- | `module_text_alignment_extended.py` | Extended analysis: Reactome broad-category RSA, Reactome description-based RSA, and Hallmark deep dive |
16
- | `module_text_alignment_publication.py` | Publication-level analysis: six main figures, bootstrap CIs, multi-model comparison, cross-dataset validation, and deep analysis |
17
  | `fetch_reactome_descriptions.py` | Fetch `summaryText` for 491 pathways from the Reactome API |
18
 
19
  ## Commands
20
 
21
  Run from the BioMod repository root. Generate each `module_embeddings.npy` with
22
- `BioMod/interpretability/module_geometry.py` before using it here. The historical
23
  `pretrain_subset` output requires a separately supplied pretraining dataset;
24
  the `pancreas_scib` example uses the bundled pancreas h5ad file. Existing result
25
  tables below describe historical runs; their raw output directories are not
@@ -41,24 +41,24 @@ pip install sentence-transformers -i https://pypi.tuna.tsinghua.edu.cn/simple
41
 
42
  ```bash
43
  # pretrain_subset is recommended because averaging over 50k cells is more stable.
44
- python BioMod/interpretability/module_text_alignment/module_text_alignment.py \
45
- --module_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
46
- --module_json model_dependencies/module_data_all/merged_modules.json \
47
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
48
  --n_permutations 1000 \
49
  --output_dir outputs/interpretability/text_alignment/pretrain_subset_v2
50
 
51
  # pancreas_scib validates cross-dataset consistency.
52
- python BioMod/interpretability/module_text_alignment/module_text_alignment.py \
53
- --module_embedding_path outputs/interpretability/geometry/pancreas_scib/module_embeddings.npy \
54
- --module_json model_dependencies/module_data_all/merged_modules.json \
55
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
56
  --n_permutations 1000 \
57
  --output_dir outputs/interpretability/text_alignment/pancreas_scib_v2
58
 
59
  # Quick mode: skip LOO retrieval and run only RSA + CKA.
60
- python BioMod/interpretability/module_text_alignment/module_text_alignment.py \
61
- --module_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
62
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
63
  --skip_loo \
64
  --output_dir outputs/interpretability/text_alignment/pretrain_subset_quick
@@ -68,24 +68,24 @@ python BioMod/interpretability/module_text_alignment/module_text_alignment.py \
68
 
69
  ```bash
70
  # Re-fetch if needed. Requires network access to reactome.org and takes about 2 minutes.
71
- python BioMod/interpretability/module_text_alignment/fetch_reactome_descriptions.py \
72
- --module_json model_dependencies/module_data_all/merged_modules.json \
73
  --output reactome_descriptions.json --delay 0.05
74
  ```
75
 
76
  The command writes refreshed descriptions to `reactome_descriptions.json` in the
77
  repository root. The analysis examples below use the bundled
78
- `BioMod/interpretability/module_text_alignment/reactome_descriptions.json` instead;
79
  set `--descriptions_file reactome_descriptions.json` to use the refreshed output.
80
 
81
  ### 4. Extended Analysis: Reactome Categories, Description-Based RSA, Hallmark Deep Dive
82
 
83
  ```bash
84
- python BioMod/interpretability/module_text_alignment/module_text_alignment_extended.py \
85
- --module_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
86
- --module_json model_dependencies/module_data_all/merged_modules.json \
87
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
88
- --descriptions_file BioMod/interpretability/module_text_alignment/reactome_descriptions.json \
89
  --n_permutations 1000 \
90
  --output_dir outputs/interpretability/text_alignment/pretrain_subset_v2_extended
91
  ```
@@ -94,23 +94,23 @@ python BioMod/interpretability/module_text_alignment/module_text_alignment_exten
94
 
95
  ```bash
96
  # Basic run: one dataset and one model.
97
- python BioMod/interpretability/module_text_alignment/module_text_alignment_publication.py \
98
- --module_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
99
- --module_json model_dependencies/module_data_all/merged_modules.json \
100
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
101
- --descriptions_file BioMod/interpretability/module_text_alignment/reactome_descriptions.json \
102
  --n_permutations 1000 \
103
  --n_bootstrap 2000 \
104
  --output_dir outputs/interpretability/text_alignment/publication
105
 
106
  # Full run: cross-dataset validation and multi-model comparison.
107
- python BioMod/interpretability/module_text_alignment/module_text_alignment_publication.py \
108
- --module_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
109
- --module_embedding_path2 outputs/interpretability/geometry/pancreas_scib/module_embeddings.npy \
110
- --module_json model_dependencies/module_data_all/merged_modules.json \
111
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
112
  --sentence_models_compare "sentence-transformers/all-mpnet-base-v2,sentence-transformers/all-MiniLM-L6-v2" \
113
- --descriptions_file BioMod/interpretability/module_text_alignment/reactome_descriptions.json \
114
  --n_permutations 1000 \
115
  --n_bootstrap 2000 \
116
  --output_dir outputs/interpretability/text_alignment/publication_full
@@ -120,8 +120,8 @@ python BioMod/interpretability/module_text_alignment/module_text_alignment_publi
120
 
121
  | Argument | Description |
122
  | --- | --- |
123
- | `--module_embedding_path` | `module_embeddings.npy` for the main dataset, from Experiment 1 |
124
- | `--module_embedding_path2` | Optional second dataset for cross-dataset validation |
125
  | `--sentence_models_compare` | Comma-separated model names for comparison, downloaded automatically from HuggingFace |
126
  | `--n_permutations` | Mantel-test permutations; 1000 is standard and 10000 gives more precise p-values |
127
  | `--n_bootstrap` | Bootstrap resampling iterations; 2000 is standard for 95% CIs |
@@ -135,7 +135,7 @@ pip install sentence-transformers -i https://pypi.tuna.tsinghua.edu.cn/simple
135
 
136
  ## Output Files
137
 
138
- ### Main Experiment Outputs (`module_text_alignment.py`)
139
 
140
  | File | Content |
141
  | --- | --- |
@@ -149,7 +149,7 @@ pip install sentence-transformers -i https://pypi.tuna.tsinghua.edu.cn/simple
149
  | `retrieval_examples.csv` | Top-5 retrieval results for representative pathways |
150
  | `run.log` | Full log |
151
 
152
- ### Extended Analysis Outputs (`module_text_alignment_extended.py`)
153
 
154
  | File | Content |
155
  | --- | --- |
@@ -159,7 +159,7 @@ pip install sentence-transformers -i https://pypi.tuna.tsinghua.edu.cn/simple
159
  | `hallmarks_per_module_alignment.csv` | Per-module alignment ranking for 50 Hallmarks |
160
  | `run_extended.log` | Extended-analysis log |
161
 
162
- ### Publication-Level Outputs (`module_text_alignment_publication.py`)
163
 
164
  | File | Content |
165
  | --- | --- |
 
11
 
12
  | Script | Function |
13
  | --- | --- |
14
+ | `biomod_text_alignment.py` | Main script: RSA/Mantel test, CKA, and leave-one-out Ridge retrieval |
15
+ | `biomod_text_alignment_extended.py` | Extended analysis: Reactome broad-category RSA, Reactome description-based RSA, and Hallmark deep dive |
16
+ | `biomod_text_alignment_publication.py` | Publication-level analysis: six main figures, bootstrap CIs, multi-model comparison, cross-dataset validation, and deep analysis |
17
  | `fetch_reactome_descriptions.py` | Fetch `summaryText` for 491 pathways from the Reactome API |
18
 
19
  ## Commands
20
 
21
  Run from the BioMod repository root. Generate each `module_embeddings.npy` with
22
+ `BioMod/interpretability/biomod_geometry.py` before using it here. The historical
23
  `pretrain_subset` output requires a separately supplied pretraining dataset;
24
  the `pancreas_scib` example uses the bundled pancreas h5ad file. Existing result
25
  tables below describe historical runs; their raw output directories are not
 
41
 
42
  ```bash
43
  # pretrain_subset is recommended because averaging over 50k cells is more stable.
44
+ python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment.py \
45
+ --biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
46
+ --biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
47
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
48
  --n_permutations 1000 \
49
  --output_dir outputs/interpretability/text_alignment/pretrain_subset_v2
50
 
51
  # pancreas_scib validates cross-dataset consistency.
52
+ python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment.py \
53
+ --biomod_embedding_path outputs/interpretability/geometry/pancreas_scib/module_embeddings.npy \
54
+ --biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
55
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
56
  --n_permutations 1000 \
57
  --output_dir outputs/interpretability/text_alignment/pancreas_scib_v2
58
 
59
  # Quick mode: skip LOO retrieval and run only RSA + CKA.
60
+ python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment.py \
61
+ --biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
62
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
63
  --skip_loo \
64
  --output_dir outputs/interpretability/text_alignment/pretrain_subset_quick
 
68
 
69
  ```bash
70
  # Re-fetch if needed. Requires network access to reactome.org and takes about 2 minutes.
71
+ python BioMod/interpretability/biomod_text_alignment/fetch_reactome_descriptions.py \
72
+ --biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
73
  --output reactome_descriptions.json --delay 0.05
74
  ```
75
 
76
  The command writes refreshed descriptions to `reactome_descriptions.json` in the
77
  repository root. The analysis examples below use the bundled
78
+ `BioMod/interpretability/biomod_text_alignment/reactome_descriptions.json` instead;
79
  set `--descriptions_file reactome_descriptions.json` to use the refreshed output.
80
 
81
  ### 4. Extended Analysis: Reactome Categories, Description-Based RSA, Hallmark Deep Dive
82
 
83
  ```bash
84
+ python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment_extended.py \
85
+ --biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
86
+ --biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
87
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
88
+ --descriptions_file BioMod/interpretability/biomod_text_alignment/reactome_descriptions.json \
89
  --n_permutations 1000 \
90
  --output_dir outputs/interpretability/text_alignment/pretrain_subset_v2_extended
91
  ```
 
94
 
95
  ```bash
96
  # Basic run: one dataset and one model.
97
+ python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment_publication.py \
98
+ --biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
99
+ --biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
100
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
101
+ --descriptions_file BioMod/interpretability/biomod_text_alignment/reactome_descriptions.json \
102
  --n_permutations 1000 \
103
  --n_bootstrap 2000 \
104
  --output_dir outputs/interpretability/text_alignment/publication
105
 
106
  # Full run: cross-dataset validation and multi-model comparison.
107
+ python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment_publication.py \
108
+ --biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
109
+ --biomod_embedding_path2 outputs/interpretability/geometry/pancreas_scib/module_embeddings.npy \
110
+ --biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
111
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
112
  --sentence_models_compare "sentence-transformers/all-mpnet-base-v2,sentence-transformers/all-MiniLM-L6-v2" \
113
+ --descriptions_file BioMod/interpretability/biomod_text_alignment/reactome_descriptions.json \
114
  --n_permutations 1000 \
115
  --n_bootstrap 2000 \
116
  --output_dir outputs/interpretability/text_alignment/publication_full
 
120
 
121
  | Argument | Description |
122
  | --- | --- |
123
+ | `--biomod_embedding_path` | `module_embeddings.npy` for the main dataset, from Experiment 1 |
124
+ | `--biomod_embedding_path2` | Optional second dataset for cross-dataset validation |
125
  | `--sentence_models_compare` | Comma-separated model names for comparison, downloaded automatically from HuggingFace |
126
  | `--n_permutations` | Mantel-test permutations; 1000 is standard and 10000 gives more precise p-values |
127
  | `--n_bootstrap` | Bootstrap resampling iterations; 2000 is standard for 95% CIs |
 
135
 
136
  ## Output Files
137
 
138
+ ### Main Experiment Outputs (`biomod_text_alignment.py`)
139
 
140
  | File | Content |
141
  | --- | --- |
 
149
  | `retrieval_examples.csv` | Top-5 retrieval results for representative pathways |
150
  | `run.log` | Full log |
151
 
152
+ ### Extended Analysis Outputs (`biomod_text_alignment_extended.py`)
153
 
154
  | File | Content |
155
  | --- | --- |
 
159
  | `hallmarks_per_module_alignment.csv` | Per-module alignment ranking for 50 Hallmarks |
160
  | `run_extended.log` | Extended-analysis log |
161
 
162
+ ### Publication-Level Outputs (`biomod_text_alignment_publication.py`)
163
 
164
  | File | Content |
165
  | --- | --- |
BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/MANUSCRIPT_SECTION.md RENAMED
File without changes
BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/__init__.py RENAMED
File without changes
BioMod/interpretability/{module_text_alignment/module_text_alignment.py → biomod_text_alignment/biomod_text_alignment.py} RENAMED
@@ -10,7 +10,7 @@
10
 
11
  Pipeline
12
  --------
13
- 1. Parse pathway names from merged_modules.json keys for Hallmarks (50) and
14
  Reactome (491) modules. Hotspot/Processed are excluded (no semantic names).
15
  2. Encode pathway names with a biomedical sentence encoder.
16
  3. Load cell-averaged module embeddings from experiment 1 output.
@@ -58,16 +58,16 @@
58
 
59
  HERE = Path(__file__).resolve().parent
60
  INTERP_DIR = HERE.parent
61
- PERFORMER_MODULE = INTERP_DIR.parent
62
- REPO_ROOT = PERFORMER_MODULE.parent
63
- for p in (str(INTERP_DIR), str(PERFORMER_MODULE), str(REPO_ROOT)):
64
  if p not in sys.path:
65
  sys.path.insert(0, p)
66
 
67
  from interpretability.utils import (
68
  MODULE_FAMILY_MAP,
69
- load_module_names,
70
- parse_module_family,
71
  )
72
 
73
  logger = logging.getLogger(__name__)
@@ -79,7 +79,7 @@
79
 
80
  def parse_pathway_text(module_name: str) -> Optional[str]:
81
  """Extract a human-readable pathway name from a module key."""
82
- family = parse_module_family(module_name)
83
  if family == "Hallmarks":
84
  raw = module_name.split("__", 1)[1]
85
  raw = raw.replace("HALLMARK_", "", 1)
@@ -218,7 +218,7 @@ def compute_rsa_by_family(
218
  ) -> Dict[str, Dict[str, float]]:
219
  """RSA computed separately for each module family (Hallmarks vs Reactome)."""
220
  results = {}
221
- families = [parse_module_family(n) for n in names]
222
  for fam in ["Hallmarks", "Reactome"]:
223
  mask = np.array([f == fam for f in families])
224
  if mask.sum() < 10:
@@ -447,11 +447,11 @@ def parse_args() -> argparse.Namespace:
447
  description=__doc__,
448
  formatter_class=argparse.RawDescriptionHelpFormatter,
449
  )
450
- parser.add_argument("--module_embedding_path", type=str, required=True,
451
- help="Path to module_embeddings.npy from experiment 1.")
452
- parser.add_argument("--module_json", type=str,
453
- default=str(REPO_ROOT / "model_dependencies" / "module_data_all" / "merged_modules.json"),
454
- help="Path to merged_modules.json.")
455
  parser.add_argument("--descriptions_file", type=str, default=None,
456
  help="Optional JSON with extended pathway descriptions.")
457
  parser.add_argument("--sentence_model", type=str,
@@ -487,7 +487,7 @@ def main():
487
 
488
  # --- Load module names and embeddings ---
489
  logger.info("Loading module names from %s", args.module_json)
490
- module_names = load_module_names(args.module_json)
491
  logger.info("Total modules: %d", len(module_names))
492
 
493
  logger.info("Loading module embeddings from %s", args.module_embedding_path)
 
10
 
11
  Pipeline
12
  --------
13
+ 1. Parse pathway names from merged_biomods.json keys for Hallmarks (50) and
14
  Reactome (491) modules. Hotspot/Processed are excluded (no semantic names).
15
  2. Encode pathway names with a biomedical sentence encoder.
16
  3. Load cell-averaged module embeddings from experiment 1 output.
 
58
 
59
  HERE = Path(__file__).resolve().parent
60
  INTERP_DIR = HERE.parent
61
+ BIOMOD_SOURCE = INTERP_DIR.parent
62
+ REPO_ROOT = BIOMOD_SOURCE.parent
63
+ for p in (str(INTERP_DIR), str(BIOMOD_SOURCE), str(REPO_ROOT)):
64
  if p not in sys.path:
65
  sys.path.insert(0, p)
66
 
67
  from interpretability.utils import (
68
  MODULE_FAMILY_MAP,
69
+ load_biomod_names,
70
+ parse_biomod_family,
71
  )
72
 
73
  logger = logging.getLogger(__name__)
 
79
 
80
  def parse_pathway_text(module_name: str) -> Optional[str]:
81
  """Extract a human-readable pathway name from a module key."""
82
+ family = parse_biomod_family(module_name)
83
  if family == "Hallmarks":
84
  raw = module_name.split("__", 1)[1]
85
  raw = raw.replace("HALLMARK_", "", 1)
 
218
  ) -> Dict[str, Dict[str, float]]:
219
  """RSA computed separately for each module family (Hallmarks vs Reactome)."""
220
  results = {}
221
+ families = [parse_biomod_family(n) for n in names]
222
  for fam in ["Hallmarks", "Reactome"]:
223
  mask = np.array([f == fam for f in families])
224
  if mask.sum() < 10:
 
447
  description=__doc__,
448
  formatter_class=argparse.RawDescriptionHelpFormatter,
449
  )
450
+ parser.add_argument("--biomod_embedding_path", "--module_embedding_path", type=str, required=True,
451
+ help="Path to module_embeddings.npy from experiment 1.", dest='module_embedding_path')
452
+ parser.add_argument("--biomod_json", "--module_json", type=str,
453
+ default=str(REPO_ROOT / "model_dependencies" / "biomod_data_all" / "merged_biomods.json"),
454
+ help="Path to merged_biomods.json.", dest='module_json')
455
  parser.add_argument("--descriptions_file", type=str, default=None,
456
  help="Optional JSON with extended pathway descriptions.")
457
  parser.add_argument("--sentence_model", type=str,
 
487
 
488
  # --- Load module names and embeddings ---
489
  logger.info("Loading module names from %s", args.module_json)
490
+ module_names = load_biomod_names(args.module_json)
491
  logger.info("Total modules: %d", len(module_names))
492
 
493
  logger.info("Loading module embeddings from %s", args.module_embedding_path)
BioMod/interpretability/{module_text_alignment/module_text_alignment_extended.py → biomod_text_alignment/biomod_text_alignment_extended.py} RENAMED
@@ -6,9 +6,9 @@
6
  3. Hallmarks deep dive (per-module alignment scores, heatmap)
7
 
8
  Usage (run from the BioMod repository root):
9
- python BioMod/interpretability/module_text_alignment/module_text_alignment_extended.py \
10
- --module_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
11
- --module_json model_dependencies/module_data_all/merged_modules.json \
12
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
13
  --output_dir outputs/interpretability/text_alignment/pretrain_subset_v2_extended
14
  """
@@ -36,14 +36,14 @@
36
 
37
  HERE = Path(__file__).resolve().parent
38
  INTERP_DIR = HERE.parent
39
- PERFORMER_MODULE = INTERP_DIR.parent
40
- REPO_ROOT = PERFORMER_MODULE.parent
41
- for p in (str(INTERP_DIR), str(PERFORMER_MODULE), str(REPO_ROOT)):
42
  if p not in sys.path:
43
  sys.path.insert(0, p)
44
 
45
- from interpretability.utils import load_module_names, parse_module_family
46
- from module_text_alignment.module_text_alignment import (
47
  build_text_descriptions,
48
  compute_cka,
49
  compute_rsa,
@@ -150,7 +150,7 @@ def reactome_category_rsa(
150
  logger.info("Analysis 1: Reactome category-level RSA")
151
  logger.info("=" * 50)
152
 
153
- families = [parse_module_family(n) for n in names]
154
  reactome_mask = np.array([f == "Reactome" for f in families])
155
  reactome_idx = np.where(reactome_mask)[0]
156
 
@@ -259,7 +259,7 @@ def reactome_with_descriptions(
259
  with open(descriptions_file, "r", encoding="utf-8") as f:
260
  descriptions = json.load(f)
261
 
262
- families = [parse_module_family(n) for n in names]
263
  reactome_mask = np.array([f == "Reactome" for f in families])
264
  reactome_idx = np.where(reactome_mask)[0]
265
 
@@ -317,7 +317,7 @@ def hallmarks_deep_dive(
317
  logger.info("Analysis 3: Hallmarks deep dive")
318
  logger.info("=" * 50)
319
 
320
- families = [parse_module_family(n) for n in names]
321
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
322
  hallmark_idx = np.where(hallmark_mask)[0]
323
 
@@ -445,9 +445,9 @@ def hallmarks_deep_dive(
445
  def parse_args():
446
  parser = argparse.ArgumentParser(description=__doc__,
447
  formatter_class=argparse.RawDescriptionHelpFormatter)
448
- parser.add_argument("--module_embedding_path", type=str, required=True)
449
- parser.add_argument("--module_json", type=str,
450
- default=str(REPO_ROOT / "model_dependencies" / "module_data_all" / "merged_modules.json"))
451
  parser.add_argument("--descriptions_file", type=str, default=None,
452
  help="JSON with Reactome summaryText descriptions.")
453
  parser.add_argument("--sentence_model", type=str,
@@ -474,7 +474,7 @@ def main():
474
  )
475
 
476
  # Load data
477
- module_names = load_module_names(args.module_json)
478
  all_embeddings = np.load(args.module_embedding_path)
479
  indices, names, texts = build_text_descriptions(module_names)
480
  module_emb = all_embeddings[indices]
 
6
  3. Hallmarks deep dive (per-module alignment scores, heatmap)
7
 
8
  Usage (run from the BioMod repository root):
9
+ python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment_extended.py \
10
+ --biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
11
+ --biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
12
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
13
  --output_dir outputs/interpretability/text_alignment/pretrain_subset_v2_extended
14
  """
 
36
 
37
  HERE = Path(__file__).resolve().parent
38
  INTERP_DIR = HERE.parent
39
+ BIOMOD_SOURCE = INTERP_DIR.parent
40
+ REPO_ROOT = BIOMOD_SOURCE.parent
41
+ for p in (str(INTERP_DIR), str(BIOMOD_SOURCE), str(REPO_ROOT)):
42
  if p not in sys.path:
43
  sys.path.insert(0, p)
44
 
45
+ from interpretability.utils import load_biomod_names, parse_biomod_family
46
+ from biomod_text_alignment.biomod_text_alignment import (
47
  build_text_descriptions,
48
  compute_cka,
49
  compute_rsa,
 
150
  logger.info("Analysis 1: Reactome category-level RSA")
151
  logger.info("=" * 50)
152
 
153
+ families = [parse_biomod_family(n) for n in names]
154
  reactome_mask = np.array([f == "Reactome" for f in families])
155
  reactome_idx = np.where(reactome_mask)[0]
156
 
 
259
  with open(descriptions_file, "r", encoding="utf-8") as f:
260
  descriptions = json.load(f)
261
 
262
+ families = [parse_biomod_family(n) for n in names]
263
  reactome_mask = np.array([f == "Reactome" for f in families])
264
  reactome_idx = np.where(reactome_mask)[0]
265
 
 
317
  logger.info("Analysis 3: Hallmarks deep dive")
318
  logger.info("=" * 50)
319
 
320
+ families = [parse_biomod_family(n) for n in names]
321
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
322
  hallmark_idx = np.where(hallmark_mask)[0]
323
 
 
445
  def parse_args():
446
  parser = argparse.ArgumentParser(description=__doc__,
447
  formatter_class=argparse.RawDescriptionHelpFormatter)
448
+ parser.add_argument("--biomod_embedding_path", "--module_embedding_path", type=str, required=True, dest='module_embedding_path')
449
+ parser.add_argument("--biomod_json", "--module_json", type=str,
450
+ default=str(REPO_ROOT / "model_dependencies" / "biomod_data_all" / "merged_biomods.json"), dest='module_json')
451
  parser.add_argument("--descriptions_file", type=str, default=None,
452
  help="JSON with Reactome summaryText descriptions.")
453
  parser.add_argument("--sentence_model", type=str,
 
474
  )
475
 
476
  # Load data
477
+ module_names = load_biomod_names(args.module_json)
478
  all_embeddings = np.load(args.module_embedding_path)
479
  indices, names, texts = build_text_descriptions(module_names)
480
  module_emb = all_embeddings[indices]
BioMod/interpretability/{module_text_alignment/module_text_alignment_publication.py → biomod_text_alignment/biomod_text_alignment_publication.py} RENAMED
@@ -4,13 +4,13 @@
4
  Supports multi-model comparison and cross-dataset validation.
5
 
6
  Usage (run from the BioMod repository root):
7
- python BioMod/interpretability/module_text_alignment/module_text_alignment_publication.py \
8
- --module_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
9
- --module_embedding_path2 outputs/interpretability/geometry/pancreas_scib/module_embeddings.npy \
10
- --module_json model_dependencies/module_data_all/merged_modules.json \
11
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
12
  --sentence_models_compare "sentence-transformers/all-mpnet-base-v2" \
13
- --descriptions_file BioMod/interpretability/module_text_alignment/reactome_descriptions.json \
14
  --n_permutations 1000 \
15
  --n_bootstrap 2000 \
16
  --output_dir outputs/interpretability/text_alignment/publication
@@ -40,19 +40,19 @@
40
 
41
  HERE = Path(__file__).resolve().parent
42
  INTERP_DIR = HERE.parent
43
- PERFORMER_MODULE = INTERP_DIR.parent
44
- REPO_ROOT = PERFORMER_MODULE.parent
45
- for p in (str(HERE), str(INTERP_DIR), str(PERFORMER_MODULE), str(REPO_ROOT)):
46
  if p not in sys.path:
47
  sys.path.insert(0, p)
48
 
49
  from interpretability.utils import (
50
  HALLMARK_PROCESS,
51
- load_module_names,
52
  parse_hallmark_process,
53
- parse_module_family,
54
  )
55
- from module_text_alignment.module_text_alignment import (
56
  build_text_descriptions,
57
  compute_cka,
58
  compute_rsa,
@@ -60,7 +60,7 @@
60
  mantel_test,
61
  permutation_test_cka,
62
  )
63
- from module_text_alignment.module_text_alignment_extended import (
64
  REACTOME_CATEGORIES,
65
  categorize_reactome,
66
  )
@@ -166,8 +166,8 @@ def bootstrap_rsa(
166
  float(np.percentile(boot_rs, 97.5))
167
 
168
 
169
- def get_module_sizes(module_json: str) -> Dict[str, int]:
170
- """Get number of genes per module from merged_modules.json."""
171
  with open(module_json, "r", encoding="utf-8") as f:
172
  modules = json.load(f)
173
  return {name: len(genes) for name, genes in modules.items()}
@@ -182,12 +182,12 @@ def parse_args() -> argparse.Namespace:
182
  description=__doc__,
183
  formatter_class=argparse.RawDescriptionHelpFormatter,
184
  )
185
- parser.add_argument("--module_embedding_path", type=str, required=True,
186
- help="Primary module_embeddings.npy (e.g. pretrain_subset)")
187
- parser.add_argument("--module_embedding_path2", type=str, default=None,
188
- help="Second dataset module_embeddings.npy for cross-dataset comparison")
189
- parser.add_argument("--module_json", type=str,
190
- default=str(REPO_ROOT / "model_dependencies" / "module_data_all" / "merged_modules.json"))
191
  parser.add_argument("--descriptions_file", type=str, default=None)
192
  parser.add_argument("--sentence_model", type=str,
193
  default="pritamdeka/S-PubMedBert-MS-MARCO")
@@ -314,7 +314,7 @@ def fig2_hallmarks_heatmaps(
314
  output_dir: Path,
315
  ) -> Dict:
316
  """Hallmarks 50x50 similarity matrices with rank-transform and clustering."""
317
- families = [parse_module_family(n) for n in names]
318
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
319
  hallmark_idx = np.where(hallmark_mask)[0]
320
 
@@ -433,7 +433,7 @@ def fig3_per_hallmark_alignment(
433
  seed: int = 42,
434
  ) -> Dict:
435
  """Lollipop chart of per-hallmark alignment scores, colored by process."""
436
- families = [parse_module_family(n) for n in names]
437
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
438
  hallmark_idx = np.where(hallmark_mask)[0]
439
 
@@ -531,7 +531,7 @@ def fig4_category_alignment(
531
  output_dir: Path,
532
  ) -> Dict:
533
  """Violin/box plot of alignment scores grouped by MSigDB process category."""
534
- families = [parse_module_family(n) for n in names]
535
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
536
  hallmark_idx = np.where(hallmark_mask)[0]
537
 
@@ -634,7 +634,7 @@ def fig5_granularity_effect(
634
  output_dir: Path = None,
635
  ) -> Dict:
636
  """RSA at multiple granularity levels: fine → category → super-category."""
637
- families = [parse_module_family(n) for n in names]
638
  reactome_mask = np.array([f == "Reactome" for f in families])
639
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
640
  reactome_idx = np.where(reactome_mask)[0]
@@ -761,7 +761,7 @@ def fig6_model_comparison(
761
  output_dir: Path = None,
762
  ) -> Dict:
763
  """Grouped bar chart comparing RSA across models and datasets."""
764
- families = [parse_module_family(n) for n in names]
765
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
766
  hallmark_idx = np.where(hallmark_mask)[0]
767
 
@@ -861,7 +861,7 @@ def fig6_model_comparison(
861
  # Deep Analysis: Alignment vs Module Size
862
  # ============================================================
863
 
864
- def analysis_alignment_vs_module_size(
865
  module_emb: np.ndarray,
866
  text_emb: np.ndarray,
867
  names: List[str],
@@ -870,7 +870,7 @@ def analysis_alignment_vs_module_size(
870
  output_dir: Path,
871
  ) -> Dict:
872
  """Scatter: n_genes vs alignment score. Tests if larger modules align better."""
873
- families = [parse_module_family(n) for n in names]
874
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
875
  hallmark_idx = np.where(hallmark_mask)[0]
876
 
@@ -891,7 +891,7 @@ def analysis_alignment_vs_module_size(
891
  per_module_corr[i] = r
892
 
893
  # Module sizes
894
- module_sizes = get_module_sizes(module_json)
895
  h_sizes = np.array([module_sizes.get(n, 0) for n in h_names])
896
 
897
  # Correlation
@@ -945,7 +945,7 @@ def analysis_negative_results(
945
  n_show: int = 5,
946
  ) -> Dict:
947
  """For poorly-aligned modules, show neighbor divergence."""
948
- families = [parse_module_family(n) for n in names]
949
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
950
  hallmark_idx = np.where(hallmark_mask)[0]
951
 
@@ -1022,7 +1022,7 @@ def analysis_knn_overlap(
1022
  output_dir: Path = None,
1023
  ) -> Dict:
1024
  """kNN overlap with bootstrap CI and per-category breakdown."""
1025
- families = [parse_module_family(n) for n in names]
1026
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
1027
  hallmark_idx = np.where(hallmark_mask)[0]
1028
 
@@ -1103,13 +1103,13 @@ def main():
1103
  logger.info("Args: %s", vars(args))
1104
 
1105
  # ---- Load data ----
1106
- module_names = load_module_names(args.module_json)
1107
  all_module_emb = np.load(args.module_embedding_path)
1108
  logger.info("Loaded module embeddings: %s from %s",
1109
  all_module_emb.shape, args.module_embedding_path)
1110
 
1111
  # Filter to semantic modules (Hallmarks + Reactome)
1112
- families = [parse_module_family(n) for n in module_names]
1113
  semantic_mask = np.array([f in ("Hallmarks", "Reactome") for f in families])
1114
  semantic_idx = np.where(semantic_mask)[0]
1115
  module_emb = all_module_emb[semantic_idx]
@@ -1205,7 +1205,7 @@ def main():
1205
 
1206
  # ---- Deep Analysis ----
1207
  logger.info("Running deep analysis: alignment vs module size...")
1208
- metrics_size = analysis_alignment_vs_module_size(
1209
  module_emb, text_emb, names, texts, args.module_json, output_dir,
1210
  )
1211
 
 
4
  Supports multi-model comparison and cross-dataset validation.
5
 
6
  Usage (run from the BioMod repository root):
7
+ python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment_publication.py \
8
+ --biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
9
+ --biomod_embedding_path2 outputs/interpretability/geometry/pancreas_scib/module_embeddings.npy \
10
+ --biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
11
  --sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
12
  --sentence_models_compare "sentence-transformers/all-mpnet-base-v2" \
13
+ --descriptions_file BioMod/interpretability/biomod_text_alignment/reactome_descriptions.json \
14
  --n_permutations 1000 \
15
  --n_bootstrap 2000 \
16
  --output_dir outputs/interpretability/text_alignment/publication
 
40
 
41
  HERE = Path(__file__).resolve().parent
42
  INTERP_DIR = HERE.parent
43
+ BIOMOD_SOURCE = INTERP_DIR.parent
44
+ REPO_ROOT = BIOMOD_SOURCE.parent
45
+ for p in (str(HERE), str(INTERP_DIR), str(BIOMOD_SOURCE), str(REPO_ROOT)):
46
  if p not in sys.path:
47
  sys.path.insert(0, p)
48
 
49
  from interpretability.utils import (
50
  HALLMARK_PROCESS,
51
+ load_biomod_names,
52
  parse_hallmark_process,
53
+ parse_biomod_family,
54
  )
55
+ from biomod_text_alignment.biomod_text_alignment import (
56
  build_text_descriptions,
57
  compute_cka,
58
  compute_rsa,
 
60
  mantel_test,
61
  permutation_test_cka,
62
  )
63
+ from biomod_text_alignment.biomod_text_alignment_extended import (
64
  REACTOME_CATEGORIES,
65
  categorize_reactome,
66
  )
 
166
  float(np.percentile(boot_rs, 97.5))
167
 
168
 
169
+ def get_biomod_sizes(module_json: str) -> Dict[str, int]:
170
+ """Get number of genes per module from merged_biomods.json."""
171
  with open(module_json, "r", encoding="utf-8") as f:
172
  modules = json.load(f)
173
  return {name: len(genes) for name, genes in modules.items()}
 
182
  description=__doc__,
183
  formatter_class=argparse.RawDescriptionHelpFormatter,
184
  )
185
+ parser.add_argument("--biomod_embedding_path", "--module_embedding_path", type=str, required=True,
186
+ help="Primary module_embeddings.npy (e.g. pretrain_subset)", dest='module_embedding_path')
187
+ parser.add_argument("--biomod_embedding_path2", "--module_embedding_path2", type=str, default=None,
188
+ help="Second dataset module_embeddings.npy for cross-dataset comparison", dest='module_embedding_path2')
189
+ parser.add_argument("--biomod_json", "--module_json", type=str,
190
+ default=str(REPO_ROOT / "model_dependencies" / "biomod_data_all" / "merged_biomods.json"), dest='module_json')
191
  parser.add_argument("--descriptions_file", type=str, default=None)
192
  parser.add_argument("--sentence_model", type=str,
193
  default="pritamdeka/S-PubMedBert-MS-MARCO")
 
314
  output_dir: Path,
315
  ) -> Dict:
316
  """Hallmarks 50x50 similarity matrices with rank-transform and clustering."""
317
+ families = [parse_biomod_family(n) for n in names]
318
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
319
  hallmark_idx = np.where(hallmark_mask)[0]
320
 
 
433
  seed: int = 42,
434
  ) -> Dict:
435
  """Lollipop chart of per-hallmark alignment scores, colored by process."""
436
+ families = [parse_biomod_family(n) for n in names]
437
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
438
  hallmark_idx = np.where(hallmark_mask)[0]
439
 
 
531
  output_dir: Path,
532
  ) -> Dict:
533
  """Violin/box plot of alignment scores grouped by MSigDB process category."""
534
+ families = [parse_biomod_family(n) for n in names]
535
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
536
  hallmark_idx = np.where(hallmark_mask)[0]
537
 
 
634
  output_dir: Path = None,
635
  ) -> Dict:
636
  """RSA at multiple granularity levels: fine → category → super-category."""
637
+ families = [parse_biomod_family(n) for n in names]
638
  reactome_mask = np.array([f == "Reactome" for f in families])
639
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
640
  reactome_idx = np.where(reactome_mask)[0]
 
761
  output_dir: Path = None,
762
  ) -> Dict:
763
  """Grouped bar chart comparing RSA across models and datasets."""
764
+ families = [parse_biomod_family(n) for n in names]
765
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
766
  hallmark_idx = np.where(hallmark_mask)[0]
767
 
 
861
  # Deep Analysis: Alignment vs Module Size
862
  # ============================================================
863
 
864
+ def analysis_alignment_vs_biomod_size(
865
  module_emb: np.ndarray,
866
  text_emb: np.ndarray,
867
  names: List[str],
 
870
  output_dir: Path,
871
  ) -> Dict:
872
  """Scatter: n_genes vs alignment score. Tests if larger modules align better."""
873
+ families = [parse_biomod_family(n) for n in names]
874
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
875
  hallmark_idx = np.where(hallmark_mask)[0]
876
 
 
891
  per_module_corr[i] = r
892
 
893
  # Module sizes
894
+ module_sizes = get_biomod_sizes(module_json)
895
  h_sizes = np.array([module_sizes.get(n, 0) for n in h_names])
896
 
897
  # Correlation
 
945
  n_show: int = 5,
946
  ) -> Dict:
947
  """For poorly-aligned modules, show neighbor divergence."""
948
+ families = [parse_biomod_family(n) for n in names]
949
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
950
  hallmark_idx = np.where(hallmark_mask)[0]
951
 
 
1022
  output_dir: Path = None,
1023
  ) -> Dict:
1024
  """kNN overlap with bootstrap CI and per-category breakdown."""
1025
+ families = [parse_biomod_family(n) for n in names]
1026
  hallmark_mask = np.array([f == "Hallmarks" for f in families])
1027
  hallmark_idx = np.where(hallmark_mask)[0]
1028
 
 
1103
  logger.info("Args: %s", vars(args))
1104
 
1105
  # ---- Load data ----
1106
+ module_names = load_biomod_names(args.module_json)
1107
  all_module_emb = np.load(args.module_embedding_path)
1108
  logger.info("Loaded module embeddings: %s from %s",
1109
  all_module_emb.shape, args.module_embedding_path)
1110
 
1111
  # Filter to semantic modules (Hallmarks + Reactome)
1112
+ families = [parse_biomod_family(n) for n in module_names]
1113
  semantic_mask = np.array([f in ("Hallmarks", "Reactome") for f in families])
1114
  semantic_idx = np.where(semantic_mask)[0]
1115
  module_emb = all_module_emb[semantic_idx]
 
1205
 
1206
  # ---- Deep Analysis ----
1207
  logger.info("Running deep analysis: alignment vs module size...")
1208
+ metrics_size = analysis_alignment_vs_biomod_size(
1209
  module_emb, text_emb, names, texts, args.module_json, output_dir,
1210
  )
1211
 
BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/fetch_reactome_descriptions.py RENAMED
@@ -1,11 +1,11 @@
1
  """Fetch Reactome pathway descriptions via the Content Service REST API.
2
 
3
  Produces a JSON file mapping module_name -> summaryText that can be passed
4
- to module_text_alignment_extended.py via --descriptions_file.
5
 
6
  Usage (run from the BioMod repository root):
7
- python BioMod/interpretability/module_text_alignment/fetch_reactome_descriptions.py \
8
- --module_json model_dependencies/module_data_all/merged_modules.json \
9
  --output reactome_descriptions.json
10
 
11
  Requires internet access to https://reactome.org/ContentService/
@@ -24,12 +24,12 @@
24
 
25
  HERE = Path(__file__).resolve().parent
26
  INTERP_DIR = HERE.parent
27
- PERFORMER_MODULE = INTERP_DIR.parent
28
- for p in (str(INTERP_DIR), str(PERFORMER_MODULE)):
29
  if p not in sys.path:
30
  sys.path.insert(0, p)
31
 
32
- from interpretability.utils import load_module_names, parse_module_family
33
 
34
  REACTOME_API = "https://reactome.org/ContentService/data/query"
35
 
@@ -56,14 +56,14 @@ def fetch_summary(rhs_id: str, retries: int = 3) -> str:
56
 
57
  def main():
58
  parser = argparse.ArgumentParser(description=__doc__)
59
- parser.add_argument("--module_json", type=str, required=True)
60
  parser.add_argument("--output", type=str, default="reactome_descriptions.json")
61
  parser.add_argument("--delay", type=float, default=0.1,
62
  help="Delay between API calls (seconds)")
63
  args = parser.parse_args()
64
 
65
- module_names = load_module_names(args.module_json)
66
- reactome_modules = [n for n in module_names if parse_module_family(n) == "Reactome"]
67
  print(f"Reactome modules to fetch: {len(reactome_modules)}")
68
 
69
  descriptions = {}
 
1
  """Fetch Reactome pathway descriptions via the Content Service REST API.
2
 
3
  Produces a JSON file mapping module_name -> summaryText that can be passed
4
+ to biomod_text_alignment_extended.py via --descriptions_file.
5
 
6
  Usage (run from the BioMod repository root):
7
+ python BioMod/interpretability/biomod_text_alignment/fetch_reactome_descriptions.py \
8
+ --biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
9
  --output reactome_descriptions.json
10
 
11
  Requires internet access to https://reactome.org/ContentService/
 
24
 
25
  HERE = Path(__file__).resolve().parent
26
  INTERP_DIR = HERE.parent
27
+ BIOMOD_SOURCE = INTERP_DIR.parent
28
+ for p in (str(INTERP_DIR), str(BIOMOD_SOURCE)):
29
  if p not in sys.path:
30
  sys.path.insert(0, p)
31
 
32
+ from interpretability.utils import load_biomod_names, parse_biomod_family
33
 
34
  REACTOME_API = "https://reactome.org/ContentService/data/query"
35
 
 
56
 
57
  def main():
58
  parser = argparse.ArgumentParser(description=__doc__)
59
+ parser.add_argument("--biomod_json", "--module_json", type=str, required=True, dest='module_json')
60
  parser.add_argument("--output", type=str, default="reactome_descriptions.json")
61
  parser.add_argument("--delay", type=float, default=0.1,
62
  help="Delay between API calls (seconds)")
63
  args = parser.parse_args()
64
 
65
+ module_names = load_biomod_names(args.module_json)
66
+ reactome_modules = [n for n in module_names if parse_biomod_family(n) == "Reactome"]
67
  print(f"Reactome modules to fetch: {len(reactome_modules)}")
68
 
69
  descriptions = {}
BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/reactome_descriptions.json RENAMED
File without changes
BioMod/interpretability/utils.py CHANGED
@@ -1,7 +1,7 @@
1
  """Shared helpers for module interpretability experiments.
2
 
3
  Provides:
4
- * module family / hallmark-process parsing from merged_modules.json keys
5
  * a generic in-memory dataset that yields raw [G_input] expression rows,
6
  compatible with DataPreprocessor.reindex_tensor_to_model_space
7
  """
@@ -86,7 +86,7 @@
86
  }
87
 
88
 
89
- def parse_module_family(module_name: str) -> str:
90
  head = module_name.split("__", 1)[0]
91
  return MODULE_FAMILY_MAP.get(head, "Other")
92
 
@@ -98,18 +98,18 @@ def parse_hallmark_process(module_name: str) -> Optional[str]:
98
  return HALLMARK_PROCESS.get(short)
99
 
100
 
101
- def build_module_metadata(module_names: Sequence[str]):
102
  rows = []
103
  for name in module_names:
104
  rows.append({
105
  "module_name": name,
106
- "family": parse_module_family(name),
107
  "hallmark_process": parse_hallmark_process(name),
108
  })
109
  return rows
110
 
111
 
112
- def load_module_names(module_json_path: str) -> List[str]:
113
  with open(module_json_path, "r", encoding="utf-8") as handle:
114
  return list(json.load(handle).keys())
115
 
 
1
  """Shared helpers for module interpretability experiments.
2
 
3
  Provides:
4
+ * module family / hallmark-process parsing from merged_biomods.json keys
5
  * a generic in-memory dataset that yields raw [G_input] expression rows,
6
  compatible with DataPreprocessor.reindex_tensor_to_model_space
7
  """
 
86
  }
87
 
88
 
89
+ def parse_biomod_family(module_name: str) -> str:
90
  head = module_name.split("__", 1)[0]
91
  return MODULE_FAMILY_MAP.get(head, "Other")
92
 
 
98
  return HALLMARK_PROCESS.get(short)
99
 
100
 
101
+ def build_biomod_metadata(module_names: Sequence[str]):
102
  rows = []
103
  for name in module_names:
104
  rows.append({
105
  "module_name": name,
106
+ "family": parse_biomod_family(name),
107
  "hallmark_process": parse_hallmark_process(name),
108
  })
109
  return rows
110
 
111
 
112
+ def load_biomod_names(module_json_path: str) -> List[str]:
113
  with open(module_json_path, "r", encoding="utf-8") as handle:
114
  return list(json.load(handle).keys())
115
 
BioMod/{models_performer_module.py → models_biomod.py} RENAMED
@@ -1,4 +1,4 @@
1
- """Module-based Performer encoder with MAE-style masking and configurable
2
  reconstruction decoders or direct prediction heads."""
3
 
4
  import sys
@@ -16,21 +16,21 @@
16
  sys.path.insert(0, model_dependencies_dir)
17
 
18
  try:
19
- from performer_pytorch_cont.ding_models import (
20
- Performer,
21
  Always,
22
  cast_tuple,
23
  )
24
  except ImportError:
25
  try:
26
- from performer_pytorch_cont.performer_pytorch import (
27
- Performer,
28
  Always,
29
  cast_tuple,
30
  )
31
  except ImportError:
32
  from performer_pytorch.performer_pytorch import (
33
- Performer,
34
  Always,
35
  cast_tuple,
36
  )
@@ -41,7 +41,7 @@
41
  import numpy as np
42
 
43
 
44
- def _migrate_legacy_module_name(value: str) -> str:
45
  """Translate serialized pre-rename names without changing their semantics."""
46
  replacements = (
47
  ("PATHCH", "MODULE"),
@@ -59,13 +59,13 @@ def _migrate_legacy_module_name(value: str) -> str:
59
  return value
60
 
61
 
62
- def migrate_legacy_module_config(config: Dict[str, object]) -> Dict[str, object]:
63
  """Return a module-named copy of an older serialized configuration."""
64
  def translate(value):
65
  if isinstance(value, dict):
66
  migrated = {}
67
  for key, item in value.items():
68
- new_key = _migrate_legacy_module_name(key) if isinstance(key, str) else key
69
  if new_key in migrated:
70
  raise ValueError(
71
  f"Legacy configuration migration would duplicate key {new_key!r}"
@@ -77,13 +77,13 @@ def translate(value):
77
  if isinstance(value, tuple):
78
  return tuple(translate(item) for item in value)
79
  if isinstance(value, str):
80
- return _migrate_legacy_module_name(value)
81
  return value
82
 
83
  return translate(dict(config))
84
 
85
 
86
- def migrate_legacy_module_state_dict(state_dict):
87
  """Translate old state-dict key names before PyTorch performs strict loading."""
88
  try:
89
  migrated = state_dict.__class__()
@@ -91,7 +91,7 @@ def migrate_legacy_module_state_dict(state_dict):
91
  migrated = {}
92
 
93
  for key, value in state_dict.items():
94
- new_key = _migrate_legacy_module_name(key) if isinstance(key, str) else key
95
  if new_key in migrated:
96
  # If both spellings are supplied, prefer the explicit new spelling.
97
  if key == new_key:
@@ -101,7 +101,7 @@ def migrate_legacy_module_state_dict(state_dict):
101
 
102
  if hasattr(state_dict, "_metadata"):
103
  migrated._metadata = {
104
- _migrate_legacy_module_name(key): value
105
  for key, value in state_dict._metadata.items()
106
  }
107
  return migrated
@@ -135,13 +135,13 @@ def normalize_fusion_method(fusion_method: str) -> str:
135
  )
136
 
137
 
138
- def sanitize_performer_module_config(
139
  config: Dict[str, object],
140
  *,
141
  source: str = "config",
142
  log_fn: Optional[Callable[[str], None]] = None,
143
  ) -> Dict[str, object]:
144
- cleaned = migrate_legacy_module_config(config)
145
  legacy_mode = str(cleaned.get("extra_gene_mode", "none")).lower()
146
  legacy_num = int(cleaned.get("extra_gene_num", 0) or 0)
147
  if legacy_mode != "none" or legacy_num > 0:
@@ -160,7 +160,7 @@ def sanitize_performer_module_config(
160
 
161
  return cleaned
162
 
163
- # ModuleEmbedding is implemented here to support three expression-fusion methods.
164
  # from sclong_module.models_module import ModuleToGeneDecoder
165
 
166
 
@@ -220,7 +220,7 @@ def forward(self, query: torch.Tensor, memory: torch.Tensor) -> torch.Tensor:
220
  return query
221
 
222
 
223
- class ModuleEmbedding(nn.Module):
224
  """Gene embedding, MLP, and attention pooling produce a module token.
225
 
226
  Expression fusion supports three methods:
@@ -823,8 +823,8 @@ def forward(
823
  return self._forward_bucketed(expr, return_module_targets)
824
 
825
 
826
- class PerformerEncoderOnly_Module(nn.Module):
827
- """Performer encoder for module inputs with configurable masking and decoding.
828
 
829
  use_gene2vec=True initializes from supplied pretrained gene embeddings;
830
  False uses random gene embeddings for ablation experiments."""
@@ -996,7 +996,7 @@ def __init__(
996
  print(f"[INFO] use_gene2vec=False: Using randomly initialized gene embeddings (dim={gene_embed_dim})")
997
 
998
  # Module embedding layer.
999
- self.module_embed = ModuleEmbedding(
1000
  module_sets=module_sets,
1001
  num_genes=num_genes,
1002
  gene_embed_dim=gene_embed_dim,
@@ -1054,7 +1054,7 @@ def __init__(
1054
  self.dropout = nn.Dropout(emb_dropout)
1055
 
1056
  # Performer encoder.
1057
- self.encoder = Performer(
1058
  base_dim, enc_depth, enc_heads, enc_dim_head,
1059
  local_attn_heads, local_window_size, causal, ff_mult, nb_features,
1060
  feature_redraw_interval, reversible, ff_chunks, generalized_attention,
@@ -1090,7 +1090,7 @@ def __init__(
1090
  if self.decoder_type == "performer":
1091
  self.decoder_pos_emb = nn.Parameter(torch.zeros(1, pos_tokens, self.dec_embed_dim))
1092
  self.decoder_dropout = nn.Dropout(emb_dropout)
1093
- self.decoder = Performer(
1094
  self.dec_embed_dim, dec_depth, dec_heads, dec_dim_head,
1095
  local_attn_heads=cast_tuple(0), local_window_size=256, causal=False, ff_mult=dec_ff_mult,
1096
  nb_features=None, feature_redraw_interval=feature_redraw_interval,
@@ -1203,7 +1203,7 @@ def _init_weights(self) -> None:
1203
  nn.init.constant_(m.bias, 0)
1204
  nn.init.constant_(m.weight, 1.0)
1205
 
1206
- def _sample_module_mask(
1207
  self,
1208
  batch_size: int,
1209
  seq_len: int,
@@ -1237,7 +1237,7 @@ def random_masking(self, x: torch.Tensor, mask_ratio: float) -> Tuple[torch.Tens
1237
  ids_restore = [3, 9, 1, 7, 2, 8, 4, 6, 5, 0] is the inverse permutation.
1238
  For example, original module 0 occupies position 3 after shuffling."""
1239
  b, l, d = x.shape
1240
- mask, ids_shuffle, ids_restore = self._sample_module_mask(
1241
  batch_size=b,
1242
  seq_len=l,
1243
  device=x.device,
@@ -1260,7 +1260,7 @@ def replace_masking(
1260
  raise RuntimeError("replace_masking requires encoder_mask_token to be initialized.")
1261
 
1262
  b, l, _ = x.shape
1263
- mask, _, _ = self._sample_module_mask(
1264
  batch_size=b,
1265
  seq_len=l,
1266
  device=x.device,
@@ -1271,7 +1271,7 @@ def replace_masking(
1271
  x_masked = torch.where(mask.bool().unsqueeze(-1), mask_tokens, x)
1272
  return x_masked, mask, ids_restore
1273
 
1274
- def _add_module_position_embedding(self, x: torch.Tensor) -> torch.Tensor:
1275
  pos = self.pos_emb[:, 1:, :] if self.use_cls else self.pos_emb
1276
  if pos.shape[1] != x.shape[1]:
1277
  raise RuntimeError(
@@ -1306,10 +1306,10 @@ def forward_encoder(
1306
  if self.mask_style == "replace":
1307
  # Replace token contents, then add module-ID embeddings while retaining all positions.
1308
  x, mask, ids_restore = self.replace_masking(x, mask_ratio)
1309
- x = self._add_module_position_embedding(x)
1310
  else:
1311
  # MAE path: add module-ID embeddings, then drop masked modules.
1312
- x = self._add_module_position_embedding(x)
1313
  x, mask, ids_restore = self.random_masking(x, mask_ratio)
1314
 
1315
  # Sparse module gating (1B): suppress unimportant modules
@@ -1361,7 +1361,7 @@ def _build_full_tokens(self, x: torch.Tensor, ids_restore: torch.Tensor) -> torc
1361
  return torch.cat([cls_token, x_], dim=1)
1362
  return x_
1363
 
1364
- def _aggregate_module_to_gene(
1365
  self,
1366
  module_feats: torch.Tensor,
1367
  ) -> torch.Tensor:
@@ -1383,13 +1383,13 @@ def _aggregate_module_to_gene(
1383
  gene_counts = gene_counts.clamp(min=1.0).unsqueeze(-1) # (B, G, 1)
1384
  return gene_feats / gene_counts
1385
 
1386
- def _predict_from_module_features(
1387
  self,
1388
  module_feats: torch.Tensor,
1389
  ) -> Union[List[torch.Tensor], torch.Tensor]:
1390
  """module_feats: (B, L, D)"""
1391
  if self.recon_target_level == "gene" and self.decoder_gene_agg:
1392
- gene_feats = self._aggregate_module_to_gene(module_feats) # (B, G, D)
1393
  assert self.gene_w is not None and self.gene_b is not None
1394
  # Apply a separate linear projection to each gene.
1395
  preds = torch.einsum("bgd,gdk->bgk", gene_feats, self.gene_w).squeeze(-1)
@@ -1445,9 +1445,9 @@ def forward_decoder_simple(
1445
  Returns per-module predictions, or a (B, G) tensor for decoder_gene_agg."""
1446
  x_full = self._build_full_tokens(x, ids_restore)
1447
  module_feats = x_full[:, 1:, :] if self.use_cls else x_full
1448
- return self._predict_from_module_features(module_feats)
1449
 
1450
- def forward_decoder_performer(
1451
  self,
1452
  x: torch.Tensor,
1453
  ids_restore: torch.Tensor,
@@ -1459,7 +1459,7 @@ def forward_decoder_performer(
1459
  x_full = self.decoder(x_full, pos_emb=self.layer_pos_emb(x_full))
1460
  x_full = self.decoder_norm(x_full)
1461
  module_feats = x_full[:, 1:, :] if self.use_cls else x_full
1462
- return self._predict_from_module_features(module_feats)
1463
 
1464
  def forward_decoder_cross_attn(
1465
  self,
@@ -1508,14 +1508,14 @@ def forward_decoder_cross_attn(
1508
  masked_queries = self.decoder_norm(masked_queries).to(dtype=module_feats.dtype)
1509
  module_feats[mask_bool] = masked_queries.reshape(-1, dim)
1510
 
1511
- return self._predict_from_module_features(module_feats)
1512
 
1513
  def forward_predictor_from_encoder(
1514
  self,
1515
  x: torch.Tensor,
1516
  ) -> Union[List[torch.Tensor], torch.Tensor]:
1517
  module_feats = x[:, 1:, :] if self.use_cls else x
1518
- return self._predict_from_module_features(module_feats)
1519
 
1520
  def compute_loss(
1521
  self,
@@ -1618,7 +1618,7 @@ def forward(
1618
  return out
1619
 
1620
  if self.decoder_type == "performer":
1621
- preds = self.forward_decoder_performer(latent, ids_restore)
1622
  elif self.decoder_type == "cross_attn":
1623
  preds = self.forward_decoder_cross_attn(latent, mask, ids_restore)
1624
  elif self.decoder_type == "simple":
@@ -1643,7 +1643,7 @@ def get_last_loss_breakdown(self) -> dict:
1643
  def load_state_dict(self, state_dict, strict: bool = True, assign: bool = False):
1644
  """Load current or pre-rename weights under the module-named interface."""
1645
  return super().load_state_dict(
1646
- migrate_legacy_module_state_dict(state_dict),
1647
  strict=strict,
1648
  assign=assign,
1649
  )
 
1
+ """BioMod encoder for gene groups with MAE-style masking and configurable
2
  reconstruction decoders or direct prediction heads."""
3
 
4
  import sys
 
16
  sys.path.insert(0, model_dependencies_dir)
17
 
18
  try:
19
+ from pytorch_cont.ding_models import (
20
+ Performer as BioModEncoder,
21
  Always,
22
  cast_tuple,
23
  )
24
  except ImportError:
25
  try:
26
+ from pytorch_cont.pytorch import (
27
+ Performer as BioModEncoder,
28
  Always,
29
  cast_tuple,
30
  )
31
  except ImportError:
32
  from performer_pytorch.performer_pytorch import (
33
+ Performer as BioModEncoder,
34
  Always,
35
  cast_tuple,
36
  )
 
41
  import numpy as np
42
 
43
 
44
+ def _migrate_legacy_biomod_name(value: str) -> str:
45
  """Translate serialized pre-rename names without changing their semantics."""
46
  replacements = (
47
  ("PATHCH", "MODULE"),
 
59
  return value
60
 
61
 
62
+ def migrate_legacy_biomod_config(config: Dict[str, object]) -> Dict[str, object]:
63
  """Return a module-named copy of an older serialized configuration."""
64
  def translate(value):
65
  if isinstance(value, dict):
66
  migrated = {}
67
  for key, item in value.items():
68
+ new_key = _migrate_legacy_biomod_name(key) if isinstance(key, str) else key
69
  if new_key in migrated:
70
  raise ValueError(
71
  f"Legacy configuration migration would duplicate key {new_key!r}"
 
77
  if isinstance(value, tuple):
78
  return tuple(translate(item) for item in value)
79
  if isinstance(value, str):
80
+ return _migrate_legacy_biomod_name(value)
81
  return value
82
 
83
  return translate(dict(config))
84
 
85
 
86
+ def migrate_legacy_biomod_state_dict(state_dict):
87
  """Translate old state-dict key names before PyTorch performs strict loading."""
88
  try:
89
  migrated = state_dict.__class__()
 
91
  migrated = {}
92
 
93
  for key, value in state_dict.items():
94
+ new_key = _migrate_legacy_biomod_name(key) if isinstance(key, str) else key
95
  if new_key in migrated:
96
  # If both spellings are supplied, prefer the explicit new spelling.
97
  if key == new_key:
 
101
 
102
  if hasattr(state_dict, "_metadata"):
103
  migrated._metadata = {
104
+ _migrate_legacy_biomod_name(key): value
105
  for key, value in state_dict._metadata.items()
106
  }
107
  return migrated
 
135
  )
136
 
137
 
138
+ def sanitize_biomod_config(
139
  config: Dict[str, object],
140
  *,
141
  source: str = "config",
142
  log_fn: Optional[Callable[[str], None]] = None,
143
  ) -> Dict[str, object]:
144
+ cleaned = migrate_legacy_biomod_config(config)
145
  legacy_mode = str(cleaned.get("extra_gene_mode", "none")).lower()
146
  legacy_num = int(cleaned.get("extra_gene_num", 0) or 0)
147
  if legacy_mode != "none" or legacy_num > 0:
 
160
 
161
  return cleaned
162
 
163
+ # BioModEmbedding is implemented here to support three expression-fusion methods.
164
  # from sclong_module.models_module import ModuleToGeneDecoder
165
 
166
 
 
220
  return query
221
 
222
 
223
+ class BioModEmbedding(nn.Module):
224
  """Gene embedding, MLP, and attention pooling produce a module token.
225
 
226
  Expression fusion supports three methods:
 
823
  return self._forward_bucketed(expr, return_module_targets)
824
 
825
 
826
+ class BioMod(nn.Module):
827
+ """BioMod encoder for gene-group inputs with configurable masking and decoding.
828
 
829
  use_gene2vec=True initializes from supplied pretrained gene embeddings;
830
  False uses random gene embeddings for ablation experiments."""
 
996
  print(f"[INFO] use_gene2vec=False: Using randomly initialized gene embeddings (dim={gene_embed_dim})")
997
 
998
  # Module embedding layer.
999
+ self.module_embed = BioModEmbedding(
1000
  module_sets=module_sets,
1001
  num_genes=num_genes,
1002
  gene_embed_dim=gene_embed_dim,
 
1054
  self.dropout = nn.Dropout(emb_dropout)
1055
 
1056
  # Performer encoder.
1057
+ self.encoder = BioModEncoder(
1058
  base_dim, enc_depth, enc_heads, enc_dim_head,
1059
  local_attn_heads, local_window_size, causal, ff_mult, nb_features,
1060
  feature_redraw_interval, reversible, ff_chunks, generalized_attention,
 
1090
  if self.decoder_type == "performer":
1091
  self.decoder_pos_emb = nn.Parameter(torch.zeros(1, pos_tokens, self.dec_embed_dim))
1092
  self.decoder_dropout = nn.Dropout(emb_dropout)
1093
+ self.decoder = BioModEncoder(
1094
  self.dec_embed_dim, dec_depth, dec_heads, dec_dim_head,
1095
  local_attn_heads=cast_tuple(0), local_window_size=256, causal=False, ff_mult=dec_ff_mult,
1096
  nb_features=None, feature_redraw_interval=feature_redraw_interval,
 
1203
  nn.init.constant_(m.bias, 0)
1204
  nn.init.constant_(m.weight, 1.0)
1205
 
1206
+ def _sample_biomod_mask(
1207
  self,
1208
  batch_size: int,
1209
  seq_len: int,
 
1237
  ids_restore = [3, 9, 1, 7, 2, 8, 4, 6, 5, 0] is the inverse permutation.
1238
  For example, original module 0 occupies position 3 after shuffling."""
1239
  b, l, d = x.shape
1240
+ mask, ids_shuffle, ids_restore = self._sample_biomod_mask(
1241
  batch_size=b,
1242
  seq_len=l,
1243
  device=x.device,
 
1260
  raise RuntimeError("replace_masking requires encoder_mask_token to be initialized.")
1261
 
1262
  b, l, _ = x.shape
1263
+ mask, _, _ = self._sample_biomod_mask(
1264
  batch_size=b,
1265
  seq_len=l,
1266
  device=x.device,
 
1271
  x_masked = torch.where(mask.bool().unsqueeze(-1), mask_tokens, x)
1272
  return x_masked, mask, ids_restore
1273
 
1274
+ def _add_biomod_position_embedding(self, x: torch.Tensor) -> torch.Tensor:
1275
  pos = self.pos_emb[:, 1:, :] if self.use_cls else self.pos_emb
1276
  if pos.shape[1] != x.shape[1]:
1277
  raise RuntimeError(
 
1306
  if self.mask_style == "replace":
1307
  # Replace token contents, then add module-ID embeddings while retaining all positions.
1308
  x, mask, ids_restore = self.replace_masking(x, mask_ratio)
1309
+ x = self._add_biomod_position_embedding(x)
1310
  else:
1311
  # MAE path: add module-ID embeddings, then drop masked modules.
1312
+ x = self._add_biomod_position_embedding(x)
1313
  x, mask, ids_restore = self.random_masking(x, mask_ratio)
1314
 
1315
  # Sparse module gating (1B): suppress unimportant modules
 
1361
  return torch.cat([cls_token, x_], dim=1)
1362
  return x_
1363
 
1364
+ def _aggregate_biomod_to_gene(
1365
  self,
1366
  module_feats: torch.Tensor,
1367
  ) -> torch.Tensor:
 
1383
  gene_counts = gene_counts.clamp(min=1.0).unsqueeze(-1) # (B, G, 1)
1384
  return gene_feats / gene_counts
1385
 
1386
+ def _predict_from_biomod_features(
1387
  self,
1388
  module_feats: torch.Tensor,
1389
  ) -> Union[List[torch.Tensor], torch.Tensor]:
1390
  """module_feats: (B, L, D)"""
1391
  if self.recon_target_level == "gene" and self.decoder_gene_agg:
1392
+ gene_feats = self._aggregate_biomod_to_gene(module_feats) # (B, G, D)
1393
  assert self.gene_w is not None and self.gene_b is not None
1394
  # Apply a separate linear projection to each gene.
1395
  preds = torch.einsum("bgd,gdk->bgk", gene_feats, self.gene_w).squeeze(-1)
 
1445
  Returns per-module predictions, or a (B, G) tensor for decoder_gene_agg."""
1446
  x_full = self._build_full_tokens(x, ids_restore)
1447
  module_feats = x_full[:, 1:, :] if self.use_cls else x_full
1448
+ return self._predict_from_biomod_features(module_feats)
1449
 
1450
+ def forward_decoder_biomod(
1451
  self,
1452
  x: torch.Tensor,
1453
  ids_restore: torch.Tensor,
 
1459
  x_full = self.decoder(x_full, pos_emb=self.layer_pos_emb(x_full))
1460
  x_full = self.decoder_norm(x_full)
1461
  module_feats = x_full[:, 1:, :] if self.use_cls else x_full
1462
+ return self._predict_from_biomod_features(module_feats)
1463
 
1464
  def forward_decoder_cross_attn(
1465
  self,
 
1508
  masked_queries = self.decoder_norm(masked_queries).to(dtype=module_feats.dtype)
1509
  module_feats[mask_bool] = masked_queries.reshape(-1, dim)
1510
 
1511
+ return self._predict_from_biomod_features(module_feats)
1512
 
1513
  def forward_predictor_from_encoder(
1514
  self,
1515
  x: torch.Tensor,
1516
  ) -> Union[List[torch.Tensor], torch.Tensor]:
1517
  module_feats = x[:, 1:, :] if self.use_cls else x
1518
+ return self._predict_from_biomod_features(module_feats)
1519
 
1520
  def compute_loss(
1521
  self,
 
1618
  return out
1619
 
1620
  if self.decoder_type == "performer":
1621
+ preds = self.forward_decoder_biomod(latent, ids_restore)
1622
  elif self.decoder_type == "cross_attn":
1623
  preds = self.forward_decoder_cross_attn(latent, mask, ids_restore)
1624
  elif self.decoder_type == "simple":
 
1643
  def load_state_dict(self, state_dict, strict: bool = True, assign: bool = False):
1644
  """Load current or pre-rename weights under the module-named interface."""
1645
  return super().load_state_dict(
1646
+ migrate_legacy_biomod_state_dict(state_dict),
1647
  strict=strict,
1648
  assign=assign,
1649
  )