Commit ·
6019d52
1
Parent(s): 7bf19c3
feat: build leakage-aware MitoInteract recovery pipeline
Browse files- AUDIT.md +22 -3
- README.md +118 -71
- model.py +94 -39
- recovery/DATA_PROVENANCE.md +61 -0
- recovery/README.md +159 -12
- recovery/cloud/modal_embed.py +92 -0
- recovery/config.json +8 -4
- recovery/pyproject.toml +18 -2
- recovery/reports/README.md +9 -0
- recovery/reports/dev-10k-audit.json +149 -0
- recovery/reports/dev-10k-baselines.json +864 -0
- recovery/reports/overfit-actual-embeddings.json +60 -0
- recovery/scripts/cluster_proteins.py +128 -0
- recovery/scripts/dataset_smoke.py +38 -9
- recovery/scripts/embed_sample.py +209 -0
- recovery/scripts/overfit_sanity.py +113 -0
- recovery/scripts/prepare_sample.py +241 -0
- recovery/scripts/run_baselines.py +203 -0
- recovery/scripts/train_head.py +245 -0
- recovery/src/mitointeract_recovery/__init__.py +2 -2
- recovery/src/mitointeract_recovery/affinity.py +9 -10
- recovery/src/mitointeract_recovery/chemistry.py +34 -0
- recovery/src/mitointeract_recovery/metrics.py +37 -0
- recovery/src/mitointeract_recovery/model.py +84 -0
- recovery/src/mitointeract_recovery/splits.py +108 -0
- recovery/tests/test_affinity.py +15 -12
- recovery/tests/test_checked_reports.py +55 -0
- recovery/tests/test_chemistry.py +20 -0
- recovery/tests/test_legacy_guard.py +29 -0
- recovery/tests/test_metrics.py +18 -0
- recovery/tests/test_model.py +32 -0
- recovery/tests/test_splits.py +45 -0
- recovery/uv.lock +0 -0
AUDIT.md
CHANGED
|
@@ -13,10 +13,11 @@ The published checkpoint is a failed proof of concept, not a scientifically usab
|
|
| 13 |
| Training set was only 32 pairs | `config.json`: `max_train_samples: 32` | Cannot support the claim that the model learned from the 1.9M-pair dataset |
|
| 14 |
| Validation set was only 16 pairs | `max_val_samples: 16` | Correlation metrics are unstable and not representative |
|
| 15 |
| Reported correlation is negative | Checkpoint: Pearson `-0.9107416`, Spearman `-0.5` | Predictions are directionally wrong on the tiny validation set |
|
| 16 |
-
| Regression error is enormous | Checkpoint: RMSE `7.0257`, MAE `6.8183` | Error is roughly the scale of the entire
|
| 17 |
| Training barely began | 32 samples / batch 4 × 2 epochs = 16 steps, but warmup is 500 steps | The run completed only 3.2% of warmup; approximate final LR was `3.2e-05`, not the configured `1e-3` |
|
| 18 |
| Model card contradicts checkpoint | README says ESM-2 650M; config/checkpoint use ESM-2 35M | Architecture and reproducibility claims are incorrect |
|
| 19 |
| Target semantics are undocumented | Dataset has `affinity_uM`, `neg_log10_affinity_M`, and normalized `affinity`; config names none | Published inference labels raw output as pKd without proving it was trained on pKd |
|
|
|
|
| 20 |
| Evaluation split is undocumented | No training/evaluation code is published | Protein/ligand leakage cannot be ruled out |
|
| 21 |
| Reproduction assets are missing | No training script, requirements, seed, split manifest, predictions, or metric calculation | Results cannot be independently reproduced |
|
| 22 |
| Serialization is unsafe and oversized | `full_model.pt` is a 315.7 MB pickle checkpoint loaded with `weights_only=False` | Avoidable code-execution risk and duplicated frozen encoders |
|
|
@@ -27,7 +28,7 @@ The separate `mitointeract_weights.pt` file contains 1,278,980 trainable head pa
|
|
| 27 |
|
| 28 |
MitoInteract v2 does not earn a model-card performance claim until all gates pass:
|
| 29 |
|
| 30 |
-
1. **Unit semantics:**
|
| 31 |
2. **Overfit sanity check:** the trainable head can overfit a fixed 32-example set to near-zero training error.
|
| 32 |
3. **Real validation:** at least 1,000 validation examples, with a published split manifest.
|
| 33 |
4. **Leakage-aware evaluation:** report random-pair, cold-protein, and cold-ligand/scaffold results separately.
|
|
@@ -40,7 +41,7 @@ MitoInteract v2 does not earn a model-card performance claim until all gates pas
|
|
| 40 |
For the first bounded experiment, use ESM-2 35M plus ChemBERTa with frozen encoders and a simpler regression head. On a fixed 10,000-pair development sample:
|
| 41 |
|
| 42 |
- validation/test size: at least 1,000 each
|
| 43 |
-
- primary target:
|
| 44 |
- primary split: cold-protein
|
| 45 |
- RMSE: lower than training-mean baseline
|
| 46 |
- Pearson: `> 0.30`
|
|
@@ -48,3 +49,21 @@ For the first bounded experiment, use ESM-2 35M plus ChemBERTa with frozen encod
|
|
| 48 |
- no NaNs or unit-conversion failures
|
| 49 |
|
| 50 |
These are recovery gates, not publishable state-of-the-art claims.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
| Training set was only 32 pairs | `config.json`: `max_train_samples: 32` | Cannot support the claim that the model learned from the 1.9M-pair dataset |
|
| 14 |
| Validation set was only 16 pairs | `max_val_samples: 16` | Correlation metrics are unstable and not representative |
|
| 15 |
| Reported correlation is negative | Checkpoint: Pearson `-0.9107416`, Spearman `-0.5` | Predictions are directionally wrong on the tiny validation set |
|
| 16 |
+
| Regression error is enormous | Checkpoint: RMSE `7.0257`, MAE `6.8183` | Error is roughly the scale of the entire reported target range |
|
| 17 |
| Training barely began | 32 samples / batch 4 × 2 epochs = 16 steps, but warmup is 500 steps | The run completed only 3.2% of warmup; approximate final LR was `3.2e-05`, not the configured `1e-3` |
|
| 18 |
| Model card contradicts checkpoint | README says ESM-2 650M; config/checkpoint use ESM-2 35M | Architecture and reproducibility claims are incorrect |
|
| 19 |
| Target semantics are undocumented | Dataset has `affinity_uM`, `neg_log10_affinity_M`, and normalized `affinity`; config names none | Published inference labels raw output as pKd without proving it was trained on pKd |
|
| 20 |
+
| Source target conflates measurement types | The pinned BindingDB notebook combines available `Ki`, `Kd`, `IC50`, and `EC50` values and averages them in linear µM | The dataset target is generic pAffinity, not pKd; assay provenance must be retained in any scientific successor |
|
| 21 |
| Evaluation split is undocumented | No training/evaluation code is published | Protein/ligand leakage cannot be ruled out |
|
| 22 |
| Reproduction assets are missing | No training script, requirements, seed, split manifest, predictions, or metric calculation | Results cannot be independently reproduced |
|
| 23 |
| Serialization is unsafe and oversized | `full_model.pt` is a 315.7 MB pickle checkpoint loaded with `weights_only=False` | Avoidable code-execution risk and duplicated frozen encoders |
|
|
|
|
| 28 |
|
| 29 |
MitoInteract v2 does not earn a model-card performance claim until all gates pass:
|
| 30 |
|
| 31 |
+
1. **Unit semantics:** pAffinity conversion tests pass, the exact target column is fixed to `neg_log10_affinity_M`, and the model card does not relabel mixed measurements as pKd.
|
| 32 |
2. **Overfit sanity check:** the trainable head can overfit a fixed 32-example set to near-zero training error.
|
| 33 |
3. **Real validation:** at least 1,000 validation examples, with a published split manifest.
|
| 34 |
4. **Leakage-aware evaluation:** report random-pair, cold-protein, and cold-ligand/scaffold results separately.
|
|
|
|
| 41 |
For the first bounded experiment, use ESM-2 35M plus ChemBERTa with frozen encoders and a simpler regression head. On a fixed 10,000-pair development sample:
|
| 42 |
|
| 43 |
- validation/test size: at least 1,000 each
|
| 44 |
+
- primary target: generic pAffinity (`neg_log10_affinity_M`), not pKd
|
| 45 |
- primary split: cold-protein
|
| 46 |
- RMSE: lower than training-mean baseline
|
| 47 |
- Pearson: `> 0.30`
|
|
|
|
| 49 |
- no NaNs or unit-conversion failures
|
| 50 |
|
| 51 |
These are recovery gates, not publishable state-of-the-art claims.
|
| 52 |
+
|
| 53 |
+
## Recovery evidence as of 2026-07-19
|
| 54 |
+
|
| 55 |
+
The deterministic 10,000-pair development sample is now backed by the immutable dataset revision `11e49b7ece33d62afd7f65bc05ce60ad37f9ba7b`. The original Parquet file was verified against SHA-256 `d50f4cae794298f32c39f75a9e8603528d20e50e4fa757025def412f5b2a78d8`.
|
| 56 |
+
|
| 57 |
+
| Gate | Result |
|
| 58 |
+
|---|---|
|
| 59 |
+
| pAffinity/µM numeric conversion | passed, while preserving the mixed-measurement warning |
|
| 60 |
+
| Python test suite | passed, 33 tests with the optional model dependencies installed |
|
| 61 |
+
| 32-pair synthetic-embedding overfit | passed |
|
| 62 |
+
| 32-pair pinned-encoder overfit | passed, RMSE below `1e-6` pAffinity |
|
| 63 |
+
| Random-pair combined ridge | RMSE `1.3358`, Pearson `0.5371`, Spearman `0.5187` |
|
| 64 |
+
| Exact protein-disjoint combined ridge | RMSE `1.3462`, Pearson `0.4384`, Spearman `0.4283` |
|
| 65 |
+
| MMseqs2 50% similarity-disjoint combined ridge | RMSE `1.4349`, Pearson `0.3625`, Spearman `0.3652` |
|
| 66 |
+
| Scaffold-disjoint combined ridge | RMSE `1.3649`, Pearson `0.5198`, Spearman `0.5354` |
|
| 67 |
+
| Full 10,000-pair neural head | pending GPU embedding run |
|
| 68 |
+
|
| 69 |
+
The MMseqs2 split clustered 2,255 unique proteins into 1,421 groups at 50% sequence identity and 80% bidirectional coverage, then held whole clusters out. The combined ridge model beat its train-mean RMSE of `1.5336`, but the ligand-only ridge slightly outperformed the combined model on this split. This is evidence of dataset shortcut risk, not evidence that the recovery model has learned general protein-ligand interaction biology.
|
README.md
CHANGED
|
@@ -4,91 +4,138 @@ tags:
|
|
| 4 |
- biology
|
| 5 |
- protein
|
| 6 |
- drug-target-interaction
|
| 7 |
-
- mitochondria
|
| 8 |
-
- apoptosis
|
| 9 |
- binding-affinity
|
| 10 |
- esm2
|
| 11 |
- chemberta
|
| 12 |
datasets:
|
| 13 |
- jglaser/binding_affinity
|
| 14 |
-
metrics:
|
| 15 |
-
- pearsonr
|
| 16 |
-
- spearmanr
|
| 17 |
-
- rmse
|
| 18 |
-
- mae
|
| 19 |
---
|
| 20 |
|
| 21 |
-
# MitoInteract
|
| 22 |
|
| 23 |
-
|
| 24 |
|
| 25 |
-
MitoInteract is
|
| 26 |
-
It combines:
|
| 27 |
-
- **ESM-2 650M** (protein encoder) for protein sequence understanding
|
| 28 |
-
- **ChemBERTa** (molecule encoder) for SMILES-based molecular representation
|
| 29 |
-
- **Bidirectional cross-attention** fusion layer
|
| 30 |
-
- **4-layer MLP** regression head
|
| 31 |
|
| 32 |
-
|
| 33 |
|
| 34 |
-
|
| 35 |
-
- Predict how ceramides interact with mitochondrial membrane proteins (VDAC1, VDAC2)
|
| 36 |
-
- Screen BCL-2 family protein interactions with BH3 mimetic drugs (venetoclax, navitoclax, ABT-737)
|
| 37 |
-
- Explore protein-lipid interactions in the apoptosis pathway
|
| 38 |
-
- Run in-silico binding experiments before wet-lab validation
|
| 39 |
|
| 40 |
-
##
|
| 41 |
|
| 42 |
-
|
| 43 |
-
from model import load_model, predict_binding
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
```
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
-
|
| 85 |
-
-
|
| 86 |
-
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
- biology
|
| 5 |
- protein
|
| 6 |
- drug-target-interaction
|
|
|
|
|
|
|
| 7 |
- binding-affinity
|
| 8 |
- esm2
|
| 9 |
- chemberta
|
| 10 |
datasets:
|
| 11 |
- jglaser/binding_affinity
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# MitoInteract
|
| 15 |
|
| 16 |
+
> **Research recovery in progress. The published v1 checkpoint is deprecated and must not be used to rank compounds or guide experiments.**
|
| 17 |
|
| 18 |
+
MitoInteract is an experimental protein-ligand binding-affinity project. The recovery target is **pAffinity**, defined as `-log10(the dataset's affinity value in molar units)`. Higher values indicate a numerically stronger reported affinity.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
This target is **not pKd**. The pinned dataset's BindingDB preprocessing combines available `Ki`, `Kd`, `IC50`, and `EC50` values and, when more than one exists, averages them in linear micromolar units. Those measurements are not scientifically interchangeable. The generic `pAffinity` name preserves the source semantics without pretending every row is a dissociation constant.
|
| 21 |
|
| 22 |
+
The first published checkpoint did not complete a meaningful training run. Its validation correlations were negative, its evaluation set contained only 16 pairs, and its model card overstated the architecture and intended use. The weights remain in this repository for provenance only. See [`AUDIT.md`](AUDIT.md) for the artifact-level evidence.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
## Current status
|
| 25 |
|
| 26 |
+
The recovery branch now provides:
|
|
|
|
| 27 |
|
| 28 |
+
- explicit and tested pAffinity/µM conversions;
|
| 29 |
+
- a dataset revision pinned to `11e49b7ece33d62afd7f65bc05ce60ad37f9ba7b`;
|
| 30 |
+
- deterministic sampling, pair deduplication, and split manifests;
|
| 31 |
+
- Bemis-Murcko scaffold grouping with an exact-ligand fallback for acyclic molecules;
|
| 32 |
+
- MMseqs2 protein clustering at 50% identity and 80% bidirectional coverage;
|
| 33 |
+
- train-mean, protein-only, ligand-only, and combined character n-gram ridge baselines;
|
| 34 |
+
- a corrected LayerNorm-based v2 head with standardized targets;
|
| 35 |
+
- pinned ESM-2 and ChemBERTa revisions, long-protein chunking, and cached frozen embeddings;
|
| 36 |
+
- safe `safetensors` output for new trainable weights;
|
| 37 |
+
- unit tests and a successful 32-pair end-to-end overfit sanity check.
|
| 38 |
|
| 39 |
+
No v2 neural checkpoint has earned a performance claim yet.
|
| 40 |
+
|
| 41 |
+
## Reproducible development baseline
|
| 42 |
+
|
| 43 |
+
These are **development results**, not an external benchmark. They use a deterministic 10,000-pair sample from `jglaser/binding_affinity`. Each test split contains 1,000 pairs.
|
| 44 |
+
|
| 45 |
+
| Split | Model | Test RMSE ↓ | Pearson ↑ | Spearman ↑ |
|
| 46 |
+
|---|---|---:|---:|---:|
|
| 47 |
+
| Random pair | train mean | 1.5650 | undefined | undefined |
|
| 48 |
+
| Random pair | combined character ridge | **1.3358** | **0.5371** | **0.5187** |
|
| 49 |
+
| Exact protein-disjoint | train mean | 1.4951 | undefined | undefined |
|
| 50 |
+
| Exact protein-disjoint | combined character ridge | **1.3462** | **0.4384** | **0.4283** |
|
| 51 |
+
| Protein-similarity-disjoint, MMseqs2 50% | train mean | 1.5336 | undefined | undefined |
|
| 52 |
+
| Protein-similarity-disjoint, MMseqs2 50% | combined character ridge | **1.4349** | **0.3625** | **0.3652** |
|
| 53 |
+
| Ligand scaffold-disjoint | train mean | 1.5834 | undefined | undefined |
|
| 54 |
+
| Ligand scaffold-disjoint | combined character ridge | **1.3649** | **0.5198** | **0.5354** |
|
| 55 |
+
|
| 56 |
+
The similarity-disjoint result is the most important current recovery signal. It passes the initial gate of beating the train mean with positive Pearson and Spearman correlation. It also shows that ligand features carry more signal than protein character features on this sample, which is a warning against interpreting random-split performance as learned protein-ligand interaction biology.
|
| 57 |
+
|
| 58 |
+
## Run the recovery checks
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
cd recovery
|
| 62 |
+
uv sync --dev
|
| 63 |
+
uv run pytest -q
|
| 64 |
+
|
| 65 |
+
uv run python scripts/dataset_smoke.py \
|
| 66 |
+
--rows 100 \
|
| 67 |
+
--output artifacts/dataset-smoke.json
|
| 68 |
```
|
| 69 |
|
| 70 |
+
To rebuild the 10,000-pair development data from the converted Parquet file:
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
uv run python scripts/prepare_sample.py \
|
| 74 |
+
--parquet /path/to/train.parquet \
|
| 75 |
+
--sample-size 10000 \
|
| 76 |
+
--seed 42 \
|
| 77 |
+
--output-dir artifacts/dev-10k
|
| 78 |
+
|
| 79 |
+
uv run python scripts/cluster_proteins.py \
|
| 80 |
+
--data-dir artifacts/dev-10k \
|
| 81 |
+
--min-seq-id 0.5 \
|
| 82 |
+
--coverage 0.8
|
| 83 |
+
|
| 84 |
+
uv run python scripts/run_baselines.py \
|
| 85 |
+
--data-dir artifacts/dev-10k \
|
| 86 |
+
--output artifacts/dev-10k/baselines.json
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
MMseqs2 is required for the sequence-similarity split. Generated data, embeddings, predictions, and weights are intentionally excluded from Git.
|
| 90 |
+
|
| 91 |
+
## v2 architecture under evaluation
|
| 92 |
+
|
| 93 |
+
The bounded neural recovery experiment uses frozen, revision-pinned encoders:
|
| 94 |
+
|
| 95 |
+
- protein: `facebook/esm2_t12_35M_UR50D`;
|
| 96 |
+
- ligand: `DeepChem/ChemBERTa-77M-MLM`;
|
| 97 |
+
- fusion: projected protein and ligand vectors plus elementwise product and absolute difference;
|
| 98 |
+
- regression: LayerNorm-based MLP over a standardized pAffinity target;
|
| 99 |
+
- serialization: trainable head in `safetensors`, with encoder names, revisions, and target scaler stored separately.
|
| 100 |
+
|
| 101 |
+
Proteins longer than ESM-2's context are split into residue chunks and pooled with length weighting instead of silently truncating the sequence. This is a reproducible baseline strategy, not proof that full-sequence pooling captures a binding site.
|
| 102 |
+
|
| 103 |
+
## Intended use
|
| 104 |
+
|
| 105 |
+
Current acceptable use:
|
| 106 |
+
|
| 107 |
+
- reproducing the failure analysis;
|
| 108 |
+
- testing protein-ligand affinity data pipelines;
|
| 109 |
+
- comparing leakage-aware splitting strategies;
|
| 110 |
+
- developing and auditing computational baselines.
|
| 111 |
+
|
| 112 |
+
Current unacceptable use:
|
| 113 |
+
|
| 114 |
+
- ranking compounds for laboratory work;
|
| 115 |
+
- estimating clinical activity, safety, dosage, or efficacy;
|
| 116 |
+
- claiming specificity for mitochondrial apoptosis proteins;
|
| 117 |
+
- treating a predicted pAffinity as experimental evidence;
|
| 118 |
+
- loading `full_model.pt` from an untrusted source.
|
| 119 |
+
|
| 120 |
+
The source dataset combines BindingDB, PDBbind-cn, BioLIP, and BindingMOAD. Its mixed assays, measurement types, source-specific preprocessing, duplicates, and possible censoring limit what a single regression target can support. See [`recovery/DATA_PROVENANCE.md`](recovery/DATA_PROVENANCE.md) for the pinned preprocessing evidence. A publishable model requires source-aware deduplication, stronger external evaluation, uncertainty estimates, and wet-lab validation.
|
| 121 |
+
|
| 122 |
+
## Legacy v1 facts
|
| 123 |
+
|
| 124 |
+
| Item | Recovered value |
|
| 125 |
+
|---|---:|
|
| 126 |
+
| Training pairs | 32 |
|
| 127 |
+
| Validation pairs | 16 |
|
| 128 |
+
| Optimizer steps | approximately 16 |
|
| 129 |
+
| Configured warmup steps | 500 |
|
| 130 |
+
| Pearson | -0.9107 |
|
| 131 |
+
| Spearman | -0.5000 |
|
| 132 |
+
| RMSE | 7.0257 in the undocumented legacy target scale |
|
| 133 |
+
| MAE | 6.8183 in the undocumented legacy target scale |
|
| 134 |
+
|
| 135 |
+
The original README claimed ESM-2 650M, while the checkpoint used ESM-2 35M. The original `full_model.pt` is a Python pickle loaded with `weights_only=False`; it is retained only as a legacy artifact.
|
| 136 |
+
|
| 137 |
+
## References
|
| 138 |
+
|
| 139 |
+
- Lin et al., [Language models of protein sequences at the scale of evolution](https://doi.org/10.1101/2022.07.20.500902)
|
| 140 |
+
- Chithrananda et al., [ChemBERTa](https://arxiv.org/abs/2010.09885)
|
| 141 |
+
- Steinegger and Söding, [MMseqs2](https://doi.org/10.1038/nbt.3988)
|
model.py
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
-
Copy this file to load the model for inference.
|
| 4 |
-
"""
|
| 5 |
import torch
|
| 6 |
import torch.nn as nn
|
| 7 |
-
from transformers import EsmModel,
|
|
|
|
| 8 |
|
| 9 |
class MitoInteract(nn.Module):
|
| 10 |
def __init__(
|
|
@@ -25,23 +30,47 @@ class MitoInteract(nn.Module):
|
|
| 25 |
self.mol_encoder = AutoModel.from_pretrained(mol_model_name)
|
| 26 |
self.mol_dim = mol_dim
|
| 27 |
if freeze_encoders:
|
| 28 |
-
for p in self.esm.parameters():
|
| 29 |
-
|
|
|
|
|
|
|
| 30 |
self.prot_proj = nn.Sequential(
|
| 31 |
-
nn.Linear(protein_dim, proj_dim),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
self.mol_proj = nn.Sequential(
|
| 33 |
-
nn.Linear(mol_dim, proj_dim),
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
self.ln_mol2prot = nn.LayerNorm(proj_dim)
|
| 37 |
self.ln_prot2mol = nn.LayerNorm(proj_dim)
|
| 38 |
fused_dim = proj_dim * 2
|
| 39 |
self.mlp = nn.Sequential(
|
| 40 |
-
nn.Linear(fused_dim, 512),
|
| 41 |
-
nn.
|
| 42 |
-
nn.
|
| 43 |
-
nn.
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
def encode_protein(self, input_ids, attention_mask):
|
| 46 |
ctx = torch.no_grad() if self.freeze_encoders else torch.enable_grad()
|
| 47 |
with ctx:
|
|
@@ -49,32 +78,44 @@ class MitoInteract(nn.Module):
|
|
| 49 |
mask = attention_mask.unsqueeze(-1).float()
|
| 50 |
pooled = (out.last_hidden_state * mask).sum(1) / mask.sum(1).clamp(min=1e-9)
|
| 51 |
return pooled, out.last_hidden_state
|
| 52 |
-
|
| 53 |
def encode_molecule(self, input_ids, attention_mask):
|
| 54 |
ctx = torch.no_grad() if self.freeze_encoders else torch.enable_grad()
|
| 55 |
with ctx:
|
| 56 |
out = self.mol_encoder(input_ids=input_ids, attention_mask=attention_mask)
|
| 57 |
return out.pooler_output, out.last_hidden_state
|
| 58 |
-
|
| 59 |
-
def forward(
|
|
|
|
|
|
|
| 60 |
prot_pooled, prot_seq = self.encode_protein(prot_input_ids, prot_attention_mask)
|
| 61 |
mol_pooled, mol_seq = self.encode_molecule(mol_input_ids, mol_attention_mask)
|
| 62 |
prot_seq_proj = self.prot_proj(prot_seq)
|
| 63 |
mol_seq_proj = self.mol_proj(mol_seq)
|
| 64 |
prot_q = self.prot_proj(prot_pooled).unsqueeze(1)
|
| 65 |
mol_q = self.mol_proj(mol_pooled).unsqueeze(1)
|
| 66 |
-
prot_pad_mask =
|
| 67 |
-
mol_pad_mask =
|
| 68 |
-
h_prot2mol, _ = self.cross_attn_prot2mol(
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
h_prot2mol = self.ln_prot2mol(h_prot2mol.squeeze(1))
|
| 71 |
h_mol2prot = self.ln_mol2prot(h_mol2prot.squeeze(1))
|
| 72 |
fused = torch.cat([h_prot2mol, h_mol2prot], dim=-1)
|
| 73 |
return self.mlp(fused).squeeze(-1)
|
| 74 |
|
| 75 |
|
| 76 |
-
def load_model(checkpoint_path, device="cpu"):
|
| 77 |
-
"""Load
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
|
| 79 |
config = checkpoint["config"]
|
| 80 |
model = MitoInteract(
|
|
@@ -92,21 +133,35 @@ def load_model(checkpoint_path, device="cpu"):
|
|
| 92 |
return model, config
|
| 93 |
|
| 94 |
|
| 95 |
-
def predict_binding(
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
model = model.to(device)
|
| 104 |
with torch.no_grad():
|
| 105 |
-
|
| 106 |
-
prot_enc["input_ids"].to(device),
|
| 107 |
-
|
|
|
|
|
|
|
| 108 |
)
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
| 1 |
+
"""Deprecated MitoInteract v1 architecture retained for artifact forensics.
|
| 2 |
+
|
| 3 |
+
The v1 checkpoint failed its evaluation gates and uses unsafe pickle
|
| 4 |
+
serialization. New work lives under ``recovery/``. Loading and inference are
|
| 5 |
+
disabled by default so this module cannot silently present an undocumented
|
| 6 |
+
legacy score as pKd.
|
| 7 |
"""
|
| 8 |
+
|
|
|
|
|
|
|
| 9 |
import torch
|
| 10 |
import torch.nn as nn
|
| 11 |
+
from transformers import EsmModel, AutoModel, AutoTokenizer
|
| 12 |
+
|
| 13 |
|
| 14 |
class MitoInteract(nn.Module):
|
| 15 |
def __init__(
|
|
|
|
| 30 |
self.mol_encoder = AutoModel.from_pretrained(mol_model_name)
|
| 31 |
self.mol_dim = mol_dim
|
| 32 |
if freeze_encoders:
|
| 33 |
+
for p in self.esm.parameters():
|
| 34 |
+
p.requires_grad = False
|
| 35 |
+
for p in self.mol_encoder.parameters():
|
| 36 |
+
p.requires_grad = False
|
| 37 |
self.prot_proj = nn.Sequential(
|
| 38 |
+
nn.Linear(protein_dim, proj_dim),
|
| 39 |
+
nn.LayerNorm(proj_dim),
|
| 40 |
+
nn.ReLU(),
|
| 41 |
+
nn.Dropout(dropout),
|
| 42 |
+
)
|
| 43 |
self.mol_proj = nn.Sequential(
|
| 44 |
+
nn.Linear(mol_dim, proj_dim),
|
| 45 |
+
nn.LayerNorm(proj_dim),
|
| 46 |
+
nn.ReLU(),
|
| 47 |
+
nn.Dropout(dropout),
|
| 48 |
+
)
|
| 49 |
+
self.cross_attn_mol2prot = nn.MultiheadAttention(
|
| 50 |
+
proj_dim, n_heads, dropout=dropout, batch_first=True
|
| 51 |
+
)
|
| 52 |
+
self.cross_attn_prot2mol = nn.MultiheadAttention(
|
| 53 |
+
proj_dim, n_heads, dropout=dropout, batch_first=True
|
| 54 |
+
)
|
| 55 |
self.ln_mol2prot = nn.LayerNorm(proj_dim)
|
| 56 |
self.ln_prot2mol = nn.LayerNorm(proj_dim)
|
| 57 |
fused_dim = proj_dim * 2
|
| 58 |
self.mlp = nn.Sequential(
|
| 59 |
+
nn.Linear(fused_dim, 512),
|
| 60 |
+
nn.BatchNorm1d(512),
|
| 61 |
+
nn.ReLU(),
|
| 62 |
+
nn.Dropout(dropout),
|
| 63 |
+
nn.Linear(512, 256),
|
| 64 |
+
nn.BatchNorm1d(256),
|
| 65 |
+
nn.ReLU(),
|
| 66 |
+
nn.Dropout(dropout),
|
| 67 |
+
nn.Linear(256, 128),
|
| 68 |
+
nn.BatchNorm1d(128),
|
| 69 |
+
nn.ReLU(),
|
| 70 |
+
nn.Dropout(dropout),
|
| 71 |
+
nn.Linear(128, 1),
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
def encode_protein(self, input_ids, attention_mask):
|
| 75 |
ctx = torch.no_grad() if self.freeze_encoders else torch.enable_grad()
|
| 76 |
with ctx:
|
|
|
|
| 78 |
mask = attention_mask.unsqueeze(-1).float()
|
| 79 |
pooled = (out.last_hidden_state * mask).sum(1) / mask.sum(1).clamp(min=1e-9)
|
| 80 |
return pooled, out.last_hidden_state
|
| 81 |
+
|
| 82 |
def encode_molecule(self, input_ids, attention_mask):
|
| 83 |
ctx = torch.no_grad() if self.freeze_encoders else torch.enable_grad()
|
| 84 |
with ctx:
|
| 85 |
out = self.mol_encoder(input_ids=input_ids, attention_mask=attention_mask)
|
| 86 |
return out.pooler_output, out.last_hidden_state
|
| 87 |
+
|
| 88 |
+
def forward(
|
| 89 |
+
self, prot_input_ids, prot_attention_mask, mol_input_ids, mol_attention_mask
|
| 90 |
+
):
|
| 91 |
prot_pooled, prot_seq = self.encode_protein(prot_input_ids, prot_attention_mask)
|
| 92 |
mol_pooled, mol_seq = self.encode_molecule(mol_input_ids, mol_attention_mask)
|
| 93 |
prot_seq_proj = self.prot_proj(prot_seq)
|
| 94 |
mol_seq_proj = self.mol_proj(mol_seq)
|
| 95 |
prot_q = self.prot_proj(prot_pooled).unsqueeze(1)
|
| 96 |
mol_q = self.mol_proj(mol_pooled).unsqueeze(1)
|
| 97 |
+
prot_pad_mask = prot_attention_mask == 0
|
| 98 |
+
mol_pad_mask = mol_attention_mask == 0
|
| 99 |
+
h_prot2mol, _ = self.cross_attn_prot2mol(
|
| 100 |
+
prot_q, mol_seq_proj, mol_seq_proj, key_padding_mask=mol_pad_mask
|
| 101 |
+
)
|
| 102 |
+
h_mol2prot, _ = self.cross_attn_mol2prot(
|
| 103 |
+
mol_q, prot_seq_proj, prot_seq_proj, key_padding_mask=prot_pad_mask
|
| 104 |
+
)
|
| 105 |
h_prot2mol = self.ln_prot2mol(h_prot2mol.squeeze(1))
|
| 106 |
h_mol2prot = self.ln_mol2prot(h_mol2prot.squeeze(1))
|
| 107 |
fused = torch.cat([h_prot2mol, h_mol2prot], dim=-1)
|
| 108 |
return self.mlp(fused).squeeze(-1)
|
| 109 |
|
| 110 |
|
| 111 |
+
def load_model(checkpoint_path, device="cpu", *, allow_unsafe_legacy=False):
|
| 112 |
+
"""Load the failed v1 pickle only after explicit risk acknowledgement."""
|
| 113 |
+
if not allow_unsafe_legacy:
|
| 114 |
+
raise RuntimeError(
|
| 115 |
+
"MitoInteract v1 is deprecated and full_model.pt requires unsafe pickle "
|
| 116 |
+
"deserialization. See AUDIT.md. Pass allow_unsafe_legacy=True only for "
|
| 117 |
+
"controlled artifact forensics, never for untrusted files."
|
| 118 |
+
)
|
| 119 |
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
|
| 120 |
config = checkpoint["config"]
|
| 121 |
model = MitoInteract(
|
|
|
|
| 133 |
return model, config
|
| 134 |
|
| 135 |
|
| 136 |
+
def predict_binding(
|
| 137 |
+
model, protein_seq, smiles, device="cpu", *, allow_invalid_legacy_output=False
|
| 138 |
+
):
|
| 139 |
+
"""Return the undocumented v1 score only after explicit acknowledgement."""
|
| 140 |
+
if not allow_invalid_legacy_output:
|
| 141 |
+
raise RuntimeError(
|
| 142 |
+
"v1 output has undocumented target semantics and failed validation. "
|
| 143 |
+
"It cannot be interpreted as pKd or converted to Kd. See AUDIT.md."
|
| 144 |
+
)
|
| 145 |
+
prot_tokenizer = AutoTokenizer.from_pretrained(model.esm.name_or_path)
|
| 146 |
+
mol_tokenizer = AutoTokenizer.from_pretrained(model.mol_encoder.name_or_path)
|
| 147 |
+
|
| 148 |
+
prot_enc = prot_tokenizer(
|
| 149 |
+
protein_seq, return_tensors="pt", padding=True, truncation=True, max_length=512
|
| 150 |
+
)
|
| 151 |
+
mol_enc = mol_tokenizer(
|
| 152 |
+
smiles, return_tensors="pt", padding=True, truncation=True, max_length=200
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
model = model.to(device)
|
| 156 |
with torch.no_grad():
|
| 157 |
+
legacy_score = model(
|
| 158 |
+
prot_enc["input_ids"].to(device),
|
| 159 |
+
prot_enc["attention_mask"].to(device),
|
| 160 |
+
mol_enc["input_ids"].to(device),
|
| 161 |
+
mol_enc["attention_mask"].to(device),
|
| 162 |
)
|
| 163 |
+
|
| 164 |
+
return {
|
| 165 |
+
"legacy_score": legacy_score.item(),
|
| 166 |
+
"warning": "Do not interpret this value as pKd, Kd, or validated affinity.",
|
| 167 |
+
}
|
recovery/DATA_PROVENANCE.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dataset target provenance
|
| 2 |
+
|
| 3 |
+
## Pinned source
|
| 4 |
+
|
| 5 |
+
- Dataset: `jglaser/binding_affinity`
|
| 6 |
+
- Revision: `11e49b7ece33d62afd7f65bc05ce60ad37f9ba7b`
|
| 7 |
+
- File: `data/all.parquet`
|
| 8 |
+
- SHA-256: `d50f4cae794298f32c39f75a9e8603528d20e50e4fa757025def412f5b2a78d8`
|
| 9 |
+
|
| 10 |
+
## Why the target is pAffinity, not pKd
|
| 11 |
+
|
| 12 |
+
The combined Parquet exposes `affinity_uM` and `neg_log10_affinity_M`, but it does not retain the measurement type or source row.
|
| 13 |
+
|
| 14 |
+
The pinned `bindingdb_single.ipynb` selects:
|
| 15 |
+
|
| 16 |
+
```python
|
| 17 |
+
['IC50 (nM)', 'Ki (nM)', 'Kd (nM)', 'EC50 (nM)']
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
Its `to_uM` function converts every available field to micromolar, appends the values to one list, and returns:
|
| 21 |
+
|
| 22 |
+
```python
|
| 23 |
+
np.mean(vals[~np.isnan(vals)])
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
It stores the result in one column:
|
| 27 |
+
|
| 28 |
+
```python
|
| 29 |
+
df_affinity['affinity_uM'] = df_affinity[
|
| 30 |
+
['IC50 (nM)', 'Ki (nM)', 'Kd (nM)', 'EC50 (nM)']
|
| 31 |
+
].parallel_apply(to_uM, axis=1)
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
The pinned `combine_dbs.ipynb` then keeps only `seq`, `smiles`, and `affinity_uM` from each source and computes:
|
| 35 |
+
|
| 36 |
+
```python
|
| 37 |
+
df_nr['neg_log10_affinity_M'] = 6 - np.log(df_nr['affinity_uM']) / np.log(10)
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
Therefore:
|
| 41 |
+
|
| 42 |
+
- the numeric transform is `-log10(affinity in M)`;
|
| 43 |
+
- not every row is a dissociation constant;
|
| 44 |
+
- some BindingDB rows may average heterogeneous measurement types before the logarithm;
|
| 45 |
+
- the final file cannot separate `Ki`, `Kd`, `IC50`, or `EC50`;
|
| 46 |
+
- the final file cannot reconstruct assay conditions, censoring operators, or original source provenance.
|
| 47 |
+
|
| 48 |
+
MitoInteract recovery calls this value **pAffinity**. It must not be presented as pKd or converted to a physical Kd.
|
| 49 |
+
|
| 50 |
+
## Consequence for v2
|
| 51 |
+
|
| 52 |
+
The current 10,000-pair experiment is useful for pipeline recovery and leakage analysis. It is not sufficient for a scientifically defensible affinity predictor. A successor dataset should retain at least:
|
| 53 |
+
|
| 54 |
+
- measurement type;
|
| 55 |
+
- numeric value, unit, and relation operator (`=`, `<`, `>`, etc.);
|
| 56 |
+
- assay and source identifiers;
|
| 57 |
+
- protein and ligand identifiers;
|
| 58 |
+
- replicate measurements before aggregation;
|
| 59 |
+
- source database and version.
|
| 60 |
+
|
| 61 |
+
A clean v2 training target should either model one measurement type at a time or use a multi-task/censoring-aware objective that keeps these distinctions explicit.
|
recovery/README.md
CHANGED
|
@@ -1,24 +1,171 @@
|
|
| 1 |
-
# MitoInteract v2 Recovery
|
| 2 |
|
| 3 |
-
This
|
| 4 |
|
| 5 |
-
##
|
| 6 |
|
| 7 |
```bash
|
| 8 |
-
cd
|
| 9 |
uv sync --dev
|
| 10 |
uv run pytest -q
|
| 11 |
-
uv run python scripts/dataset_smoke.py
|
|
|
|
|
|
|
| 12 |
```
|
| 13 |
|
| 14 |
-
The
|
| 15 |
|
| 16 |
-
##
|
| 17 |
|
| 18 |
-
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MitoInteract v2 Recovery
|
| 2 |
|
| 3 |
+
This package contains the reproducible data, baseline, embedding, and training pipeline used to recover MitoInteract from the failed v1 checkpoint.
|
| 4 |
|
| 5 |
+
## 1. Install and test
|
| 6 |
|
| 7 |
```bash
|
| 8 |
+
cd recovery
|
| 9 |
uv sync --dev
|
| 10 |
uv run pytest -q
|
| 11 |
+
uv run python scripts/dataset_smoke.py \
|
| 12 |
+
--rows 100 \
|
| 13 |
+
--output artifacts/dataset-smoke.json
|
| 14 |
```
|
| 15 |
|
| 16 |
+
The Dataset Viewer smoke test verifies that `neg_log10_affinity_M` agrees numerically with `affinity_uM`. It is a plumbing check, not a performance result.
|
| 17 |
|
| 18 |
+
## Target semantics
|
| 19 |
|
| 20 |
+
The target is generic **pAffinity**, not pKd:
|
| 21 |
|
| 22 |
+
```text
|
| 23 |
+
pAffinity = -log10(dataset affinity in molar units)
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
The pinned source preprocessing combines heterogeneous measurements. In particular, its BindingDB notebook collects `Ki`, `Kd`, `IC50`, and `EC50` and averages the available values in linear micromolar units. These quantities are not interchangeable. The recovery pipeline preserves the dataset's numeric target so the failed run can be audited, but it does not relabel the target as a dissociation constant. See [`DATA_PROVENANCE.md`](DATA_PROVENANCE.md) for the pinned code evidence. A scientifically stronger successor dataset must retain measurement type, assay provenance, relation operators/censoring, and source identifiers.
|
| 27 |
+
|
| 28 |
+
## 2. Download the pinned source data
|
| 29 |
+
|
| 30 |
+
The recovery configuration pins `jglaser/binding_affinity` to commit:
|
| 31 |
+
|
| 32 |
+
```text
|
| 33 |
+
11e49b7ece33d62afd7f65bc05ce60ad37f9ba7b
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
Download `data/all.parquet` at that revision and verify its SHA-256:
|
| 37 |
+
|
| 38 |
+
```text
|
| 39 |
+
d50f4cae794298f32c39f75a9e8603528d20e50e4fa757025def412f5b2a78d8
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
For example:
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
hf download jglaser/binding_affinity data/all.parquet \
|
| 46 |
+
--repo-type dataset \
|
| 47 |
+
--revision 11e49b7ece33d62afd7f65bc05ce60ad37f9ba7b \
|
| 48 |
+
--local-dir .cache/binding-affinity
|
| 49 |
+
|
| 50 |
+
sha256sum .cache/binding-affinity/data/all.parquet
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## 3. Build the fixed development sample
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
uv run python scripts/prepare_sample.py \
|
| 57 |
+
--parquet .cache/binding-affinity/data/all.parquet \
|
| 58 |
+
--dataset-revision 11e49b7ece33d62afd7f65bc05ce60ad37f9ba7b \
|
| 59 |
+
--sample-size 10000 \
|
| 60 |
+
--candidate-multiplier 3 \
|
| 61 |
+
--seed 42 \
|
| 62 |
+
--output-dir artifacts/dev-10k
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
The script:
|
| 66 |
+
|
| 67 |
+
- samples source row indexes deterministically;
|
| 68 |
+
- validates the pAffinity/µM conversion;
|
| 69 |
+
- canonicalizes SMILES;
|
| 70 |
+
- aggregates duplicate protein-ligand measurements by median pAffinity;
|
| 71 |
+
- emits pair IDs and entity IDs derived from content hashes;
|
| 72 |
+
- writes 8,000/1,000/1,000 manifests for random-pair, exact-protein, and scaffold-disjoint splits;
|
| 73 |
+
- records row counts, target distributions, sequence lengths, and overlap checks.
|
| 74 |
+
|
| 75 |
+
Generated data stays under `artifacts/` and is ignored by Git.
|
| 76 |
+
|
| 77 |
+
## 4. Add the protein-similarity split
|
| 78 |
+
|
| 79 |
+
Install MMseqs2 through the operating system package manager, then run:
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
uv run python scripts/cluster_proteins.py \
|
| 83 |
+
--data-dir artifacts/dev-10k \
|
| 84 |
+
--min-seq-id 0.5 \
|
| 85 |
+
--coverage 0.8 \
|
| 86 |
+
--seed 42
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
The resulting split keeps MMseqs2 clusters disjoint across train, validation, and test. The clustering requires at least 50% actual sequence identity over 80% of both sequences.
|
| 90 |
+
|
| 91 |
+
## 5. Run non-neural baselines
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
uv run python scripts/run_baselines.py \
|
| 95 |
+
--data-dir artifacts/dev-10k \
|
| 96 |
+
--output artifacts/dev-10k/baselines.json
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
The benchmark reports train-mean, protein-only, ligand-only, and combined character n-gram ridge models. It selects ridge regularization on validation data and reports RMSE, MAE, Pearson, Spearman, and R² on the untouched test split.
|
| 100 |
+
|
| 101 |
+
These shortcut-sensitive baselines must be run before a transformer model. A neural model that cannot beat them has not justified its cost.
|
| 102 |
+
|
| 103 |
+
## 6. Verify the v2 trainable head
|
| 104 |
+
|
| 105 |
+
The default environment keeps PyTorch optional. Install a CPU build for local checks:
|
| 106 |
+
|
| 107 |
+
```bash
|
| 108 |
+
uv pip install --python .venv/bin/python torch \
|
| 109 |
+
--index-url https://download.pytorch.org/whl/cpu
|
| 110 |
+
uv pip install --python .venv/bin/python transformers safetensors
|
| 111 |
+
|
| 112 |
+
.venv/bin/python -m pytest -q
|
| 113 |
+
.venv/bin/python scripts/overfit_sanity.py \
|
| 114 |
+
--sample artifacts/dev-10k/sample.jsonl \
|
| 115 |
+
--examples 32 \
|
| 116 |
+
--max-rmse 0.05 \
|
| 117 |
+
--output artifacts/overfit-sanity.json
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
The overfit check isolates the trainable fusion/head path. Passing it does not establish generalization.
|
| 121 |
+
|
| 122 |
+
## 7. Cache pinned encoder embeddings
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
.venv/bin/python scripts/embed_sample.py \
|
| 126 |
+
--sample artifacts/dev-10k/sample.jsonl \
|
| 127 |
+
--batch-size 8 \
|
| 128 |
+
--device auto \
|
| 129 |
+
--output artifacts/embeddings-10k.npz
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
Defaults are pinned to:
|
| 133 |
+
|
| 134 |
+
- `facebook/esm2_t12_35M_UR50D@6fbf070e65b0b7291e7bbcd451118c216cff79d8`
|
| 135 |
+
- `DeepChem/ChemBERTa-77M-MLM@ed8a5374f2024ec8da53760af91a33fb8f6a15ff`
|
| 136 |
+
|
| 137 |
+
Protein sequences longer than 1,022 residues are chunked, mean-pooled per chunk, and combined with length weighting. Embeddings are cached so the frozen encoders do not run again for each split or head experiment.
|
| 138 |
+
|
| 139 |
+
## 8. Train the v2 head
|
| 140 |
+
|
| 141 |
+
```bash
|
| 142 |
+
.venv/bin/python scripts/train_head.py \
|
| 143 |
+
--embeddings artifacts/embeddings-10k.npz \
|
| 144 |
+
--manifest artifacts/dev-10k/split-protein_similarity_50.jsonl \
|
| 145 |
+
--output-dir artifacts/v2-head-protein-similarity \
|
| 146 |
+
--epochs 100 \
|
| 147 |
+
--batch-size 256 \
|
| 148 |
+
--patience 10 \
|
| 149 |
+
--device auto
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
The trainer:
|
| 153 |
+
|
| 154 |
+
- standardizes pAffinity using training data only;
|
| 155 |
+
- uses AdamW, gradient clipping, 5% warmup, and cosine decay;
|
| 156 |
+
- selects the checkpoint by validation RMSE;
|
| 157 |
+
- saves only trainable weights in `model.safetensors`;
|
| 158 |
+
- records target scaling, encoder revisions, split counts, optimizer settings, history, and test metrics.
|
| 159 |
+
|
| 160 |
+
## Acceptance gates
|
| 161 |
+
|
| 162 |
+
A v2 checkpoint is not releasable unless all of these pass:
|
| 163 |
|
| 164 |
+
1. Tests and pAffinity conversion checks pass.
|
| 165 |
+
2. The 32-pair overfit sanity reaches RMSE ≤ 0.05 pAffinity.
|
| 166 |
+
3. Validation and test each contain at least 1,000 pairs.
|
| 167 |
+
4. The published primary result uses the MMseqs2 protein-similarity split, not only a random split.
|
| 168 |
+
5. Test RMSE beats the train mean and both Pearson and Spearman are positive.
|
| 169 |
+
6. The neural model beats or materially complements the combined character ridge baseline.
|
| 170 |
+
7. Dataset, encoder, split, seed, predictions, metrics, and weights are reproducible.
|
| 171 |
+
8. Model-card language remains computational and does not imply experimental or clinical validity.
|
recovery/cloud/modal_embed.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run the 10k frozen-encoder pass on a temporary Modal L4.
|
| 2 |
+
|
| 3 |
+
This script is intentionally not run by tests. Build the sample locally first,
|
| 4 |
+
review the current Modal price, then invoke it with:
|
| 5 |
+
|
| 6 |
+
modal run cloud/modal_embed.py
|
| 7 |
+
|
| 8 |
+
Retrieve the result with the `modal volume get` command printed on completion.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
import subprocess
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
import modal
|
| 19 |
+
|
| 20 |
+
app = modal.App("mitointeract-v2-embeddings")
|
| 21 |
+
output_volume = modal.Volume.from_name(
|
| 22 |
+
"mitointeract-v2-artifacts", create_if_missing=True
|
| 23 |
+
)
|
| 24 |
+
cache_volume = modal.Volume.from_name("mitointeract-hf-cache", create_if_missing=True)
|
| 25 |
+
|
| 26 |
+
recovery_dir = Path(__file__).parents[1]
|
| 27 |
+
image = (
|
| 28 |
+
modal.Image.debian_slim(python_version="3.12")
|
| 29 |
+
.pip_install(
|
| 30 |
+
"numpy>=2.0",
|
| 31 |
+
"safetensors>=0.5",
|
| 32 |
+
"torch>=2.4",
|
| 33 |
+
"transformers>=4.45",
|
| 34 |
+
)
|
| 35 |
+
.add_local_dir(recovery_dir / "src", remote_path="/app/src")
|
| 36 |
+
.add_local_file(
|
| 37 |
+
recovery_dir / "scripts/embed_sample.py", remote_path="/app/embed_sample.py"
|
| 38 |
+
)
|
| 39 |
+
.add_local_file(
|
| 40 |
+
recovery_dir / "artifacts/dev-10k/sample.jsonl",
|
| 41 |
+
remote_path="/input/sample.jsonl",
|
| 42 |
+
)
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@app.function(
|
| 47 |
+
image=image,
|
| 48 |
+
gpu="L4",
|
| 49 |
+
cpu=4,
|
| 50 |
+
memory=16_384,
|
| 51 |
+
timeout=3_600,
|
| 52 |
+
volumes={"/output": output_volume, "/cache": cache_volume},
|
| 53 |
+
)
|
| 54 |
+
def embed() -> dict:
|
| 55 |
+
env = os.environ.copy()
|
| 56 |
+
env["PYTHONPATH"] = "/app/src"
|
| 57 |
+
env["HF_HOME"] = "/cache/huggingface"
|
| 58 |
+
subprocess.run(
|
| 59 |
+
[
|
| 60 |
+
"python",
|
| 61 |
+
"/app/embed_sample.py",
|
| 62 |
+
"--sample",
|
| 63 |
+
"/input/sample.jsonl",
|
| 64 |
+
"--batch-size",
|
| 65 |
+
"32",
|
| 66 |
+
"--device",
|
| 67 |
+
"cuda",
|
| 68 |
+
"--output",
|
| 69 |
+
"/output/embeddings-10k.npz",
|
| 70 |
+
],
|
| 71 |
+
check=True,
|
| 72 |
+
env=env,
|
| 73 |
+
)
|
| 74 |
+
output_volume.commit()
|
| 75 |
+
cache_volume.commit()
|
| 76 |
+
metadata = json.loads(Path("/output/embeddings-10k.json").read_text())
|
| 77 |
+
metadata["output_bytes"] = Path("/output/embeddings-10k.npz").stat().st_size
|
| 78 |
+
return metadata
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@app.local_entrypoint()
|
| 82 |
+
def main() -> None:
|
| 83 |
+
result = embed.remote()
|
| 84 |
+
print(json.dumps(result, indent=2))
|
| 85 |
+
print(
|
| 86 |
+
"modal volume get mitointeract-v2-artifacts "
|
| 87 |
+
"embeddings-10k.npz artifacts/embeddings-10k.npz"
|
| 88 |
+
)
|
| 89 |
+
print(
|
| 90 |
+
"modal volume get mitointeract-v2-artifacts "
|
| 91 |
+
"embeddings-10k.json artifacts/embeddings-10k.json"
|
| 92 |
+
)
|
recovery/config.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
{
|
| 2 |
"dataset": "jglaser/binding_affinity",
|
| 3 |
-
"dataset_revision": "
|
| 4 |
"target_column": "neg_log10_affinity_M",
|
| 5 |
-
"target_unit": "
|
| 6 |
"sample_size": 10000,
|
| 7 |
"seed": 42,
|
| 8 |
"splits": ["random_pair", "cold_protein", "cold_ligand_scaffold"],
|
|
@@ -11,8 +11,12 @@
|
|
| 11 |
"minimum_test_rows": 1000,
|
| 12 |
"model": {
|
| 13 |
"protein_encoder": "facebook/esm2_t12_35M_UR50D",
|
| 14 |
-
"
|
| 15 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
},
|
| 17 |
"acceptance": {
|
| 18 |
"beat_train_mean_rmse": true,
|
|
|
|
| 1 |
{
|
| 2 |
"dataset": "jglaser/binding_affinity",
|
| 3 |
+
"dataset_revision": "11e49b7ece33d62afd7f65bc05ce60ad37f9ba7b",
|
| 4 |
"target_column": "neg_log10_affinity_M",
|
| 5 |
+
"target_unit": "pAffinity",
|
| 6 |
"sample_size": 10000,
|
| 7 |
"seed": 42,
|
| 8 |
"splits": ["random_pair", "cold_protein", "cold_ligand_scaffold"],
|
|
|
|
| 11 |
"minimum_test_rows": 1000,
|
| 12 |
"model": {
|
| 13 |
"protein_encoder": "facebook/esm2_t12_35M_UR50D",
|
| 14 |
+
"protein_revision": "6fbf070e65b0b7291e7bbcd451118c216cff79d8",
|
| 15 |
+
"molecule_encoder": "DeepChem/ChemBERTa-77M-MLM",
|
| 16 |
+
"molecule_revision": "ed8a5374f2024ec8da53760af91a33fb8f6a15ff",
|
| 17 |
+
"freeze_encoders": true,
|
| 18 |
+
"protein_long_sequence_strategy": "length_weighted_chunk_mean",
|
| 19 |
+
"target": "standardized_pAffinity"
|
| 20 |
},
|
| 21 |
"acceptance": {
|
| 22 |
"beat_train_mean_rmse": true,
|
recovery/pyproject.toml
CHANGED
|
@@ -3,7 +3,20 @@ name = "mitointeract-recovery"
|
|
| 3 |
version = "0.1.0"
|
| 4 |
description = "Reproducibility and target-semantics checks for MitoInteract v2"
|
| 5 |
requires-python = ">=3.11"
|
| 6 |
-
dependencies = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
[build-system]
|
| 9 |
requires = ["setuptools>=64"]
|
|
@@ -16,7 +29,10 @@ where = ["src"]
|
|
| 16 |
package = true
|
| 17 |
|
| 18 |
[dependency-groups]
|
| 19 |
-
dev = [
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
[tool.pytest.ini_options]
|
| 22 |
pythonpath = ["src"]
|
|
|
|
| 3 |
version = "0.1.0"
|
| 4 |
description = "Reproducibility and target-semantics checks for MitoInteract v2"
|
| 5 |
requires-python = ">=3.11"
|
| 6 |
+
dependencies = [
|
| 7 |
+
"numpy>=2.0",
|
| 8 |
+
"pyarrow>=18",
|
| 9 |
+
"rdkit>=2025.3",
|
| 10 |
+
"scikit-learn>=1.6",
|
| 11 |
+
"scipy>=1.14",
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
[project.optional-dependencies]
|
| 15 |
+
model = [
|
| 16 |
+
"safetensors>=0.5",
|
| 17 |
+
"torch>=2.4",
|
| 18 |
+
"transformers>=4.45",
|
| 19 |
+
]
|
| 20 |
|
| 21 |
[build-system]
|
| 22 |
requires = ["setuptools>=64"]
|
|
|
|
| 29 |
package = true
|
| 30 |
|
| 31 |
[dependency-groups]
|
| 32 |
+
dev = [
|
| 33 |
+
"pytest>=9",
|
| 34 |
+
"ruff>=0.14",
|
| 35 |
+
]
|
| 36 |
|
| 37 |
[tool.pytest.ini_options]
|
| 38 |
pythonpath = ["src"]
|
recovery/reports/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Checked recovery reports
|
| 2 |
+
|
| 3 |
+
These JSON files are small, reviewable outputs from the deterministic recovery pipeline. Large source data, split manifests, embeddings, predictions, and model weights remain under the ignored `artifacts/` directory.
|
| 4 |
+
|
| 5 |
+
- `dev-10k-audit.json`: immutable source revision and hash, sample statistics, split audits, and hashes for reproducing the sample and manifests.
|
| 6 |
+
- `dev-10k-baselines.json`: validation selection and untouched test metrics for every cheap baseline and split.
|
| 7 |
+
- `overfit-actual-embeddings.json`: 32-pair optimization sanity check using the pinned frozen encoders.
|
| 8 |
+
|
| 9 |
+
The checked reports support model-card statements but are not an external benchmark or a releasable neural checkpoint.
|
recovery/reports/dev-10k-audit.json
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"source_parquet": "data/all.parquet",
|
| 3 |
+
"source_sha256": "d50f4cae794298f32c39f75a9e8603528d20e50e4fa757025def412f5b2a78d8",
|
| 4 |
+
"dataset": "jglaser/binding_affinity",
|
| 5 |
+
"dataset_revision": "11e49b7ece33d62afd7f65bc05ce60ad37f9ba7b",
|
| 6 |
+
"source_rows": 1836729,
|
| 7 |
+
"candidate_rows": 30000,
|
| 8 |
+
"sample_rows": 10000,
|
| 9 |
+
"seed": 42,
|
| 10 |
+
"target": {
|
| 11 |
+
"column": "neg_log10_affinity_M",
|
| 12 |
+
"unit": "pAffinity"
|
| 13 |
+
},
|
| 14 |
+
"rejected": {},
|
| 15 |
+
"unique_proteins": 2255,
|
| 16 |
+
"unique_ligands": 9897,
|
| 17 |
+
"unique_scaffold_groups": 8463,
|
| 18 |
+
"duplicate_measurements_in_sample": 51,
|
| 19 |
+
"protein_length": {
|
| 20 |
+
"min": 19,
|
| 21 |
+
"median": 479.0,
|
| 22 |
+
"max": 7096,
|
| 23 |
+
"over_512": 4318
|
| 24 |
+
},
|
| 25 |
+
"splits": {
|
| 26 |
+
"random_pair": {
|
| 27 |
+
"train": {
|
| 28 |
+
"rows": 8000,
|
| 29 |
+
"mean_paffinity": 6.5244491582587365,
|
| 30 |
+
"std_paffinity": 1.5626149739941089,
|
| 31 |
+
"min_paffinity": 8.881784197001252e-16,
|
| 32 |
+
"max_paffinity": 14.0
|
| 33 |
+
},
|
| 34 |
+
"validation": {
|
| 35 |
+
"rows": 1000,
|
| 36 |
+
"mean_paffinity": 6.507777023136616,
|
| 37 |
+
"std_paffinity": 1.5282972208815573,
|
| 38 |
+
"min_paffinity": 0.7708500027656555,
|
| 39 |
+
"max_paffinity": 10.74472713470459
|
| 40 |
+
},
|
| 41 |
+
"test": {
|
| 42 |
+
"rows": 1000,
|
| 43 |
+
"mean_paffinity": 6.420703814268112,
|
| 44 |
+
"std_paffinity": 1.5623837036360122,
|
| 45 |
+
"min_paffinity": 1.2039999961853027,
|
| 46 |
+
"max_paffinity": 10.95860767364502
|
| 47 |
+
}
|
| 48 |
+
},
|
| 49 |
+
"cold_protein_exact": {
|
| 50 |
+
"train": {
|
| 51 |
+
"rows": 8000,
|
| 52 |
+
"mean_paffinity": 6.476698521107435,
|
| 53 |
+
"std_paffinity": 1.5651385137888398,
|
| 54 |
+
"min_paffinity": 0.6000000238418579,
|
| 55 |
+
"max_paffinity": 14.0
|
| 56 |
+
},
|
| 57 |
+
"validation": {
|
| 58 |
+
"rows": 1000,
|
| 59 |
+
"mean_paffinity": 6.65395347070694,
|
| 60 |
+
"std_paffinity": 1.5705924306056336,
|
| 61 |
+
"min_paffinity": 2.3372421264648438,
|
| 62 |
+
"max_paffinity": 13.0
|
| 63 |
+
},
|
| 64 |
+
"test": {
|
| 65 |
+
"rows": 1000,
|
| 66 |
+
"mean_paffinity": 6.656532463908196,
|
| 67 |
+
"std_paffinity": 1.4850308092775097,
|
| 68 |
+
"min_paffinity": 8.881784197001252e-16,
|
| 69 |
+
"max_paffinity": 12.698969841003418
|
| 70 |
+
}
|
| 71 |
+
},
|
| 72 |
+
"cold_ligand_scaffold": {
|
| 73 |
+
"train": {
|
| 74 |
+
"rows": 8000,
|
| 75 |
+
"mean_paffinity": 6.537638315707445,
|
| 76 |
+
"std_paffinity": 1.5519537903490064,
|
| 77 |
+
"min_paffinity": 0.6000000238418579,
|
| 78 |
+
"max_paffinity": 13.214670181274414
|
| 79 |
+
},
|
| 80 |
+
"validation": {
|
| 81 |
+
"rows": 1000,
|
| 82 |
+
"mean_paffinity": 6.312392803311348,
|
| 83 |
+
"std_paffinity": 1.5805480574681217,
|
| 84 |
+
"min_paffinity": 8.881784197001252e-16,
|
| 85 |
+
"max_paffinity": 13.0
|
| 86 |
+
},
|
| 87 |
+
"test": {
|
| 88 |
+
"rows": 1000,
|
| 89 |
+
"mean_paffinity": 6.5105747745037075,
|
| 90 |
+
"std_paffinity": 1.583959954584063,
|
| 91 |
+
"min_paffinity": 2.0315170288085938,
|
| 92 |
+
"max_paffinity": 14.0
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
},
|
| 96 |
+
"limitations": [
|
| 97 |
+
"the source target can combine Ki, Kd, IC50, and EC50; pAffinity is not pKd",
|
| 98 |
+
"assay type, relation operator, and source provenance are absent from the combined Parquet",
|
| 99 |
+
"cold_protein_exact is entity-disjoint but not sequence-similarity-clustered",
|
| 100 |
+
"scaffold groups use Bemis-Murcko with exact-molecule fallback for acyclic ligands",
|
| 101 |
+
"this development sample is not an external biological benchmark"
|
| 102 |
+
],
|
| 103 |
+
"split_manifests": {
|
| 104 |
+
"protein_similarity_50": "split-protein_similarity_50.jsonl"
|
| 105 |
+
},
|
| 106 |
+
"external_split_audits": {
|
| 107 |
+
"protein_similarity_50": {
|
| 108 |
+
"tool": "MMseqs2",
|
| 109 |
+
"command": [
|
| 110 |
+
"mmseqs",
|
| 111 |
+
"easy-cluster",
|
| 112 |
+
"artifacts/dev-10k/mmseqs-identity-50-coverage-80/proteins.fasta",
|
| 113 |
+
"artifacts/dev-10k/mmseqs-identity-50-coverage-80/clusters",
|
| 114 |
+
"artifacts/dev-10k/mmseqs-identity-50-coverage-80/tmp",
|
| 115 |
+
"--min-seq-id",
|
| 116 |
+
"0.5",
|
| 117 |
+
"-c",
|
| 118 |
+
"0.8",
|
| 119 |
+
"--cov-mode",
|
| 120 |
+
"0",
|
| 121 |
+
"--alignment-mode",
|
| 122 |
+
"3",
|
| 123 |
+
"--threads",
|
| 124 |
+
"8"
|
| 125 |
+
],
|
| 126 |
+
"min_sequence_identity": 0.5,
|
| 127 |
+
"minimum_alignment_coverage": 0.8,
|
| 128 |
+
"coverage_mode": "both query and target",
|
| 129 |
+
"alignment_mode": "actual identical residues / aligned columns",
|
| 130 |
+
"unique_proteins": 2255,
|
| 131 |
+
"clusters": 1421,
|
| 132 |
+
"largest_cluster_proteins": 15,
|
| 133 |
+
"split_rows": {
|
| 134 |
+
"validation": 1000,
|
| 135 |
+
"train": 8000,
|
| 136 |
+
"test": 1000
|
| 137 |
+
},
|
| 138 |
+
"manifest": "split-protein_similarity_50.jsonl",
|
| 139 |
+
"protein_cluster_overlap": 0
|
| 140 |
+
}
|
| 141 |
+
},
|
| 142 |
+
"sample_sha256": "b3f10e03a3882a6b57fc38de5384a639e88494a738dcca45a04196f87790186b",
|
| 143 |
+
"manifest_sha256": {
|
| 144 |
+
"split-random_pair.jsonl": "b316172efa0832e9a9677e33c91f999c32b0c2496c1d1ebcf65887a8ff919635",
|
| 145 |
+
"split-cold_protein_exact.jsonl": "5c71713f1fe2fdf7091ac57a1d5f4027b8e1c90f50af379a45074467a3f1ba53",
|
| 146 |
+
"split-protein_similarity_50.jsonl": "42edbf243966f7f9a1c6a16d86d394a94a5386ee4a96defbb9be7f364deaab78",
|
| 147 |
+
"split-cold_ligand_scaffold.jsonl": "b38a49046902eba950c91f97a5781ace3e95cc9f6252295f026cc895f38557e1"
|
| 148 |
+
}
|
| 149 |
+
}
|
recovery/reports/dev-10k-baselines.json
ADDED
|
@@ -0,0 +1,864 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"sample_rows": 10000,
|
| 3 |
+
"target": "pAffinity",
|
| 4 |
+
"packages": {
|
| 5 |
+
"numpy": "2.5.1",
|
| 6 |
+
"scipy": "1.18.0",
|
| 7 |
+
"scikit-learn": "1.9.0"
|
| 8 |
+
},
|
| 9 |
+
"splits": {
|
| 10 |
+
"cold_ligand_scaffold": {
|
| 11 |
+
"rows": {
|
| 12 |
+
"train": 8000,
|
| 13 |
+
"validation": 1000,
|
| 14 |
+
"test": 1000
|
| 15 |
+
},
|
| 16 |
+
"mean_baseline": {
|
| 17 |
+
"prediction": 6.537638315707445,
|
| 18 |
+
"validation": {
|
| 19 |
+
"rmse": 1.5957348058680887,
|
| 20 |
+
"mae": 1.2895477378728986,
|
| 21 |
+
"pearson_r": null,
|
| 22 |
+
"spearman_r": null,
|
| 23 |
+
"r2": -0.020329719922339562
|
| 24 |
+
},
|
| 25 |
+
"test": {
|
| 26 |
+
"rmse": 1.5833990791492547,
|
| 27 |
+
"mae": 1.3026160468125345,
|
| 28 |
+
"pearson_r": null,
|
| 29 |
+
"spearman_r": null,
|
| 30 |
+
"r2": -0.00029222365027892216
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"feature_dimensions": {
|
| 34 |
+
"protein": 4096,
|
| 35 |
+
"ligand": 4096
|
| 36 |
+
},
|
| 37 |
+
"models": [
|
| 38 |
+
{
|
| 39 |
+
"name": "protein_char3_ridge",
|
| 40 |
+
"selected_alpha": 1.0,
|
| 41 |
+
"validation_trials": [
|
| 42 |
+
{
|
| 43 |
+
"alpha": 0.1,
|
| 44 |
+
"metrics": {
|
| 45 |
+
"rmse": 1.527804068122877,
|
| 46 |
+
"mae": 1.1779963380098344,
|
| 47 |
+
"pearson_r": 0.3896740635545639,
|
| 48 |
+
"spearman_r": 0.3792641691445764,
|
| 49 |
+
"r2": 0.06469248187168464
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"alpha": 1.0,
|
| 54 |
+
"metrics": {
|
| 55 |
+
"rmse": 1.4578139871351854,
|
| 56 |
+
"mae": 1.139968386530876,
|
| 57 |
+
"pearson_r": 0.4151059016140626,
|
| 58 |
+
"spearman_r": 0.3969308305062462,
|
| 59 |
+
"r2": 0.14842416968739125
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"alpha": 10.0,
|
| 64 |
+
"metrics": {
|
| 65 |
+
"rmse": 1.4723908858399237,
|
| 66 |
+
"mae": 1.1793289355039596,
|
| 67 |
+
"pearson_r": 0.39965520038704133,
|
| 68 |
+
"spearman_r": 0.39853528582342707,
|
| 69 |
+
"r2": 0.1313089600423143
|
| 70 |
+
}
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"alpha": 100.0,
|
| 74 |
+
"metrics": {
|
| 75 |
+
"rmse": 1.554020183734794,
|
| 76 |
+
"mae": 1.257076021552086,
|
| 77 |
+
"pearson_r": 0.3325547399758985,
|
| 78 |
+
"spearman_r": 0.3505718847248242,
|
| 79 |
+
"r2": 0.032318560087709836
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
],
|
| 83 |
+
"validation": {
|
| 84 |
+
"rmse": 1.4578139871351854,
|
| 85 |
+
"mae": 1.139968386530876,
|
| 86 |
+
"pearson_r": 0.4151059016140626,
|
| 87 |
+
"spearman_r": 0.3969308305062462,
|
| 88 |
+
"r2": 0.14842416968739125
|
| 89 |
+
},
|
| 90 |
+
"test": {
|
| 91 |
+
"rmse": 1.39215070131138,
|
| 92 |
+
"mae": 1.102342973947525,
|
| 93 |
+
"pearson_r": 0.47756240225675844,
|
| 94 |
+
"spearman_r": 0.4740036884171099,
|
| 95 |
+
"r2": 0.22675233445655052
|
| 96 |
+
},
|
| 97 |
+
"fit_and_eval_seconds": 0.9593746850005118
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"name": "ligand_char2_5_ridge",
|
| 101 |
+
"selected_alpha": 10.0,
|
| 102 |
+
"validation_trials": [
|
| 103 |
+
{
|
| 104 |
+
"alpha": 0.1,
|
| 105 |
+
"metrics": {
|
| 106 |
+
"rmse": 1.5653168764143357,
|
| 107 |
+
"mae": 1.2302509154081345,
|
| 108 |
+
"pearson_r": 0.35459474446304934,
|
| 109 |
+
"spearman_r": 0.32893305055591643,
|
| 110 |
+
"r2": 0.018198624347302683
|
| 111 |
+
}
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"alpha": 1.0,
|
| 115 |
+
"metrics": {
|
| 116 |
+
"rmse": 1.4623783005609468,
|
| 117 |
+
"mae": 1.1591383439302445,
|
| 118 |
+
"pearson_r": 0.4035976657359315,
|
| 119 |
+
"spearman_r": 0.37928879379063246,
|
| 120 |
+
"r2": 0.1430833734386795
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"alpha": 10.0,
|
| 125 |
+
"metrics": {
|
| 126 |
+
"rmse": 1.4539723529408441,
|
| 127 |
+
"mae": 1.157127993941307,
|
| 128 |
+
"pearson_r": 0.4061425224793911,
|
| 129 |
+
"spearman_r": 0.39141890069825885,
|
| 130 |
+
"r2": 0.15290640459726268
|
| 131 |
+
}
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"alpha": 100.0,
|
| 135 |
+
"metrics": {
|
| 136 |
+
"rmse": 1.5244228028633535,
|
| 137 |
+
"mae": 1.22633742749691,
|
| 138 |
+
"pearson_r": 0.3536892569060556,
|
| 139 |
+
"spearman_r": 0.3496830979478171,
|
| 140 |
+
"r2": 0.06882785931877777
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
],
|
| 144 |
+
"validation": {
|
| 145 |
+
"rmse": 1.4539723529408441,
|
| 146 |
+
"mae": 1.157127993941307,
|
| 147 |
+
"pearson_r": 0.4061425224793911,
|
| 148 |
+
"spearman_r": 0.39141890069825885,
|
| 149 |
+
"r2": 0.15290640459726268
|
| 150 |
+
},
|
| 151 |
+
"test": {
|
| 152 |
+
"rmse": 1.4494043209545224,
|
| 153 |
+
"mae": 1.1682950427532197,
|
| 154 |
+
"pearson_r": 0.4147866313943454,
|
| 155 |
+
"spearman_r": 0.42730966425629635,
|
| 156 |
+
"r2": 0.16184330209141884
|
| 157 |
+
},
|
| 158 |
+
"fit_and_eval_seconds": 0.33016813500034914
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"name": "combined_char_ridge",
|
| 162 |
+
"selected_alpha": 10.0,
|
| 163 |
+
"validation_trials": [
|
| 164 |
+
{
|
| 165 |
+
"alpha": 0.1,
|
| 166 |
+
"metrics": {
|
| 167 |
+
"rmse": 1.6081591147564207,
|
| 168 |
+
"mae": 1.2413834615945816,
|
| 169 |
+
"pearson_r": 0.4169937363739051,
|
| 170 |
+
"spearman_r": 0.40429283856812304,
|
| 171 |
+
"r2": -0.03628004246158212
|
| 172 |
+
}
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"alpha": 1.0,
|
| 176 |
+
"metrics": {
|
| 177 |
+
"rmse": 1.4107695549582286,
|
| 178 |
+
"mae": 1.1013368602991105,
|
| 179 |
+
"pearson_r": 0.4874112220852569,
|
| 180 |
+
"spearman_r": 0.47383417558330937,
|
| 181 |
+
"r2": 0.20249895881043067
|
| 182 |
+
}
|
| 183 |
+
},
|
| 184 |
+
{
|
| 185 |
+
"alpha": 10.0,
|
| 186 |
+
"metrics": {
|
| 187 |
+
"rmse": 1.383549482454833,
|
| 188 |
+
"mae": 1.0967753337621688,
|
| 189 |
+
"pearson_r": 0.49859659893827324,
|
| 190 |
+
"spearman_r": 0.4945176557261553,
|
| 191 |
+
"r2": 0.23297681168774853
|
| 192 |
+
}
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"alpha": 100.0,
|
| 196 |
+
"metrics": {
|
| 197 |
+
"rmse": 1.4929408447274506,
|
| 198 |
+
"mae": 1.200959615111351,
|
| 199 |
+
"pearson_r": 0.43016081195245404,
|
| 200 |
+
"spearman_r": 0.4306665031943897,
|
| 201 |
+
"r2": 0.10689133934020467
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
],
|
| 205 |
+
"validation": {
|
| 206 |
+
"rmse": 1.383549482454833,
|
| 207 |
+
"mae": 1.0967753337621688,
|
| 208 |
+
"pearson_r": 0.49859659893827324,
|
| 209 |
+
"spearman_r": 0.4945176557261553,
|
| 210 |
+
"r2": 0.23297681168774853
|
| 211 |
+
},
|
| 212 |
+
"test": {
|
| 213 |
+
"rmse": 1.3649470834344057,
|
| 214 |
+
"mae": 1.094587422132492,
|
| 215 |
+
"pearson_r": 0.5198256551098647,
|
| 216 |
+
"spearman_r": 0.5354444265178329,
|
| 217 |
+
"r2": 0.2566767000232979
|
| 218 |
+
},
|
| 219 |
+
"fit_and_eval_seconds": 1.7034703729987086
|
| 220 |
+
}
|
| 221 |
+
]
|
| 222 |
+
},
|
| 223 |
+
"cold_protein_exact": {
|
| 224 |
+
"rows": {
|
| 225 |
+
"train": 8000,
|
| 226 |
+
"validation": 1000,
|
| 227 |
+
"test": 1000
|
| 228 |
+
},
|
| 229 |
+
"mean_baseline": {
|
| 230 |
+
"prediction": 6.476698521107435,
|
| 231 |
+
"validation": {
|
| 232 |
+
"rmse": 1.579782624176554,
|
| 233 |
+
"mae": 1.3174895944134593,
|
| 234 |
+
"pearson_r": null,
|
| 235 |
+
"spearman_r": null,
|
| 236 |
+
"r2": -0.01274982588145357
|
| 237 |
+
},
|
| 238 |
+
"test": {
|
| 239 |
+
"rmse": 1.4951426135931578,
|
| 240 |
+
"mae": 1.2163382358753083,
|
| 241 |
+
"pearson_r": null,
|
| 242 |
+
"spearman_r": null,
|
| 243 |
+
"r2": -0.014679353070982115
|
| 244 |
+
}
|
| 245 |
+
},
|
| 246 |
+
"feature_dimensions": {
|
| 247 |
+
"protein": 4096,
|
| 248 |
+
"ligand": 4096
|
| 249 |
+
},
|
| 250 |
+
"models": [
|
| 251 |
+
{
|
| 252 |
+
"name": "protein_char3_ridge",
|
| 253 |
+
"selected_alpha": 1.0,
|
| 254 |
+
"validation_trials": [
|
| 255 |
+
{
|
| 256 |
+
"alpha": 0.1,
|
| 257 |
+
"metrics": {
|
| 258 |
+
"rmse": 1.5913619001081973,
|
| 259 |
+
"mae": 1.2860890839099883,
|
| 260 |
+
"pearson_r": 0.24087607131254302,
|
| 261 |
+
"spearman_r": 0.24239051227885716,
|
| 262 |
+
"r2": -0.02765046684019734
|
| 263 |
+
}
|
| 264 |
+
},
|
| 265 |
+
{
|
| 266 |
+
"alpha": 1.0,
|
| 267 |
+
"metrics": {
|
| 268 |
+
"rmse": 1.4859462663648586,
|
| 269 |
+
"mae": 1.2179750628471375,
|
| 270 |
+
"pearson_r": 0.3474792859155311,
|
| 271 |
+
"spearman_r": 0.33699637680490147,
|
| 272 |
+
"r2": 0.10398821505374234
|
| 273 |
+
}
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"alpha": 10.0,
|
| 277 |
+
"metrics": {
|
| 278 |
+
"rmse": 1.5008105727933332,
|
| 279 |
+
"mae": 1.2442660155296326,
|
| 280 |
+
"pearson_r": 0.3831242209268426,
|
| 281 |
+
"spearman_r": 0.3726678732274564,
|
| 282 |
+
"r2": 0.08597247826157572
|
| 283 |
+
}
|
| 284 |
+
},
|
| 285 |
+
{
|
| 286 |
+
"alpha": 100.0,
|
| 287 |
+
"metrics": {
|
| 288 |
+
"rmse": 1.553986133423702,
|
| 289 |
+
"mae": 1.2952312564849853,
|
| 290 |
+
"pearson_r": 0.3282098020500765,
|
| 291 |
+
"spearman_r": 0.30711899038156937,
|
| 292 |
+
"r2": 0.02005479994650128
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
],
|
| 296 |
+
"validation": {
|
| 297 |
+
"rmse": 1.4859462663648586,
|
| 298 |
+
"mae": 1.2179750628471375,
|
| 299 |
+
"pearson_r": 0.3474792859155311,
|
| 300 |
+
"spearman_r": 0.33699637680490147,
|
| 301 |
+
"r2": 0.10398821505374234
|
| 302 |
+
},
|
| 303 |
+
"test": {
|
| 304 |
+
"rmse": 1.4585936156876136,
|
| 305 |
+
"mae": 1.1720177594423293,
|
| 306 |
+
"pearson_r": 0.2545678403741694,
|
| 307 |
+
"spearman_r": 0.2484803560755494,
|
| 308 |
+
"r2": 0.03432230415157966
|
| 309 |
+
},
|
| 310 |
+
"fit_and_eval_seconds": 0.9749435680005263
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
"name": "ligand_char2_5_ridge",
|
| 314 |
+
"selected_alpha": 10.0,
|
| 315 |
+
"validation_trials": [
|
| 316 |
+
{
|
| 317 |
+
"alpha": 0.1,
|
| 318 |
+
"metrics": {
|
| 319 |
+
"rmse": 1.6018811256320094,
|
| 320 |
+
"mae": 1.2999265880584716,
|
| 321 |
+
"pearson_r": 0.30216596804115387,
|
| 322 |
+
"spearman_r": 0.29339634813312904,
|
| 323 |
+
"r2": -0.04128132661572703
|
| 324 |
+
}
|
| 325 |
+
},
|
| 326 |
+
{
|
| 327 |
+
"alpha": 1.0,
|
| 328 |
+
"metrics": {
|
| 329 |
+
"rmse": 1.4768323814132662,
|
| 330 |
+
"mae": 1.2094637866020204,
|
| 331 |
+
"pearson_r": 0.37716980445129944,
|
| 332 |
+
"spearman_r": 0.36773099907324625,
|
| 333 |
+
"r2": 0.11494568428343488
|
| 334 |
+
}
|
| 335 |
+
},
|
| 336 |
+
{
|
| 337 |
+
"alpha": 10.0,
|
| 338 |
+
"metrics": {
|
| 339 |
+
"rmse": 1.4613319935586893,
|
| 340 |
+
"mae": 1.2048628535270691,
|
| 341 |
+
"pearson_r": 0.3911678449191863,
|
| 342 |
+
"spearman_r": 0.3873732410005544,
|
| 343 |
+
"r2": 0.1334267140116593
|
| 344 |
+
}
|
| 345 |
+
},
|
| 346 |
+
{
|
| 347 |
+
"alpha": 100.0,
|
| 348 |
+
"metrics": {
|
| 349 |
+
"rmse": 1.5197247823028681,
|
| 350 |
+
"mae": 1.2624011125564576,
|
| 351 |
+
"pearson_r": 0.3423555617155642,
|
| 352 |
+
"spearman_r": 0.3437308713625526,
|
| 353 |
+
"r2": 0.06278894470085006
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
],
|
| 357 |
+
"validation": {
|
| 358 |
+
"rmse": 1.4613319935586893,
|
| 359 |
+
"mae": 1.2048628535270691,
|
| 360 |
+
"pearson_r": 0.3911678449191863,
|
| 361 |
+
"spearman_r": 0.3873732410005544,
|
| 362 |
+
"r2": 0.1334267140116593
|
| 363 |
+
},
|
| 364 |
+
"test": {
|
| 365 |
+
"rmse": 1.3526643485350685,
|
| 366 |
+
"mae": 1.0842266837358474,
|
| 367 |
+
"pearson_r": 0.4362727683300783,
|
| 368 |
+
"spearman_r": 0.4191402824529675,
|
| 369 |
+
"r2": 0.16949228447261422
|
| 370 |
+
},
|
| 371 |
+
"fit_and_eval_seconds": 0.37831321699923137
|
| 372 |
+
},
|
| 373 |
+
{
|
| 374 |
+
"name": "combined_char_ridge",
|
| 375 |
+
"selected_alpha": 1.0,
|
| 376 |
+
"validation_trials": [
|
| 377 |
+
{
|
| 378 |
+
"alpha": 0.1,
|
| 379 |
+
"metrics": {
|
| 380 |
+
"rmse": 1.609541967480026,
|
| 381 |
+
"mae": 1.2925864708423616,
|
| 382 |
+
"pearson_r": 0.3470317009211976,
|
| 383 |
+
"spearman_r": 0.3383021776125508,
|
| 384 |
+
"r2": -0.05126479701152342
|
| 385 |
+
}
|
| 386 |
+
},
|
| 387 |
+
{
|
| 388 |
+
"alpha": 1.0,
|
| 389 |
+
"metrics": {
|
| 390 |
+
"rmse": 1.4096703358753107,
|
| 391 |
+
"mae": 1.149479679107666,
|
| 392 |
+
"pearson_r": 0.4577114457735785,
|
| 393 |
+
"spearman_r": 0.4511058044444574,
|
| 394 |
+
"r2": 0.1936146421708813
|
| 395 |
+
}
|
| 396 |
+
},
|
| 397 |
+
{
|
| 398 |
+
"alpha": 10.0,
|
| 399 |
+
"metrics": {
|
| 400 |
+
"rmse": 1.4111861303751425,
|
| 401 |
+
"mae": 1.165886058807373,
|
| 402 |
+
"pearson_r": 0.48119186255022445,
|
| 403 |
+
"spearman_r": 0.481658449066694,
|
| 404 |
+
"r2": 0.19187952492255234
|
| 405 |
+
}
|
| 406 |
+
},
|
| 407 |
+
{
|
| 408 |
+
"alpha": 100.0,
|
| 409 |
+
"metrics": {
|
| 410 |
+
"rmse": 1.500300406119371,
|
| 411 |
+
"mae": 1.2459029321670532,
|
| 412 |
+
"pearson_r": 0.41484717418396205,
|
| 413 |
+
"spearman_r": 0.4187622144178708,
|
| 414 |
+
"r2": 0.08659377868944229
|
| 415 |
+
}
|
| 416 |
+
}
|
| 417 |
+
],
|
| 418 |
+
"validation": {
|
| 419 |
+
"rmse": 1.4096703358753107,
|
| 420 |
+
"mae": 1.149479679107666,
|
| 421 |
+
"pearson_r": 0.4577114457735785,
|
| 422 |
+
"spearman_r": 0.4511058044444574,
|
| 423 |
+
"r2": 0.1936146421708813
|
| 424 |
+
},
|
| 425 |
+
"test": {
|
| 426 |
+
"rmse": 1.3462287870584813,
|
| 427 |
+
"mae": 1.0676061400175094,
|
| 428 |
+
"pearson_r": 0.4384364045433516,
|
| 429 |
+
"spearman_r": 0.42825367206750203,
|
| 430 |
+
"r2": 0.17737608661000348
|
| 431 |
+
},
|
| 432 |
+
"fit_and_eval_seconds": 1.6212121779990412
|
| 433 |
+
}
|
| 434 |
+
]
|
| 435 |
+
},
|
| 436 |
+
"protein_similarity_50": {
|
| 437 |
+
"rows": {
|
| 438 |
+
"train": 8000,
|
| 439 |
+
"validation": 1000,
|
| 440 |
+
"test": 1000
|
| 441 |
+
},
|
| 442 |
+
"mean_baseline": {
|
| 443 |
+
"prediction": 6.4936330486238,
|
| 444 |
+
"validation": {
|
| 445 |
+
"rmse": 1.5513375597083523,
|
| 446 |
+
"mae": 1.2790881541401746,
|
| 447 |
+
"pearson_r": null,
|
| 448 |
+
"spearman_r": null,
|
| 449 |
+
"r2": -0.013098447070553743
|
| 450 |
+
},
|
| 451 |
+
"test": {
|
| 452 |
+
"rmse": 1.533614785804936,
|
| 453 |
+
"mae": 1.2540174638183117,
|
| 454 |
+
"pearson_r": null,
|
| 455 |
+
"spearman_r": null,
|
| 456 |
+
"r2": -5.474558686380249e-05
|
| 457 |
+
}
|
| 458 |
+
},
|
| 459 |
+
"feature_dimensions": {
|
| 460 |
+
"protein": 4096,
|
| 461 |
+
"ligand": 4096
|
| 462 |
+
},
|
| 463 |
+
"models": [
|
| 464 |
+
{
|
| 465 |
+
"name": "protein_char3_ridge",
|
| 466 |
+
"selected_alpha": 10.0,
|
| 467 |
+
"validation_trials": [
|
| 468 |
+
{
|
| 469 |
+
"alpha": 0.1,
|
| 470 |
+
"metrics": {
|
| 471 |
+
"rmse": 1.7552403417237736,
|
| 472 |
+
"mae": 1.401314944624901,
|
| 473 |
+
"pearson_r": 0.18824801515316072,
|
| 474 |
+
"spearman_r": 0.18074336545364095,
|
| 475 |
+
"r2": -0.29691710691928597
|
| 476 |
+
}
|
| 477 |
+
},
|
| 478 |
+
{
|
| 479 |
+
"alpha": 1.0,
|
| 480 |
+
"metrics": {
|
| 481 |
+
"rmse": 1.5601924885212413,
|
| 482 |
+
"mae": 1.2610043984651567,
|
| 483 |
+
"pearson_r": 0.2319965337290784,
|
| 484 |
+
"spearman_r": 0.23655822614618757,
|
| 485 |
+
"r2": -0.024696847762769814
|
| 486 |
+
}
|
| 487 |
+
},
|
| 488 |
+
{
|
| 489 |
+
"alpha": 10.0,
|
| 490 |
+
"metrics": {
|
| 491 |
+
"rmse": 1.5356435415785854,
|
| 492 |
+
"mae": 1.2523401461839676,
|
| 493 |
+
"pearson_r": 0.21912642622037123,
|
| 494 |
+
"spearman_r": 0.2117505257188302,
|
| 495 |
+
"r2": 0.0072957755423069814
|
| 496 |
+
}
|
| 497 |
+
},
|
| 498 |
+
{
|
| 499 |
+
"alpha": 100.0,
|
| 500 |
+
"metrics": {
|
| 501 |
+
"rmse": 1.5432243885780599,
|
| 502 |
+
"mae": 1.2689961529970168,
|
| 503 |
+
"pearson_r": 0.19756629969633466,
|
| 504 |
+
"spearman_r": 0.19839572987049425,
|
| 505 |
+
"r2": -0.0025295698557341595
|
| 506 |
+
}
|
| 507 |
+
}
|
| 508 |
+
],
|
| 509 |
+
"validation": {
|
| 510 |
+
"rmse": 1.5356435415785854,
|
| 511 |
+
"mae": 1.2523401461839676,
|
| 512 |
+
"pearson_r": 0.21912642622037123,
|
| 513 |
+
"spearman_r": 0.2117505257188302,
|
| 514 |
+
"r2": 0.0072957755423069814
|
| 515 |
+
},
|
| 516 |
+
"test": {
|
| 517 |
+
"rmse": 1.5253163396597564,
|
| 518 |
+
"mae": 1.2421429891586304,
|
| 519 |
+
"pearson_r": 0.12107755752116321,
|
| 520 |
+
"spearman_r": 0.11902515580955182,
|
| 521 |
+
"r2": 0.010738639684262585
|
| 522 |
+
},
|
| 523 |
+
"fit_and_eval_seconds": 0.8420703150004556
|
| 524 |
+
},
|
| 525 |
+
{
|
| 526 |
+
"name": "ligand_char2_5_ridge",
|
| 527 |
+
"selected_alpha": 10.0,
|
| 528 |
+
"validation_trials": [
|
| 529 |
+
{
|
| 530 |
+
"alpha": 0.1,
|
| 531 |
+
"metrics": {
|
| 532 |
+
"rmse": 1.5799239713380486,
|
| 533 |
+
"mae": 1.271127613902092,
|
| 534 |
+
"pearson_r": 0.29265036816534756,
|
| 535 |
+
"spearman_r": 0.2854553015351282,
|
| 536 |
+
"r2": -0.050779065362796194
|
| 537 |
+
}
|
| 538 |
+
},
|
| 539 |
+
{
|
| 540 |
+
"alpha": 1.0,
|
| 541 |
+
"metrics": {
|
| 542 |
+
"rmse": 1.4859846732161874,
|
| 543 |
+
"mae": 1.2077869259119034,
|
| 544 |
+
"pearson_r": 0.33129654600169606,
|
| 545 |
+
"spearman_r": 0.3199643083980807,
|
| 546 |
+
"r2": 0.07046083081253718
|
| 547 |
+
}
|
| 548 |
+
},
|
| 549 |
+
{
|
| 550 |
+
"alpha": 10.0,
|
| 551 |
+
"metrics": {
|
| 552 |
+
"rmse": 1.4720109593236679,
|
| 553 |
+
"mae": 1.1972612363100053,
|
| 554 |
+
"pearson_r": 0.3227538249245248,
|
| 555 |
+
"spearman_r": 0.316034036664011,
|
| 556 |
+
"r2": 0.08786079723640017
|
| 557 |
+
}
|
| 558 |
+
},
|
| 559 |
+
{
|
| 560 |
+
"alpha": 100.0,
|
| 561 |
+
"metrics": {
|
| 562 |
+
"rmse": 1.5116912475349742,
|
| 563 |
+
"mae": 1.23919963991642,
|
| 564 |
+
"pearson_r": 0.25043786181937333,
|
| 565 |
+
"spearman_r": 0.24831507291908664,
|
| 566 |
+
"r2": 0.03802179626202029
|
| 567 |
+
}
|
| 568 |
+
}
|
| 569 |
+
],
|
| 570 |
+
"validation": {
|
| 571 |
+
"rmse": 1.4720109593236679,
|
| 572 |
+
"mae": 1.1972612363100053,
|
| 573 |
+
"pearson_r": 0.3227538249245248,
|
| 574 |
+
"spearman_r": 0.316034036664011,
|
| 575 |
+
"r2": 0.08786079723640017
|
| 576 |
+
},
|
| 577 |
+
"test": {
|
| 578 |
+
"rmse": 1.429188581726333,
|
| 579 |
+
"mae": 1.1390189518928528,
|
| 580 |
+
"pearson_r": 0.36960662009993567,
|
| 581 |
+
"spearman_r": 0.3651127131105154,
|
| 582 |
+
"r2": 0.1314990922237853
|
| 583 |
+
},
|
| 584 |
+
"fit_and_eval_seconds": 0.33130267600063235
|
| 585 |
+
},
|
| 586 |
+
{
|
| 587 |
+
"name": "combined_char_ridge",
|
| 588 |
+
"selected_alpha": 10.0,
|
| 589 |
+
"validation_trials": [
|
| 590 |
+
{
|
| 591 |
+
"alpha": 0.1,
|
| 592 |
+
"metrics": {
|
| 593 |
+
"rmse": 1.695446275393881,
|
| 594 |
+
"mae": 1.3751192058324815,
|
| 595 |
+
"pearson_r": 0.28274147950808365,
|
| 596 |
+
"spearman_r": 0.2880691599027914,
|
| 597 |
+
"r2": -0.21006054325174417
|
| 598 |
+
}
|
| 599 |
+
},
|
| 600 |
+
{
|
| 601 |
+
"alpha": 1.0,
|
| 602 |
+
"metrics": {
|
| 603 |
+
"rmse": 1.4883475906405312,
|
| 604 |
+
"mae": 1.2132670460939408,
|
| 605 |
+
"pearson_r": 0.35310402171271504,
|
| 606 |
+
"spearman_r": 0.36051834925201537,
|
| 607 |
+
"r2": 0.06750229342577019
|
| 608 |
+
}
|
| 609 |
+
},
|
| 610 |
+
{
|
| 611 |
+
"alpha": 10.0,
|
| 612 |
+
"metrics": {
|
| 613 |
+
"rmse": 1.4627022031596522,
|
| 614 |
+
"mae": 1.1911575428247452,
|
| 615 |
+
"pearson_r": 0.36076986449064835,
|
| 616 |
+
"spearman_r": 0.3723077063029598,
|
| 617 |
+
"r2": 0.09936075775091613
|
| 618 |
+
}
|
| 619 |
+
},
|
| 620 |
+
{
|
| 621 |
+
"alpha": 100.0,
|
| 622 |
+
"metrics": {
|
| 623 |
+
"rmse": 1.5056113027663196,
|
| 624 |
+
"mae": 1.2321353279352187,
|
| 625 |
+
"pearson_r": 0.2885589685976343,
|
| 626 |
+
"spearman_r": 0.29454935455844794,
|
| 627 |
+
"r2": 0.04574428935903241
|
| 628 |
+
}
|
| 629 |
+
}
|
| 630 |
+
],
|
| 631 |
+
"validation": {
|
| 632 |
+
"rmse": 1.4627022031596522,
|
| 633 |
+
"mae": 1.1911575428247452,
|
| 634 |
+
"pearson_r": 0.36076986449064835,
|
| 635 |
+
"spearman_r": 0.3723077063029598,
|
| 636 |
+
"r2": 0.09936075775091613
|
| 637 |
+
},
|
| 638 |
+
"test": {
|
| 639 |
+
"rmse": 1.4348684777145742,
|
| 640 |
+
"mae": 1.1439314622879029,
|
| 641 |
+
"pearson_r": 0.36247186743462856,
|
| 642 |
+
"spearman_r": 0.3652137611435173,
|
| 643 |
+
"r2": 0.12458216430470337
|
| 644 |
+
},
|
| 645 |
+
"fit_and_eval_seconds": 1.6895546520008793
|
| 646 |
+
}
|
| 647 |
+
]
|
| 648 |
+
},
|
| 649 |
+
"random_pair": {
|
| 650 |
+
"rows": {
|
| 651 |
+
"train": 8000,
|
| 652 |
+
"validation": 1000,
|
| 653 |
+
"test": 1000
|
| 654 |
+
},
|
| 655 |
+
"mean_baseline": {
|
| 656 |
+
"prediction": 6.5244491582587365,
|
| 657 |
+
"validation": {
|
| 658 |
+
"rmse": 1.5276238617697966,
|
| 659 |
+
"mae": 1.2404643696633129,
|
| 660 |
+
"pearson_r": null,
|
| 661 |
+
"spearman_r": null,
|
| 662 |
+
"r2": -0.00011912455955709333
|
| 663 |
+
},
|
| 664 |
+
"test": {
|
| 665 |
+
"rmse": 1.565044692955996,
|
| 666 |
+
"mae": 1.2979176334988474,
|
| 667 |
+
"pearson_r": null,
|
| 668 |
+
"spearman_r": null,
|
| 669 |
+
"r2": -0.004413634248849885
|
| 670 |
+
}
|
| 671 |
+
},
|
| 672 |
+
"feature_dimensions": {
|
| 673 |
+
"protein": 4096,
|
| 674 |
+
"ligand": 4096
|
| 675 |
+
},
|
| 676 |
+
"models": [
|
| 677 |
+
{
|
| 678 |
+
"name": "protein_char3_ridge",
|
| 679 |
+
"selected_alpha": 1.0,
|
| 680 |
+
"validation_trials": [
|
| 681 |
+
{
|
| 682 |
+
"alpha": 0.1,
|
| 683 |
+
"metrics": {
|
| 684 |
+
"rmse": 1.4410565980112502,
|
| 685 |
+
"mae": 1.1288913173079491,
|
| 686 |
+
"pearson_r": 0.4079221480849496,
|
| 687 |
+
"spearman_r": 0.3893575689504746,
|
| 688 |
+
"r2": 0.1100185770832387
|
| 689 |
+
}
|
| 690 |
+
},
|
| 691 |
+
{
|
| 692 |
+
"alpha": 1.0,
|
| 693 |
+
"metrics": {
|
| 694 |
+
"rmse": 1.3745768065170116,
|
| 695 |
+
"mae": 1.100080373942852,
|
| 696 |
+
"pearson_r": 0.44118951305358517,
|
| 697 |
+
"spearman_r": 0.41362525923337845,
|
| 698 |
+
"r2": 0.1902389400564115
|
| 699 |
+
}
|
| 700 |
+
},
|
| 701 |
+
{
|
| 702 |
+
"alpha": 10.0,
|
| 703 |
+
"metrics": {
|
| 704 |
+
"rmse": 1.3955357347301767,
|
| 705 |
+
"mae": 1.1366457569003106,
|
| 706 |
+
"pearson_r": 0.4230883826225867,
|
| 707 |
+
"spearman_r": 0.4074744496313428,
|
| 708 |
+
"r2": 0.16535693645157734
|
| 709 |
+
}
|
| 710 |
+
},
|
| 711 |
+
{
|
| 712 |
+
"alpha": 100.0,
|
| 713 |
+
"metrics": {
|
| 714 |
+
"rmse": 1.4814185114679912,
|
| 715 |
+
"mae": 1.2066325885653495,
|
| 716 |
+
"pearson_r": 0.3582189512625387,
|
| 717 |
+
"spearman_r": 0.36664589962084587,
|
| 718 |
+
"r2": 0.059466217343855376
|
| 719 |
+
}
|
| 720 |
+
}
|
| 721 |
+
],
|
| 722 |
+
"validation": {
|
| 723 |
+
"rmse": 1.3745768065170116,
|
| 724 |
+
"mae": 1.100080373942852,
|
| 725 |
+
"pearson_r": 0.44118951305358517,
|
| 726 |
+
"spearman_r": 0.41362525923337845,
|
| 727 |
+
"r2": 0.1902389400564115
|
| 728 |
+
},
|
| 729 |
+
"test": {
|
| 730 |
+
"rmse": 1.3880651061666627,
|
| 731 |
+
"mae": 1.1175376932621002,
|
| 732 |
+
"pearson_r": 0.4640112366064133,
|
| 733 |
+
"spearman_r": 0.4357397752396537,
|
| 734 |
+
"r2": 0.20990596198875733
|
| 735 |
+
},
|
| 736 |
+
"fit_and_eval_seconds": 0.894843939000566
|
| 737 |
+
},
|
| 738 |
+
{
|
| 739 |
+
"name": "ligand_char2_5_ridge",
|
| 740 |
+
"selected_alpha": 10.0,
|
| 741 |
+
"validation_trials": [
|
| 742 |
+
{
|
| 743 |
+
"alpha": 0.1,
|
| 744 |
+
"metrics": {
|
| 745 |
+
"rmse": 1.4856668026573707,
|
| 746 |
+
"mae": 1.1865731849074364,
|
| 747 |
+
"pearson_r": 0.3688529817391914,
|
| 748 |
+
"spearman_r": 0.37857178194968666,
|
| 749 |
+
"r2": 0.05406411042117554
|
| 750 |
+
}
|
| 751 |
+
},
|
| 752 |
+
{
|
| 753 |
+
"alpha": 1.0,
|
| 754 |
+
"metrics": {
|
| 755 |
+
"rmse": 1.3916760241468447,
|
| 756 |
+
"mae": 1.1180368124842643,
|
| 757 |
+
"pearson_r": 0.42160711722938904,
|
| 758 |
+
"spearman_r": 0.42128867139651427,
|
| 759 |
+
"r2": 0.16996738915693665
|
| 760 |
+
}
|
| 761 |
+
},
|
| 762 |
+
{
|
| 763 |
+
"alpha": 10.0,
|
| 764 |
+
"metrics": {
|
| 765 |
+
"rmse": 1.3915522045386022,
|
| 766 |
+
"mae": 1.1223492309451104,
|
| 767 |
+
"pearson_r": 0.42089896113032044,
|
| 768 |
+
"spearman_r": 0.41150350807895536,
|
| 769 |
+
"r2": 0.17011508120440122
|
| 770 |
+
}
|
| 771 |
+
},
|
| 772 |
+
{
|
| 773 |
+
"alpha": 100.0,
|
| 774 |
+
"metrics": {
|
| 775 |
+
"rmse": 1.462418544451856,
|
| 776 |
+
"mae": 1.1855577809214592,
|
| 777 |
+
"pearson_r": 0.35716066077001357,
|
| 778 |
+
"spearman_r": 0.34302021119123416,
|
| 779 |
+
"r2": 0.08343718024568059
|
| 780 |
+
}
|
| 781 |
+
}
|
| 782 |
+
],
|
| 783 |
+
"validation": {
|
| 784 |
+
"rmse": 1.3915522045386022,
|
| 785 |
+
"mae": 1.1223492309451104,
|
| 786 |
+
"pearson_r": 0.42089896113032044,
|
| 787 |
+
"spearman_r": 0.41150350807895536,
|
| 788 |
+
"r2": 0.17011508120440122
|
| 789 |
+
},
|
| 790 |
+
"test": {
|
| 791 |
+
"rmse": 1.4268869133537447,
|
| 792 |
+
"mae": 1.175576719045639,
|
| 793 |
+
"pearson_r": 0.4211078957570381,
|
| 794 |
+
"spearman_r": 0.40335385177971433,
|
| 795 |
+
"r2": 0.1650927724032577
|
| 796 |
+
},
|
| 797 |
+
"fit_and_eval_seconds": 0.4427177519992256
|
| 798 |
+
},
|
| 799 |
+
{
|
| 800 |
+
"name": "combined_char_ridge",
|
| 801 |
+
"selected_alpha": 10.0,
|
| 802 |
+
"validation_trials": [
|
| 803 |
+
{
|
| 804 |
+
"alpha": 0.1,
|
| 805 |
+
"metrics": {
|
| 806 |
+
"rmse": 1.4956017228690686,
|
| 807 |
+
"mae": 1.1701721274256707,
|
| 808 |
+
"pearson_r": 0.4361198131829029,
|
| 809 |
+
"spearman_r": 0.4281298523300441,
|
| 810 |
+
"r2": 0.0413705240127229
|
| 811 |
+
}
|
| 812 |
+
},
|
| 813 |
+
{
|
| 814 |
+
"alpha": 1.0,
|
| 815 |
+
"metrics": {
|
| 816 |
+
"rmse": 1.3140696311066697,
|
| 817 |
+
"mae": 1.0519584881663322,
|
| 818 |
+
"pearson_r": 0.5201378315237909,
|
| 819 |
+
"spearman_r": 0.5029682145871107,
|
| 820 |
+
"r2": 0.25995927103340644
|
| 821 |
+
}
|
| 822 |
+
},
|
| 823 |
+
{
|
| 824 |
+
"alpha": 10.0,
|
| 825 |
+
"metrics": {
|
| 826 |
+
"rmse": 1.307333278135575,
|
| 827 |
+
"mae": 1.0559683529734611,
|
| 828 |
+
"pearson_r": 0.5300812334907434,
|
| 829 |
+
"spearman_r": 0.5194865614899057,
|
| 830 |
+
"r2": 0.26752720749752146
|
| 831 |
+
}
|
| 832 |
+
},
|
| 833 |
+
{
|
| 834 |
+
"alpha": 100.0,
|
| 835 |
+
"metrics": {
|
| 836 |
+
"rmse": 1.4257502567100055,
|
| 837 |
+
"mae": 1.15831251257658,
|
| 838 |
+
"pearson_r": 0.45176846530905573,
|
| 839 |
+
"spearman_r": 0.44875210828870205,
|
| 840 |
+
"r2": 0.1288242419468516
|
| 841 |
+
}
|
| 842 |
+
}
|
| 843 |
+
],
|
| 844 |
+
"validation": {
|
| 845 |
+
"rmse": 1.307333278135575,
|
| 846 |
+
"mae": 1.0559683529734611,
|
| 847 |
+
"pearson_r": 0.5300812334907434,
|
| 848 |
+
"spearman_r": 0.5194865614899057,
|
| 849 |
+
"r2": 0.26752720749752146
|
| 850 |
+
},
|
| 851 |
+
"test": {
|
| 852 |
+
"rmse": 1.335818862102712,
|
| 853 |
+
"mae": 1.0964913694858551,
|
| 854 |
+
"pearson_r": 0.5371481422264178,
|
| 855 |
+
"spearman_r": 0.5186748222826298,
|
| 856 |
+
"r2": 0.2682642831079074
|
| 857 |
+
},
|
| 858 |
+
"fit_and_eval_seconds": 1.8038349809994543
|
| 859 |
+
}
|
| 860 |
+
]
|
| 861 |
+
}
|
| 862 |
+
},
|
| 863 |
+
"total_seconds": 26.8222462800004
|
| 864 |
+
}
|
recovery/reports/overfit-actual-embeddings.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"examples": 32,
|
| 3 |
+
"steps": 1000,
|
| 4 |
+
"seed": 42,
|
| 5 |
+
"rmse_paffinity": 2.6320698458933376e-07,
|
| 6 |
+
"mae_paffinity": 9.685754776000977e-08,
|
| 7 |
+
"acceptance_max_rmse_paffinity": 0.05,
|
| 8 |
+
"passed": true,
|
| 9 |
+
"target_scaler": {
|
| 10 |
+
"mean": 6.446505546569824,
|
| 11 |
+
"std": 1.7902863025665283
|
| 12 |
+
},
|
| 13 |
+
"history": [
|
| 14 |
+
{
|
| 15 |
+
"step": 1,
|
| 16 |
+
"standardized_mse": 1.0336081981658936
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"step": 100,
|
| 20 |
+
"standardized_mse": 2.772125753836008e-06
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"step": 200,
|
| 24 |
+
"standardized_mse": 1.03808101004077e-10
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"step": 300,
|
| 28 |
+
"standardized_mse": 1.9242752194188056e-14
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"step": 400,
|
| 32 |
+
"standardized_mse": 1.2282174246831118e-14
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"step": 500,
|
| 36 |
+
"standardized_mse": 1.1517161193925346e-14
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"step": 600,
|
| 40 |
+
"standardized_mse": 5.5680982768535614e-05
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"step": 700,
|
| 44 |
+
"standardized_mse": 3.629010425498791e-09
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"step": 800,
|
| 48 |
+
"standardized_mse": 6.048471863336416e-14
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"step": 900,
|
| 52 |
+
"standardized_mse": 1.406742154404575e-14
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"step": 1000,
|
| 56 |
+
"standardized_mse": 1.5145552184365335e-14
|
| 57 |
+
}
|
| 58 |
+
],
|
| 59 |
+
"scope": "trainable head over 32 embeddings from the pinned frozen encoders recorded in recovery/config.json"
|
| 60 |
+
}
|
recovery/scripts/cluster_proteins.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Create a sequence-similarity-disjoint protein split using MMseqs2."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import shutil
|
| 9 |
+
import subprocess
|
| 10 |
+
from collections import Counter
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
from mitointeract_recovery.splits import assign_grouped_splits, assert_group_disjoint
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def read_rows(path: Path) -> list[dict]:
|
| 17 |
+
with path.open() as handle:
|
| 18 |
+
return [json.loads(line) for line in handle if line.strip()]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main() -> None:
|
| 22 |
+
parser = argparse.ArgumentParser()
|
| 23 |
+
parser.add_argument("--data-dir", type=Path, default=Path("artifacts/dev-10k"))
|
| 24 |
+
parser.add_argument("--min-seq-id", type=float, default=0.5)
|
| 25 |
+
parser.add_argument("--coverage", type=float, default=0.8)
|
| 26 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 27 |
+
parser.add_argument("--threads", type=int, default=8)
|
| 28 |
+
args = parser.parse_args()
|
| 29 |
+
|
| 30 |
+
if not shutil.which("mmseqs"):
|
| 31 |
+
raise RuntimeError("MMseqs2 is required; install the mmseqs2 package")
|
| 32 |
+
rows = read_rows(args.data_dir / "sample.jsonl")
|
| 33 |
+
proteins = sorted({row["protein_id"]: row["sequence"] for row in rows}.items())
|
| 34 |
+
suffix = (
|
| 35 |
+
f"identity-{int(args.min_seq_id * 100)}-coverage-{int(args.coverage * 100)}"
|
| 36 |
+
)
|
| 37 |
+
work = args.data_dir / f"mmseqs-{suffix}"
|
| 38 |
+
work.mkdir(parents=True, exist_ok=True)
|
| 39 |
+
fasta = work / "proteins.fasta"
|
| 40 |
+
with fasta.open("w") as handle:
|
| 41 |
+
for protein_id, sequence in proteins:
|
| 42 |
+
handle.write(f">{protein_id}\n{sequence}\n")
|
| 43 |
+
|
| 44 |
+
prefix = work / "clusters"
|
| 45 |
+
tmp = work / "tmp"
|
| 46 |
+
command = [
|
| 47 |
+
"mmseqs",
|
| 48 |
+
"easy-cluster",
|
| 49 |
+
str(fasta),
|
| 50 |
+
str(prefix),
|
| 51 |
+
str(tmp),
|
| 52 |
+
"--min-seq-id",
|
| 53 |
+
str(args.min_seq_id),
|
| 54 |
+
"-c",
|
| 55 |
+
str(args.coverage),
|
| 56 |
+
"--cov-mode",
|
| 57 |
+
"0",
|
| 58 |
+
"--alignment-mode",
|
| 59 |
+
"3",
|
| 60 |
+
"--threads",
|
| 61 |
+
str(args.threads),
|
| 62 |
+
]
|
| 63 |
+
cluster_tsv = prefix.with_name(prefix.name + "_cluster.tsv")
|
| 64 |
+
if not cluster_tsv.exists():
|
| 65 |
+
subprocess.run(command, check=True)
|
| 66 |
+
protein_to_cluster: dict[str, str] = {}
|
| 67 |
+
with cluster_tsv.open() as handle:
|
| 68 |
+
for line in handle:
|
| 69 |
+
representative, member = line.rstrip("\n").split("\t")
|
| 70 |
+
protein_to_cluster[member] = representative
|
| 71 |
+
missing = {protein_id for protein_id, _ in proteins} - protein_to_cluster.keys()
|
| 72 |
+
if missing:
|
| 73 |
+
raise AssertionError(f"MMseqs2 omitted {len(missing)} proteins")
|
| 74 |
+
|
| 75 |
+
enriched = [
|
| 76 |
+
dict(row, protein_similarity_cluster=protein_to_cluster[row["protein_id"]])
|
| 77 |
+
for row in rows
|
| 78 |
+
]
|
| 79 |
+
assignments = assign_grouped_splits(
|
| 80 |
+
enriched,
|
| 81 |
+
group_key="protein_similarity_cluster",
|
| 82 |
+
seed=args.seed,
|
| 83 |
+
)
|
| 84 |
+
assert_group_disjoint(enriched, assignments, group_key="protein_similarity_cluster")
|
| 85 |
+
manifest_name = f"split-protein_similarity_{int(args.min_seq_id * 100)}.jsonl"
|
| 86 |
+
manifest = args.data_dir / manifest_name
|
| 87 |
+
with manifest.open("w") as handle:
|
| 88 |
+
for row in rows:
|
| 89 |
+
handle.write(
|
| 90 |
+
json.dumps(
|
| 91 |
+
{"pair_id": row["pair_id"], "split": assignments[row["pair_id"]]}
|
| 92 |
+
)
|
| 93 |
+
+ "\n"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
cluster_sizes = Counter(protein_to_cluster.values())
|
| 97 |
+
row_counts = Counter(assignments.values())
|
| 98 |
+
report = {
|
| 99 |
+
"tool": "MMseqs2",
|
| 100 |
+
"command": command,
|
| 101 |
+
"min_sequence_identity": args.min_seq_id,
|
| 102 |
+
"minimum_alignment_coverage": args.coverage,
|
| 103 |
+
"coverage_mode": "both query and target",
|
| 104 |
+
"alignment_mode": "actual identical residues / aligned columns",
|
| 105 |
+
"unique_proteins": len(proteins),
|
| 106 |
+
"clusters": len(cluster_sizes),
|
| 107 |
+
"largest_cluster_proteins": max(cluster_sizes.values()),
|
| 108 |
+
"split_rows": dict(row_counts),
|
| 109 |
+
"manifest": manifest_name,
|
| 110 |
+
"protein_cluster_overlap": 0,
|
| 111 |
+
}
|
| 112 |
+
(args.data_dir / f"protein-clusters-{suffix}.json").write_text(
|
| 113 |
+
json.dumps(report, indent=2) + "\n"
|
| 114 |
+
)
|
| 115 |
+
audit_path = args.data_dir / "audit.json"
|
| 116 |
+
audit = json.loads(audit_path.read_text())
|
| 117 |
+
audit.setdefault("split_manifests", {})[
|
| 118 |
+
f"protein_similarity_{int(args.min_seq_id * 100)}"
|
| 119 |
+
] = manifest_name
|
| 120 |
+
audit.setdefault("external_split_audits", {})[
|
| 121 |
+
f"protein_similarity_{int(args.min_seq_id * 100)}"
|
| 122 |
+
] = report
|
| 123 |
+
audit_path.write_text(json.dumps(audit, indent=2) + "\n")
|
| 124 |
+
print(json.dumps(report, indent=2))
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
if __name__ == "__main__":
|
| 128 |
+
main()
|
recovery/scripts/dataset_smoke.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""Validate dataset target semantics and compute a deterministic mean baseline."""
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import argparse
|
|
@@ -11,14 +12,20 @@ import urllib.parse
|
|
| 11 |
import urllib.request
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
-
from mitointeract_recovery import
|
| 15 |
|
| 16 |
|
| 17 |
def fetch_rows(dataset: str, rows: int) -> list[dict]:
|
| 18 |
if not 2 <= rows <= 100:
|
| 19 |
raise ValueError("Dataset Viewer smoke checks support 2-100 rows")
|
| 20 |
query = urllib.parse.urlencode(
|
| 21 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
)
|
| 23 |
url = f"https://datasets-server.huggingface.co/rows?{query}"
|
| 24 |
with urllib.request.urlopen(url, timeout=60) as response:
|
|
@@ -31,7 +38,9 @@ def pearson(xs: list[float], ys: list[float]) -> float | None:
|
|
| 31 |
return None
|
| 32 |
xm, ym = statistics.mean(xs), statistics.mean(ys)
|
| 33 |
numerator = sum((x - xm) * (y - ym) for x, y in zip(xs, ys))
|
| 34 |
-
denominator = math.sqrt(
|
|
|
|
|
|
|
| 35 |
return numerator / denominator if denominator else None
|
| 36 |
|
| 37 |
|
|
@@ -40,17 +49,26 @@ def main() -> None:
|
|
| 40 |
parser.add_argument("--dataset", default="jglaser/binding_affinity")
|
| 41 |
parser.add_argument("--rows", type=int, default=100)
|
| 42 |
parser.add_argument("--seed", type=int, default=42)
|
| 43 |
-
parser.add_argument(
|
|
|
|
|
|
|
| 44 |
args = parser.parse_args()
|
| 45 |
|
| 46 |
rows = fetch_rows(args.dataset, args.rows)
|
| 47 |
errors = []
|
| 48 |
for index, row in enumerate(rows):
|
| 49 |
-
calculated =
|
| 50 |
published = float(row["neg_log10_affinity_M"])
|
| 51 |
delta = abs(calculated - published)
|
| 52 |
if delta > 1e-5:
|
| 53 |
-
errors.append(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
shuffled = list(rows)
|
| 56 |
random.Random(args.seed).shuffle(shuffled)
|
|
@@ -60,16 +78,27 @@ def main() -> None:
|
|
| 60 |
test_targets = [float(row["neg_log10_affinity_M"]) for row in test]
|
| 61 |
mean_prediction = statistics.mean(train_targets)
|
| 62 |
predictions = [mean_prediction] * len(test_targets)
|
| 63 |
-
rmse = math.sqrt(
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
report = {
|
| 67 |
"dataset": args.dataset,
|
| 68 |
"rows": len(rows),
|
| 69 |
"seed": args.seed,
|
| 70 |
"target_column": "neg_log10_affinity_M",
|
|
|
|
| 71 |
"unit_check": {"passed": not errors, "errors": errors},
|
| 72 |
-
"split": {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
"train_mean_baseline": {
|
| 74 |
"prediction": mean_prediction,
|
| 75 |
"rmse": rmse,
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""Validate dataset target semantics and compute a deterministic mean baseline."""
|
| 3 |
+
|
| 4 |
from __future__ import annotations
|
| 5 |
|
| 6 |
import argparse
|
|
|
|
| 12 |
import urllib.request
|
| 13 |
from pathlib import Path
|
| 14 |
|
| 15 |
+
from mitointeract_recovery import micromolar_to_paffinity
|
| 16 |
|
| 17 |
|
| 18 |
def fetch_rows(dataset: str, rows: int) -> list[dict]:
|
| 19 |
if not 2 <= rows <= 100:
|
| 20 |
raise ValueError("Dataset Viewer smoke checks support 2-100 rows")
|
| 21 |
query = urllib.parse.urlencode(
|
| 22 |
+
{
|
| 23 |
+
"dataset": dataset,
|
| 24 |
+
"config": "default",
|
| 25 |
+
"split": "train",
|
| 26 |
+
"offset": 0,
|
| 27 |
+
"length": rows,
|
| 28 |
+
}
|
| 29 |
)
|
| 30 |
url = f"https://datasets-server.huggingface.co/rows?{query}"
|
| 31 |
with urllib.request.urlopen(url, timeout=60) as response:
|
|
|
|
| 38 |
return None
|
| 39 |
xm, ym = statistics.mean(xs), statistics.mean(ys)
|
| 40 |
numerator = sum((x - xm) * (y - ym) for x, y in zip(xs, ys))
|
| 41 |
+
denominator = math.sqrt(
|
| 42 |
+
sum((x - xm) ** 2 for x in xs) * sum((y - ym) ** 2 for y in ys)
|
| 43 |
+
)
|
| 44 |
return numerator / denominator if denominator else None
|
| 45 |
|
| 46 |
|
|
|
|
| 49 |
parser.add_argument("--dataset", default="jglaser/binding_affinity")
|
| 50 |
parser.add_argument("--rows", type=int, default=100)
|
| 51 |
parser.add_argument("--seed", type=int, default=42)
|
| 52 |
+
parser.add_argument(
|
| 53 |
+
"--output", type=Path, default=Path("artifacts/dataset-smoke.json")
|
| 54 |
+
)
|
| 55 |
args = parser.parse_args()
|
| 56 |
|
| 57 |
rows = fetch_rows(args.dataset, args.rows)
|
| 58 |
errors = []
|
| 59 |
for index, row in enumerate(rows):
|
| 60 |
+
calculated = micromolar_to_paffinity(float(row["affinity_uM"]))
|
| 61 |
published = float(row["neg_log10_affinity_M"])
|
| 62 |
delta = abs(calculated - published)
|
| 63 |
if delta > 1e-5:
|
| 64 |
+
errors.append(
|
| 65 |
+
{
|
| 66 |
+
"row": index,
|
| 67 |
+
"calculated": calculated,
|
| 68 |
+
"published": published,
|
| 69 |
+
"delta": delta,
|
| 70 |
+
}
|
| 71 |
+
)
|
| 72 |
|
| 73 |
shuffled = list(rows)
|
| 74 |
random.Random(args.seed).shuffle(shuffled)
|
|
|
|
| 78 |
test_targets = [float(row["neg_log10_affinity_M"]) for row in test]
|
| 79 |
mean_prediction = statistics.mean(train_targets)
|
| 80 |
predictions = [mean_prediction] * len(test_targets)
|
| 81 |
+
rmse = math.sqrt(
|
| 82 |
+
statistics.mean(
|
| 83 |
+
(pred - target) ** 2 for pred, target in zip(predictions, test_targets)
|
| 84 |
+
)
|
| 85 |
+
)
|
| 86 |
+
mae = statistics.mean(
|
| 87 |
+
abs(pred - target) for pred, target in zip(predictions, test_targets)
|
| 88 |
+
)
|
| 89 |
|
| 90 |
report = {
|
| 91 |
"dataset": args.dataset,
|
| 92 |
"rows": len(rows),
|
| 93 |
"seed": args.seed,
|
| 94 |
"target_column": "neg_log10_affinity_M",
|
| 95 |
+
"target_unit": "pAffinity (-log10 of the dataset's mixed affinity value in M)",
|
| 96 |
"unit_check": {"passed": not errors, "errors": errors},
|
| 97 |
+
"split": {
|
| 98 |
+
"train_rows": len(train),
|
| 99 |
+
"test_rows": len(test),
|
| 100 |
+
"kind": "seeded smoke only",
|
| 101 |
+
},
|
| 102 |
"train_mean_baseline": {
|
| 103 |
"prediction": mean_prediction,
|
| 104 |
"rmse": rmse,
|
recovery/scripts/embed_sample.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Cache pinned frozen-encoder embeddings for a MitoInteract sample."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import time
|
| 9 |
+
from collections import defaultdict
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch
|
| 14 |
+
from transformers import AutoModel, AutoTokenizer
|
| 15 |
+
|
| 16 |
+
DEFAULT_PROTEIN_MODEL = "facebook/esm2_t12_35M_UR50D"
|
| 17 |
+
DEFAULT_PROTEIN_REVISION = "6fbf070e65b0b7291e7bbcd451118c216cff79d8"
|
| 18 |
+
DEFAULT_LIGAND_MODEL = "DeepChem/ChemBERTa-77M-MLM"
|
| 19 |
+
DEFAULT_LIGAND_REVISION = "ed8a5374f2024ec8da53760af91a33fb8f6a15ff"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def read_rows(path: Path, limit: int | None) -> list[dict]:
|
| 23 |
+
rows = []
|
| 24 |
+
with path.open() as handle:
|
| 25 |
+
for line in handle:
|
| 26 |
+
if line.strip():
|
| 27 |
+
rows.append(json.loads(line))
|
| 28 |
+
if limit and len(rows) >= limit:
|
| 29 |
+
break
|
| 30 |
+
return rows
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def masked_mean(
|
| 34 |
+
last_hidden: torch.Tensor, attention: torch.Tensor, special: torch.Tensor
|
| 35 |
+
) -> torch.Tensor:
|
| 36 |
+
mask = attention.bool() & ~special.bool()
|
| 37 |
+
weights = mask.unsqueeze(-1).to(last_hidden.dtype)
|
| 38 |
+
return (last_hidden * weights).sum(dim=1) / weights.sum(dim=1).clamp_min(1)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def encode_texts(
|
| 42 |
+
model, tokenizer, texts: list[str], batch_size: int, device: torch.device
|
| 43 |
+
) -> np.ndarray:
|
| 44 |
+
outputs = []
|
| 45 |
+
for start in range(0, len(texts), batch_size):
|
| 46 |
+
batch = texts[start : start + batch_size]
|
| 47 |
+
encoded = tokenizer(
|
| 48 |
+
batch,
|
| 49 |
+
padding=True,
|
| 50 |
+
truncation=True,
|
| 51 |
+
max_length=min(getattr(tokenizer, "model_max_length", 512), 512),
|
| 52 |
+
return_special_tokens_mask=True,
|
| 53 |
+
return_tensors="pt",
|
| 54 |
+
)
|
| 55 |
+
special = encoded.pop("special_tokens_mask")
|
| 56 |
+
encoded = {key: value.to(device) for key, value in encoded.items()}
|
| 57 |
+
with torch.inference_mode():
|
| 58 |
+
hidden = model(**encoded).last_hidden_state
|
| 59 |
+
pooled = masked_mean(hidden, encoded["attention_mask"], special.to(device))
|
| 60 |
+
outputs.append(pooled.float().cpu().numpy())
|
| 61 |
+
return np.concatenate(outputs, axis=0)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def encode_proteins(
|
| 65 |
+
model,
|
| 66 |
+
tokenizer,
|
| 67 |
+
entities: list[tuple[str, str]],
|
| 68 |
+
batch_size: int,
|
| 69 |
+
device: torch.device,
|
| 70 |
+
chunk_residues: int,
|
| 71 |
+
) -> dict[str, np.ndarray]:
|
| 72 |
+
chunks: list[str] = []
|
| 73 |
+
owners: list[str] = []
|
| 74 |
+
weights: list[int] = []
|
| 75 |
+
for entity_id, sequence in entities:
|
| 76 |
+
for start in range(0, len(sequence), chunk_residues):
|
| 77 |
+
chunk = sequence[start : start + chunk_residues]
|
| 78 |
+
chunks.append(chunk)
|
| 79 |
+
owners.append(entity_id)
|
| 80 |
+
weights.append(len(chunk))
|
| 81 |
+
|
| 82 |
+
chunk_embeddings = []
|
| 83 |
+
for start in range(0, len(chunks), batch_size):
|
| 84 |
+
batch = chunks[start : start + batch_size]
|
| 85 |
+
encoded = tokenizer(
|
| 86 |
+
batch,
|
| 87 |
+
padding=True,
|
| 88 |
+
truncation=True,
|
| 89 |
+
max_length=chunk_residues + 2,
|
| 90 |
+
return_special_tokens_mask=True,
|
| 91 |
+
return_tensors="pt",
|
| 92 |
+
)
|
| 93 |
+
special = encoded.pop("special_tokens_mask")
|
| 94 |
+
encoded = {key: value.to(device) for key, value in encoded.items()}
|
| 95 |
+
with torch.inference_mode():
|
| 96 |
+
hidden = model(**encoded).last_hidden_state
|
| 97 |
+
pooled = masked_mean(hidden, encoded["attention_mask"], special.to(device))
|
| 98 |
+
chunk_embeddings.extend(pooled.float().cpu().numpy())
|
| 99 |
+
|
| 100 |
+
accum: dict[str, list[tuple[np.ndarray, int]]] = defaultdict(list)
|
| 101 |
+
for owner, embedding, weight in zip(owners, chunk_embeddings, weights):
|
| 102 |
+
accum[owner].append((embedding, weight))
|
| 103 |
+
result = {}
|
| 104 |
+
for owner, values in accum.items():
|
| 105 |
+
matrix = np.stack([value for value, _ in values])
|
| 106 |
+
entity_weights = np.asarray([weight for _, weight in values], dtype=np.float32)
|
| 107 |
+
result[owner] = np.average(matrix, axis=0, weights=entity_weights)
|
| 108 |
+
return result
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def main() -> None:
|
| 112 |
+
parser = argparse.ArgumentParser()
|
| 113 |
+
parser.add_argument(
|
| 114 |
+
"--sample", type=Path, default=Path("artifacts/dev-10k/sample.jsonl")
|
| 115 |
+
)
|
| 116 |
+
parser.add_argument("--limit", type=int)
|
| 117 |
+
parser.add_argument("--batch-size", type=int, default=8)
|
| 118 |
+
parser.add_argument("--protein-chunk-residues", type=int, default=1022)
|
| 119 |
+
parser.add_argument("--device", choices=["auto", "cpu", "cuda"], default="auto")
|
| 120 |
+
parser.add_argument("--protein-model", default=DEFAULT_PROTEIN_MODEL)
|
| 121 |
+
parser.add_argument("--protein-revision", default=DEFAULT_PROTEIN_REVISION)
|
| 122 |
+
parser.add_argument("--ligand-model", default=DEFAULT_LIGAND_MODEL)
|
| 123 |
+
parser.add_argument("--ligand-revision", default=DEFAULT_LIGAND_REVISION)
|
| 124 |
+
parser.add_argument("--output", type=Path, default=Path("artifacts/embeddings.npz"))
|
| 125 |
+
args = parser.parse_args()
|
| 126 |
+
|
| 127 |
+
device_name = (
|
| 128 |
+
"cuda" if args.device == "auto" and torch.cuda.is_available() else args.device
|
| 129 |
+
)
|
| 130 |
+
if device_name == "auto":
|
| 131 |
+
device_name = "cpu"
|
| 132 |
+
device = torch.device(device_name)
|
| 133 |
+
rows = read_rows(args.sample, args.limit)
|
| 134 |
+
proteins = sorted({row["protein_id"]: row["sequence"] for row in rows}.items())
|
| 135 |
+
ligands = sorted({row["ligand_id"]: row["smiles"] for row in rows}.items())
|
| 136 |
+
|
| 137 |
+
started = time.monotonic()
|
| 138 |
+
protein_tokenizer = AutoTokenizer.from_pretrained(
|
| 139 |
+
args.protein_model, revision=args.protein_revision
|
| 140 |
+
)
|
| 141 |
+
protein_model = (
|
| 142 |
+
AutoModel.from_pretrained(args.protein_model, revision=args.protein_revision)
|
| 143 |
+
.eval()
|
| 144 |
+
.to(device)
|
| 145 |
+
)
|
| 146 |
+
protein_embeddings = encode_proteins(
|
| 147 |
+
protein_model,
|
| 148 |
+
protein_tokenizer,
|
| 149 |
+
proteins,
|
| 150 |
+
args.batch_size,
|
| 151 |
+
device,
|
| 152 |
+
args.protein_chunk_residues,
|
| 153 |
+
)
|
| 154 |
+
del protein_model
|
| 155 |
+
|
| 156 |
+
ligand_tokenizer = AutoTokenizer.from_pretrained(
|
| 157 |
+
args.ligand_model, revision=args.ligand_revision
|
| 158 |
+
)
|
| 159 |
+
ligand_model = (
|
| 160 |
+
AutoModel.from_pretrained(args.ligand_model, revision=args.ligand_revision)
|
| 161 |
+
.eval()
|
| 162 |
+
.to(device)
|
| 163 |
+
)
|
| 164 |
+
ligand_matrix = encode_texts(
|
| 165 |
+
ligand_model,
|
| 166 |
+
ligand_tokenizer,
|
| 167 |
+
[smiles for _, smiles in ligands],
|
| 168 |
+
args.batch_size,
|
| 169 |
+
device,
|
| 170 |
+
)
|
| 171 |
+
ligand_embeddings = {
|
| 172 |
+
entity_id: embedding
|
| 173 |
+
for (entity_id, _), embedding in zip(ligands, ligand_matrix)
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
pair_protein = np.stack(
|
| 177 |
+
[protein_embeddings[row["protein_id"]] for row in rows]
|
| 178 |
+
).astype(np.float32)
|
| 179 |
+
pair_ligand = np.stack(
|
| 180 |
+
[ligand_embeddings[row["ligand_id"]] for row in rows]
|
| 181 |
+
).astype(np.float32)
|
| 182 |
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 183 |
+
np.savez_compressed(
|
| 184 |
+
args.output,
|
| 185 |
+
pair_ids=np.asarray([row["pair_id"] for row in rows]),
|
| 186 |
+
paffinity=np.asarray([row["paffinity"] for row in rows], dtype=np.float32),
|
| 187 |
+
protein=pair_protein,
|
| 188 |
+
ligand=pair_ligand,
|
| 189 |
+
)
|
| 190 |
+
metadata = {
|
| 191 |
+
"rows": len(rows),
|
| 192 |
+
"unique_proteins": len(proteins),
|
| 193 |
+
"unique_ligands": len(ligands),
|
| 194 |
+
"protein_dim": int(pair_protein.shape[1]),
|
| 195 |
+
"ligand_dim": int(pair_ligand.shape[1]),
|
| 196 |
+
"protein_model": args.protein_model,
|
| 197 |
+
"protein_revision": args.protein_revision,
|
| 198 |
+
"ligand_model": args.ligand_model,
|
| 199 |
+
"ligand_revision": args.ligand_revision,
|
| 200 |
+
"protein_chunk_residues": args.protein_chunk_residues,
|
| 201 |
+
"device": str(device),
|
| 202 |
+
"seconds": time.monotonic() - started,
|
| 203 |
+
}
|
| 204 |
+
args.output.with_suffix(".json").write_text(json.dumps(metadata, indent=2) + "\n")
|
| 205 |
+
print(json.dumps(metadata, indent=2))
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
if __name__ == "__main__":
|
| 209 |
+
main()
|
recovery/scripts/overfit_sanity.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Prove the v2 trainable head can memorize a fixed 32-example set."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import math
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
import torch
|
| 13 |
+
|
| 14 |
+
from mitointeract_recovery.model import MitoInteractHead, TargetScaler
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def read_targets(path: Path, count: int) -> torch.Tensor:
|
| 18 |
+
values = []
|
| 19 |
+
with path.open() as handle:
|
| 20 |
+
for line in handle:
|
| 21 |
+
if line.strip():
|
| 22 |
+
values.append(float(json.loads(line)["paffinity"]))
|
| 23 |
+
if len(values) == count:
|
| 24 |
+
break
|
| 25 |
+
if len(values) != count:
|
| 26 |
+
raise ValueError(f"needed {count} examples, found {len(values)}")
|
| 27 |
+
return torch.tensor(values, dtype=torch.float32)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def main() -> None:
|
| 31 |
+
parser = argparse.ArgumentParser()
|
| 32 |
+
parser.add_argument(
|
| 33 |
+
"--sample", type=Path, default=Path("artifacts/dev-10k/sample.jsonl")
|
| 34 |
+
)
|
| 35 |
+
parser.add_argument(
|
| 36 |
+
"--embeddings", type=Path, help="Optional NPZ from embed_sample.py"
|
| 37 |
+
)
|
| 38 |
+
parser.add_argument("--examples", type=int, default=32)
|
| 39 |
+
parser.add_argument("--steps", type=int, default=2000)
|
| 40 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 41 |
+
parser.add_argument("--max-rmse", type=float, default=0.05)
|
| 42 |
+
parser.add_argument(
|
| 43 |
+
"--output", type=Path, default=Path("artifacts/overfit-sanity.json")
|
| 44 |
+
)
|
| 45 |
+
args = parser.parse_args()
|
| 46 |
+
|
| 47 |
+
torch.manual_seed(args.seed)
|
| 48 |
+
torch.set_num_threads(min(torch.get_num_threads(), 8))
|
| 49 |
+
embedding_scope = "synthetic fixed embeddings"
|
| 50 |
+
if args.embeddings:
|
| 51 |
+
arrays = np.load(args.embeddings)
|
| 52 |
+
available = len(arrays["paffinity"])
|
| 53 |
+
if available < args.examples:
|
| 54 |
+
raise ValueError(f"needed {args.examples} embeddings, found {available}")
|
| 55 |
+
targets = torch.from_numpy(arrays["paffinity"][: args.examples]).float()
|
| 56 |
+
protein = torch.from_numpy(arrays["protein"][: args.examples]).float()
|
| 57 |
+
ligand = torch.from_numpy(arrays["ligand"][: args.examples]).float()
|
| 58 |
+
embedding_scope = f"actual pinned frozen encoders from {args.embeddings}"
|
| 59 |
+
else:
|
| 60 |
+
targets = read_targets(args.sample, args.examples)
|
| 61 |
+
# Fixed synthetic embeddings isolate the trainable fusion/head path from
|
| 62 |
+
# encoder downloads and prove optimization can fit a tiny set exactly.
|
| 63 |
+
protein = torch.randn(args.examples, 32)
|
| 64 |
+
ligand = torch.randn(args.examples, 24)
|
| 65 |
+
|
| 66 |
+
scaler = TargetScaler.fit(targets)
|
| 67 |
+
encoded_targets = scaler.encode(targets)
|
| 68 |
+
model = MitoInteractHead(
|
| 69 |
+
protein.shape[1],
|
| 70 |
+
ligand.shape[1],
|
| 71 |
+
projection_dim=64,
|
| 72 |
+
hidden_dim=128,
|
| 73 |
+
dropout=0.0,
|
| 74 |
+
)
|
| 75 |
+
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=0.0)
|
| 76 |
+
|
| 77 |
+
history = []
|
| 78 |
+
for step in range(1, args.steps + 1):
|
| 79 |
+
model.train()
|
| 80 |
+
optimizer.zero_grad(set_to_none=True)
|
| 81 |
+
prediction = model(protein, ligand)
|
| 82 |
+
loss = torch.nn.functional.mse_loss(prediction, encoded_targets)
|
| 83 |
+
loss.backward()
|
| 84 |
+
optimizer.step()
|
| 85 |
+
if step == 1 or step % 100 == 0 or step == args.steps:
|
| 86 |
+
history.append({"step": step, "standardized_mse": float(loss.detach())})
|
| 87 |
+
|
| 88 |
+
model.eval()
|
| 89 |
+
with torch.no_grad():
|
| 90 |
+
decoded = scaler.decode(model(protein, ligand))
|
| 91 |
+
rmse = math.sqrt(float(torch.mean((decoded - targets) ** 2)))
|
| 92 |
+
mae = float(torch.mean(torch.abs(decoded - targets)))
|
| 93 |
+
report = {
|
| 94 |
+
"examples": args.examples,
|
| 95 |
+
"steps": args.steps,
|
| 96 |
+
"seed": args.seed,
|
| 97 |
+
"rmse_paffinity": rmse,
|
| 98 |
+
"mae_paffinity": mae,
|
| 99 |
+
"acceptance_max_rmse_paffinity": args.max_rmse,
|
| 100 |
+
"passed": rmse <= args.max_rmse,
|
| 101 |
+
"target_scaler": {"mean": scaler.mean, "std": scaler.std},
|
| 102 |
+
"history": history,
|
| 103 |
+
"scope": f"trainable head over {embedding_scope}",
|
| 104 |
+
}
|
| 105 |
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 106 |
+
args.output.write_text(json.dumps(report, indent=2) + "\n")
|
| 107 |
+
print(json.dumps(report, indent=2))
|
| 108 |
+
if not report["passed"]:
|
| 109 |
+
raise SystemExit(1)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
if __name__ == "__main__":
|
| 113 |
+
main()
|
recovery/scripts/prepare_sample.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Create a deterministic, deduplicated DTA development sample and split manifests."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import hashlib
|
| 8 |
+
import json
|
| 9 |
+
import math
|
| 10 |
+
import random
|
| 11 |
+
import statistics
|
| 12 |
+
from collections import Counter, defaultdict
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
import pyarrow.parquet as pq
|
| 16 |
+
|
| 17 |
+
from mitointeract_recovery import micromolar_to_paffinity
|
| 18 |
+
from mitointeract_recovery.chemistry import canonicalize_smiles, scaffold_id, stable_id
|
| 19 |
+
from mitointeract_recovery.splits import (
|
| 20 |
+
assign_grouped_splits,
|
| 21 |
+
assign_random_splits,
|
| 22 |
+
assert_group_disjoint,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
COLUMNS = ["seq", "smiles", "smiles_can", "affinity_uM", "neg_log10_affinity_M"]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def stable_pair_id(sequence: str, smiles: str) -> str:
|
| 29 |
+
return stable_id("pair", f"{sequence}\0{smiles}")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def sample_raw_rows(
|
| 33 |
+
parquet_path: Path, candidate_count: int, seed: int
|
| 34 |
+
) -> tuple[list[dict], int]:
|
| 35 |
+
parquet = pq.ParquetFile(parquet_path)
|
| 36 |
+
total_rows = parquet.metadata.num_rows
|
| 37 |
+
if candidate_count > total_rows:
|
| 38 |
+
candidate_count = total_rows
|
| 39 |
+
wanted = sorted(random.Random(seed).sample(range(total_rows), candidate_count))
|
| 40 |
+
selected: list[dict] = []
|
| 41 |
+
pointer = 0
|
| 42 |
+
offset = 0
|
| 43 |
+
|
| 44 |
+
for batch in parquet.iter_batches(batch_size=8192, columns=COLUMNS):
|
| 45 |
+
batch_end = offset + batch.num_rows
|
| 46 |
+
local_indices: list[int] = []
|
| 47 |
+
while pointer < len(wanted) and wanted[pointer] < batch_end:
|
| 48 |
+
local_indices.append(wanted[pointer] - offset)
|
| 49 |
+
pointer += 1
|
| 50 |
+
if local_indices:
|
| 51 |
+
data = batch.to_pydict()
|
| 52 |
+
for local in local_indices:
|
| 53 |
+
selected.append({column: data[column][local] for column in COLUMNS})
|
| 54 |
+
offset = batch_end
|
| 55 |
+
if pointer == len(wanted):
|
| 56 |
+
break
|
| 57 |
+
|
| 58 |
+
if len(selected) != candidate_count:
|
| 59 |
+
raise RuntimeError(
|
| 60 |
+
f"requested {candidate_count} rows but selected {len(selected)}"
|
| 61 |
+
)
|
| 62 |
+
return selected, total_rows
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def normalize_and_deduplicate(
|
| 66 |
+
raw_rows: list[dict], sample_size: int, seed: int
|
| 67 |
+
) -> tuple[list[dict], Counter]:
|
| 68 |
+
rejected = Counter()
|
| 69 |
+
pairs: dict[str, dict] = {}
|
| 70 |
+
targets: dict[str, list[float]] = defaultdict(list)
|
| 71 |
+
|
| 72 |
+
for raw in raw_rows:
|
| 73 |
+
sequence = "".join(str(raw.get("seq") or "").split()).upper()
|
| 74 |
+
source_smiles = str(raw.get("smiles_can") or raw.get("smiles") or "").strip()
|
| 75 |
+
if not sequence:
|
| 76 |
+
rejected["empty_sequence"] += 1
|
| 77 |
+
continue
|
| 78 |
+
try:
|
| 79 |
+
canonical = canonicalize_smiles(source_smiles)
|
| 80 |
+
except ValueError:
|
| 81 |
+
rejected["invalid_smiles"] += 1
|
| 82 |
+
continue
|
| 83 |
+
try:
|
| 84 |
+
affinity_um = float(raw["affinity_uM"])
|
| 85 |
+
published_paffinity = float(raw["neg_log10_affinity_M"])
|
| 86 |
+
calculated_paffinity = micromolar_to_paffinity(affinity_um)
|
| 87 |
+
except (TypeError, ValueError, OverflowError):
|
| 88 |
+
rejected["invalid_affinity"] += 1
|
| 89 |
+
continue
|
| 90 |
+
if (
|
| 91 |
+
not math.isfinite(published_paffinity)
|
| 92 |
+
or abs(calculated_paffinity - published_paffinity) > 1e-4
|
| 93 |
+
):
|
| 94 |
+
rejected["unit_mismatch"] += 1
|
| 95 |
+
continue
|
| 96 |
+
|
| 97 |
+
pair_id = stable_pair_id(sequence, canonical)
|
| 98 |
+
if pair_id not in pairs:
|
| 99 |
+
pairs[pair_id] = {
|
| 100 |
+
"pair_id": pair_id,
|
| 101 |
+
"protein_id": stable_id("protein", sequence),
|
| 102 |
+
"ligand_id": stable_id("ligand", canonical),
|
| 103 |
+
"scaffold_id": scaffold_id(canonical),
|
| 104 |
+
"sequence": sequence,
|
| 105 |
+
"smiles": canonical,
|
| 106 |
+
"protein_length": len(sequence),
|
| 107 |
+
"smiles_length": len(canonical),
|
| 108 |
+
}
|
| 109 |
+
targets[pair_id].append(published_paffinity)
|
| 110 |
+
|
| 111 |
+
normalized: list[dict] = []
|
| 112 |
+
for pair_id, row in pairs.items():
|
| 113 |
+
values = targets[pair_id]
|
| 114 |
+
normalized.append(
|
| 115 |
+
{
|
| 116 |
+
**row,
|
| 117 |
+
"paffinity": statistics.median(values),
|
| 118 |
+
"replicate_count": len(values),
|
| 119 |
+
"replicate_paffinity_range": max(values) - min(values),
|
| 120 |
+
}
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
normalized.sort(
|
| 124 |
+
key=lambda row: hashlib.sha256(f"{seed}:{row['pair_id']}".encode()).hexdigest()
|
| 125 |
+
)
|
| 126 |
+
if len(normalized) < sample_size:
|
| 127 |
+
raise RuntimeError(
|
| 128 |
+
f"only {len(normalized)} valid unique pairs remained; increase --candidate-multiplier"
|
| 129 |
+
)
|
| 130 |
+
return normalized[:sample_size], rejected
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def write_jsonl(path: Path, rows: list[dict]) -> None:
|
| 134 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 135 |
+
with path.open("w") as handle:
|
| 136 |
+
for row in rows:
|
| 137 |
+
handle.write(json.dumps(row, sort_keys=True) + "\n")
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def target_stats(rows: list[dict], assignments: dict[str, str]) -> dict:
|
| 141 |
+
result = {}
|
| 142 |
+
for split in ("train", "validation", "test"):
|
| 143 |
+
values = [
|
| 144 |
+
row["paffinity"] for row in rows if assignments[row["pair_id"]] == split
|
| 145 |
+
]
|
| 146 |
+
result[split] = {
|
| 147 |
+
"rows": len(values),
|
| 148 |
+
"mean_paffinity": statistics.mean(values),
|
| 149 |
+
"std_paffinity": statistics.stdev(values) if len(values) > 1 else 0.0,
|
| 150 |
+
"min_paffinity": min(values),
|
| 151 |
+
"max_paffinity": max(values),
|
| 152 |
+
}
|
| 153 |
+
return result
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def main() -> None:
|
| 157 |
+
parser = argparse.ArgumentParser()
|
| 158 |
+
parser.add_argument("--parquet", type=Path, required=True)
|
| 159 |
+
parser.add_argument(
|
| 160 |
+
"--dataset-revision",
|
| 161 |
+
default="11e49b7ece33d62afd7f65bc05ce60ad37f9ba7b",
|
| 162 |
+
)
|
| 163 |
+
parser.add_argument("--sample-size", type=int, default=10_000)
|
| 164 |
+
parser.add_argument("--candidate-multiplier", type=int, default=3)
|
| 165 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 166 |
+
parser.add_argument("--output-dir", type=Path, default=Path("artifacts/dev-10k"))
|
| 167 |
+
args = parser.parse_args()
|
| 168 |
+
if args.sample_size < 100:
|
| 169 |
+
raise ValueError("sample size must be at least 100")
|
| 170 |
+
|
| 171 |
+
raw_rows, source_rows = sample_raw_rows(
|
| 172 |
+
args.parquet,
|
| 173 |
+
args.sample_size * args.candidate_multiplier,
|
| 174 |
+
args.seed,
|
| 175 |
+
)
|
| 176 |
+
rows, rejected = normalize_and_deduplicate(raw_rows, args.sample_size, args.seed)
|
| 177 |
+
|
| 178 |
+
manifests = {
|
| 179 |
+
"random_pair": assign_random_splits(rows, seed=args.seed),
|
| 180 |
+
"cold_protein_exact": assign_grouped_splits(rows, "protein_id", seed=args.seed),
|
| 181 |
+
"cold_ligand_scaffold": assign_grouped_splits(
|
| 182 |
+
rows, "scaffold_id", seed=args.seed
|
| 183 |
+
),
|
| 184 |
+
}
|
| 185 |
+
assert_group_disjoint(rows, manifests["cold_protein_exact"], "protein_id")
|
| 186 |
+
assert_group_disjoint(rows, manifests["cold_ligand_scaffold"], "scaffold_id")
|
| 187 |
+
|
| 188 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 189 |
+
write_jsonl(args.output_dir / "sample.jsonl", rows)
|
| 190 |
+
for name, assignments in manifests.items():
|
| 191 |
+
write_jsonl(
|
| 192 |
+
args.output_dir / f"split-{name}.jsonl",
|
| 193 |
+
[
|
| 194 |
+
{"pair_id": pair_id, "split": split}
|
| 195 |
+
for pair_id, split in sorted(assignments.items())
|
| 196 |
+
],
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
with args.parquet.open("rb") as handle:
|
| 200 |
+
source_sha256 = hashlib.file_digest(handle, "sha256").hexdigest()
|
| 201 |
+
audit = {
|
| 202 |
+
"source_parquet": str(args.parquet),
|
| 203 |
+
"source_sha256": source_sha256,
|
| 204 |
+
"dataset": "jglaser/binding_affinity",
|
| 205 |
+
"dataset_revision": args.dataset_revision,
|
| 206 |
+
"source_rows": source_rows,
|
| 207 |
+
"candidate_rows": len(raw_rows),
|
| 208 |
+
"sample_rows": len(rows),
|
| 209 |
+
"seed": args.seed,
|
| 210 |
+
"target": {"column": "neg_log10_affinity_M", "unit": "pAffinity"},
|
| 211 |
+
"rejected": dict(rejected),
|
| 212 |
+
"unique_proteins": len({row["protein_id"] for row in rows}),
|
| 213 |
+
"unique_ligands": len({row["ligand_id"] for row in rows}),
|
| 214 |
+
"unique_scaffold_groups": len({row["scaffold_id"] for row in rows}),
|
| 215 |
+
"duplicate_measurements_in_sample": sum(
|
| 216 |
+
row["replicate_count"] - 1 for row in rows
|
| 217 |
+
),
|
| 218 |
+
"protein_length": {
|
| 219 |
+
"min": min(row["protein_length"] for row in rows),
|
| 220 |
+
"median": statistics.median(row["protein_length"] for row in rows),
|
| 221 |
+
"max": max(row["protein_length"] for row in rows),
|
| 222 |
+
"over_512": sum(row["protein_length"] > 512 for row in rows),
|
| 223 |
+
},
|
| 224 |
+
"splits": {
|
| 225 |
+
name: target_stats(rows, assignments)
|
| 226 |
+
for name, assignments in manifests.items()
|
| 227 |
+
},
|
| 228 |
+
"limitations": [
|
| 229 |
+
"the source target can combine Ki, Kd, IC50, and EC50; pAffinity is not pKd",
|
| 230 |
+
"assay type, relation operator, and source provenance are absent from the combined Parquet",
|
| 231 |
+
"cold_protein_exact is entity-disjoint but not sequence-similarity-clustered",
|
| 232 |
+
"scaffold groups use Bemis-Murcko with exact-molecule fallback for acyclic ligands",
|
| 233 |
+
"this development sample is not an external biological benchmark",
|
| 234 |
+
],
|
| 235 |
+
}
|
| 236 |
+
(args.output_dir / "audit.json").write_text(json.dumps(audit, indent=2) + "\n")
|
| 237 |
+
print(json.dumps(audit, indent=2))
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
if __name__ == "__main__":
|
| 241 |
+
main()
|
recovery/scripts/run_baselines.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Benchmark leakage-auditing character n-gram ridge baselines."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import importlib.metadata
|
| 8 |
+
import json
|
| 9 |
+
import time
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
from scipy.sparse import hstack
|
| 14 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 15 |
+
from sklearn.linear_model import Ridge
|
| 16 |
+
|
| 17 |
+
from mitointeract_recovery.metrics import regression_metrics
|
| 18 |
+
|
| 19 |
+
ALPHAS = (0.1, 1.0, 10.0, 100.0)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def read_jsonl(path: Path) -> list[dict]:
|
| 23 |
+
with path.open() as handle:
|
| 24 |
+
return [json.loads(line) for line in handle if line.strip()]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def read_manifest(path: Path) -> dict[str, str]:
|
| 28 |
+
return {row["pair_id"]: row["split"] for row in read_jsonl(path)}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def partition(rows: list[dict], manifest: dict[str, str]) -> dict[str, list[dict]]:
|
| 32 |
+
result = {"train": [], "validation": [], "test": []}
|
| 33 |
+
for row in rows:
|
| 34 |
+
result[manifest[row["pair_id"]]].append(row)
|
| 35 |
+
return result
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def targets(rows: list[dict]) -> np.ndarray:
|
| 39 |
+
return np.asarray([row["paffinity"] for row in rows], dtype=np.float64)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def select_ridge(
|
| 43 |
+
train_x,
|
| 44 |
+
train_y: np.ndarray,
|
| 45 |
+
validation_x,
|
| 46 |
+
validation_y: np.ndarray,
|
| 47 |
+
) -> tuple[Ridge, float, list[dict]]:
|
| 48 |
+
trials = []
|
| 49 |
+
best = None
|
| 50 |
+
for alpha in ALPHAS:
|
| 51 |
+
model = Ridge(alpha=alpha, solver="lsqr", tol=1e-4)
|
| 52 |
+
model.fit(train_x, train_y)
|
| 53 |
+
predictions = model.predict(validation_x)
|
| 54 |
+
metrics = regression_metrics(validation_y, predictions)
|
| 55 |
+
trials.append({"alpha": alpha, "metrics": metrics})
|
| 56 |
+
if best is None or metrics["rmse"] < best[0]:
|
| 57 |
+
best = (metrics["rmse"], model, alpha)
|
| 58 |
+
return best[1], best[2], trials
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def evaluate_feature_set(
|
| 62 |
+
name: str,
|
| 63 |
+
train_x,
|
| 64 |
+
validation_x,
|
| 65 |
+
test_x,
|
| 66 |
+
train_y: np.ndarray,
|
| 67 |
+
validation_y: np.ndarray,
|
| 68 |
+
test_y: np.ndarray,
|
| 69 |
+
) -> dict:
|
| 70 |
+
started = time.monotonic()
|
| 71 |
+
model, alpha, trials = select_ridge(train_x, train_y, validation_x, validation_y)
|
| 72 |
+
return {
|
| 73 |
+
"name": name,
|
| 74 |
+
"selected_alpha": alpha,
|
| 75 |
+
"validation_trials": trials,
|
| 76 |
+
"validation": regression_metrics(validation_y, model.predict(validation_x)),
|
| 77 |
+
"test": regression_metrics(test_y, model.predict(test_x)),
|
| 78 |
+
"fit_and_eval_seconds": time.monotonic() - started,
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def benchmark_split(rows: list[dict], manifest_path: Path) -> dict:
|
| 83 |
+
manifest = read_manifest(manifest_path)
|
| 84 |
+
splits = partition(rows, manifest)
|
| 85 |
+
train_y = targets(splits["train"])
|
| 86 |
+
validation_y = targets(splits["validation"])
|
| 87 |
+
test_y = targets(splits["test"])
|
| 88 |
+
|
| 89 |
+
mean = float(train_y.mean())
|
| 90 |
+
result = {
|
| 91 |
+
"rows": {name: len(values) for name, values in splits.items()},
|
| 92 |
+
"mean_baseline": {
|
| 93 |
+
"prediction": mean,
|
| 94 |
+
"validation": regression_metrics(
|
| 95 |
+
validation_y, np.full_like(validation_y, mean)
|
| 96 |
+
),
|
| 97 |
+
"test": regression_metrics(test_y, np.full_like(test_y, mean)),
|
| 98 |
+
},
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
protein_vectorizer = TfidfVectorizer(
|
| 102 |
+
analyzer="char",
|
| 103 |
+
ngram_range=(3, 3),
|
| 104 |
+
lowercase=False,
|
| 105 |
+
min_df=2,
|
| 106 |
+
max_features=4096,
|
| 107 |
+
sublinear_tf=True,
|
| 108 |
+
dtype=np.float32,
|
| 109 |
+
)
|
| 110 |
+
ligand_vectorizer = TfidfVectorizer(
|
| 111 |
+
analyzer="char",
|
| 112 |
+
ngram_range=(2, 5),
|
| 113 |
+
lowercase=False,
|
| 114 |
+
min_df=2,
|
| 115 |
+
max_features=4096,
|
| 116 |
+
sublinear_tf=True,
|
| 117 |
+
dtype=np.float32,
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
protein_train = protein_vectorizer.fit_transform(
|
| 121 |
+
[row["sequence"] for row in splits["train"]]
|
| 122 |
+
)
|
| 123 |
+
protein_validation = protein_vectorizer.transform(
|
| 124 |
+
[row["sequence"] for row in splits["validation"]]
|
| 125 |
+
)
|
| 126 |
+
protein_test = protein_vectorizer.transform(
|
| 127 |
+
[row["sequence"] for row in splits["test"]]
|
| 128 |
+
)
|
| 129 |
+
ligand_train = ligand_vectorizer.fit_transform(
|
| 130 |
+
[row["smiles"] for row in splits["train"]]
|
| 131 |
+
)
|
| 132 |
+
ligand_validation = ligand_vectorizer.transform(
|
| 133 |
+
[row["smiles"] for row in splits["validation"]]
|
| 134 |
+
)
|
| 135 |
+
ligand_test = ligand_vectorizer.transform([row["smiles"] for row in splits["test"]])
|
| 136 |
+
|
| 137 |
+
result["feature_dimensions"] = {
|
| 138 |
+
"protein": protein_train.shape[1],
|
| 139 |
+
"ligand": ligand_train.shape[1],
|
| 140 |
+
}
|
| 141 |
+
result["models"] = [
|
| 142 |
+
evaluate_feature_set(
|
| 143 |
+
"protein_char3_ridge",
|
| 144 |
+
protein_train,
|
| 145 |
+
protein_validation,
|
| 146 |
+
protein_test,
|
| 147 |
+
train_y,
|
| 148 |
+
validation_y,
|
| 149 |
+
test_y,
|
| 150 |
+
),
|
| 151 |
+
evaluate_feature_set(
|
| 152 |
+
"ligand_char2_5_ridge",
|
| 153 |
+
ligand_train,
|
| 154 |
+
ligand_validation,
|
| 155 |
+
ligand_test,
|
| 156 |
+
train_y,
|
| 157 |
+
validation_y,
|
| 158 |
+
test_y,
|
| 159 |
+
),
|
| 160 |
+
evaluate_feature_set(
|
| 161 |
+
"combined_char_ridge",
|
| 162 |
+
hstack([protein_train, ligand_train], format="csr"),
|
| 163 |
+
hstack([protein_validation, ligand_validation], format="csr"),
|
| 164 |
+
hstack([protein_test, ligand_test], format="csr"),
|
| 165 |
+
train_y,
|
| 166 |
+
validation_y,
|
| 167 |
+
test_y,
|
| 168 |
+
),
|
| 169 |
+
]
|
| 170 |
+
return result
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def main() -> None:
|
| 174 |
+
parser = argparse.ArgumentParser()
|
| 175 |
+
parser.add_argument("--data-dir", type=Path, default=Path("artifacts/dev-10k"))
|
| 176 |
+
parser.add_argument(
|
| 177 |
+
"--output", type=Path, default=Path("artifacts/dev-10k/baselines.json")
|
| 178 |
+
)
|
| 179 |
+
args = parser.parse_args()
|
| 180 |
+
rows = read_jsonl(args.data_dir / "sample.jsonl")
|
| 181 |
+
|
| 182 |
+
started = time.monotonic()
|
| 183 |
+
report = {
|
| 184 |
+
"sample_rows": len(rows),
|
| 185 |
+
"target": "pAffinity",
|
| 186 |
+
"packages": {
|
| 187 |
+
package: importlib.metadata.version(package)
|
| 188 |
+
for package in ("numpy", "scipy", "scikit-learn")
|
| 189 |
+
},
|
| 190 |
+
"splits": {},
|
| 191 |
+
}
|
| 192 |
+
for manifest_path in sorted(args.data_dir.glob("split-*.jsonl")):
|
| 193 |
+
split_name = manifest_path.stem.removeprefix("split-")
|
| 194 |
+
report["splits"][split_name] = benchmark_split(rows, manifest_path)
|
| 195 |
+
report["total_seconds"] = time.monotonic() - started
|
| 196 |
+
|
| 197 |
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 198 |
+
args.output.write_text(json.dumps(report, indent=2) + "\n")
|
| 199 |
+
print(json.dumps(report, indent=2))
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
if __name__ == "__main__":
|
| 203 |
+
main()
|
recovery/scripts/train_head.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Train and evaluate the MitoInteract v2 head over cached frozen embeddings."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import copy
|
| 8 |
+
import json
|
| 9 |
+
import math
|
| 10 |
+
import random
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
import torch
|
| 15 |
+
from safetensors.torch import save_file
|
| 16 |
+
from torch.utils.data import DataLoader, TensorDataset
|
| 17 |
+
|
| 18 |
+
from mitointeract_recovery.metrics import regression_metrics
|
| 19 |
+
from mitointeract_recovery.model import MitoInteractHead, TargetScaler
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def read_manifest(path: Path) -> dict[str, str]:
|
| 23 |
+
with path.open() as handle:
|
| 24 |
+
return {
|
| 25 |
+
row["pair_id"]: row["split"]
|
| 26 |
+
for row in (json.loads(line) for line in handle if line.strip())
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def evaluate(
|
| 31 |
+
model: MitoInteractHead,
|
| 32 |
+
protein: torch.Tensor,
|
| 33 |
+
ligand: torch.Tensor,
|
| 34 |
+
targets: torch.Tensor,
|
| 35 |
+
indices: np.ndarray,
|
| 36 |
+
scaler: TargetScaler,
|
| 37 |
+
) -> dict:
|
| 38 |
+
model.eval()
|
| 39 |
+
with torch.inference_mode():
|
| 40 |
+
prediction = (
|
| 41 |
+
scaler.decode(model(protein[indices], ligand[indices])).cpu().numpy()
|
| 42 |
+
)
|
| 43 |
+
return regression_metrics(targets[indices].cpu().numpy(), prediction)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def main() -> None:
|
| 47 |
+
parser = argparse.ArgumentParser()
|
| 48 |
+
parser.add_argument("--embeddings", type=Path, required=True)
|
| 49 |
+
parser.add_argument("--manifest", type=Path, required=True)
|
| 50 |
+
parser.add_argument("--output-dir", type=Path, default=Path("artifacts/v2-head"))
|
| 51 |
+
parser.add_argument("--epochs", type=int, default=100)
|
| 52 |
+
parser.add_argument("--batch-size", type=int, default=256)
|
| 53 |
+
parser.add_argument("--learning-rate", type=float, default=1e-3)
|
| 54 |
+
parser.add_argument("--weight-decay", type=float, default=1e-4)
|
| 55 |
+
parser.add_argument("--warmup-ratio", type=float, default=0.05)
|
| 56 |
+
parser.add_argument("--patience", type=int, default=10)
|
| 57 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 58 |
+
parser.add_argument("--projection-dim", type=int, default=256)
|
| 59 |
+
parser.add_argument("--hidden-dim", type=int, default=512)
|
| 60 |
+
parser.add_argument("--dropout", type=float, default=0.1)
|
| 61 |
+
parser.add_argument("--device", choices=["auto", "cpu", "cuda"], default="auto")
|
| 62 |
+
args = parser.parse_args()
|
| 63 |
+
|
| 64 |
+
random.seed(args.seed)
|
| 65 |
+
np.random.seed(args.seed)
|
| 66 |
+
torch.manual_seed(args.seed)
|
| 67 |
+
device_name = (
|
| 68 |
+
"cuda" if args.device == "auto" and torch.cuda.is_available() else args.device
|
| 69 |
+
)
|
| 70 |
+
if device_name == "auto":
|
| 71 |
+
device_name = "cpu"
|
| 72 |
+
device = torch.device(device_name)
|
| 73 |
+
|
| 74 |
+
arrays = np.load(args.embeddings)
|
| 75 |
+
pair_ids = arrays["pair_ids"].astype(str)
|
| 76 |
+
protein = torch.from_numpy(arrays["protein"]).float().to(device)
|
| 77 |
+
ligand = torch.from_numpy(arrays["ligand"]).float().to(device)
|
| 78 |
+
targets = torch.from_numpy(arrays["paffinity"]).float().to(device)
|
| 79 |
+
manifest = read_manifest(args.manifest)
|
| 80 |
+
split_indices = {
|
| 81 |
+
split: np.asarray(
|
| 82 |
+
[
|
| 83 |
+
index
|
| 84 |
+
for index, pair_id in enumerate(pair_ids)
|
| 85 |
+
if manifest.get(pair_id) == split
|
| 86 |
+
]
|
| 87 |
+
)
|
| 88 |
+
for split in ("train", "validation", "test")
|
| 89 |
+
}
|
| 90 |
+
if any(not len(indices) for indices in split_indices.values()):
|
| 91 |
+
raise ValueError(
|
| 92 |
+
f"all splits must be non-empty: { {k: len(v) for k, v in split_indices.items()} }"
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
scaler = TargetScaler.fit(targets[split_indices["train"]])
|
| 96 |
+
encoded_targets = scaler.encode(targets)
|
| 97 |
+
train_dataset = TensorDataset(
|
| 98 |
+
protein[split_indices["train"]],
|
| 99 |
+
ligand[split_indices["train"]],
|
| 100 |
+
encoded_targets[split_indices["train"]],
|
| 101 |
+
)
|
| 102 |
+
generator = torch.Generator().manual_seed(args.seed)
|
| 103 |
+
loader = DataLoader(
|
| 104 |
+
train_dataset,
|
| 105 |
+
batch_size=min(args.batch_size, len(train_dataset)),
|
| 106 |
+
shuffle=True,
|
| 107 |
+
generator=generator,
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
model = MitoInteractHead(
|
| 111 |
+
protein.shape[1],
|
| 112 |
+
ligand.shape[1],
|
| 113 |
+
projection_dim=args.projection_dim,
|
| 114 |
+
hidden_dim=args.hidden_dim,
|
| 115 |
+
dropout=args.dropout,
|
| 116 |
+
).to(device)
|
| 117 |
+
optimizer = torch.optim.AdamW(
|
| 118 |
+
model.parameters(),
|
| 119 |
+
lr=args.learning_rate,
|
| 120 |
+
weight_decay=args.weight_decay,
|
| 121 |
+
)
|
| 122 |
+
total_steps = max(1, args.epochs * len(loader))
|
| 123 |
+
warmup_steps = max(1, round(total_steps * args.warmup_ratio))
|
| 124 |
+
|
| 125 |
+
def lr_factor(step: int) -> float:
|
| 126 |
+
if step < warmup_steps:
|
| 127 |
+
return (step + 1) / warmup_steps
|
| 128 |
+
progress = (step - warmup_steps) / max(1, total_steps - warmup_steps)
|
| 129 |
+
return 0.5 * (1 + math.cos(math.pi * progress))
|
| 130 |
+
|
| 131 |
+
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_factor)
|
| 132 |
+
best_state = None
|
| 133 |
+
best_validation = math.inf
|
| 134 |
+
best_epoch = 0
|
| 135 |
+
remaining_patience = args.patience
|
| 136 |
+
history = []
|
| 137 |
+
|
| 138 |
+
for epoch in range(1, args.epochs + 1):
|
| 139 |
+
model.train()
|
| 140 |
+
losses = []
|
| 141 |
+
for batch_protein, batch_ligand, batch_target in loader:
|
| 142 |
+
optimizer.zero_grad(set_to_none=True)
|
| 143 |
+
prediction = model(batch_protein, batch_ligand)
|
| 144 |
+
loss = torch.nn.functional.mse_loss(prediction, batch_target)
|
| 145 |
+
loss.backward()
|
| 146 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 147 |
+
optimizer.step()
|
| 148 |
+
scheduler.step()
|
| 149 |
+
losses.append(float(loss.detach()))
|
| 150 |
+
|
| 151 |
+
validation = evaluate(
|
| 152 |
+
model,
|
| 153 |
+
protein,
|
| 154 |
+
ligand,
|
| 155 |
+
targets,
|
| 156 |
+
split_indices["validation"],
|
| 157 |
+
scaler,
|
| 158 |
+
)
|
| 159 |
+
history.append(
|
| 160 |
+
{
|
| 161 |
+
"epoch": epoch,
|
| 162 |
+
"train_standardized_mse": float(np.mean(losses)),
|
| 163 |
+
"validation": validation,
|
| 164 |
+
"learning_rate": optimizer.param_groups[0]["lr"],
|
| 165 |
+
}
|
| 166 |
+
)
|
| 167 |
+
if validation["rmse"] < best_validation:
|
| 168 |
+
best_validation = validation["rmse"]
|
| 169 |
+
best_epoch = epoch
|
| 170 |
+
best_state = copy.deepcopy(
|
| 171 |
+
{key: value.detach().cpu() for key, value in model.state_dict().items()}
|
| 172 |
+
)
|
| 173 |
+
remaining_patience = args.patience
|
| 174 |
+
else:
|
| 175 |
+
remaining_patience -= 1
|
| 176 |
+
if remaining_patience <= 0:
|
| 177 |
+
break
|
| 178 |
+
|
| 179 |
+
model.load_state_dict(best_state)
|
| 180 |
+
test_metrics = evaluate(
|
| 181 |
+
model, protein, ligand, targets, split_indices["test"], scaler
|
| 182 |
+
)
|
| 183 |
+
validation_metrics = evaluate(
|
| 184 |
+
model, protein, ligand, targets, split_indices["validation"], scaler
|
| 185 |
+
)
|
| 186 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 187 |
+
save_file(best_state, args.output_dir / "model.safetensors")
|
| 188 |
+
|
| 189 |
+
encoder_metadata_path = args.embeddings.with_suffix(".json")
|
| 190 |
+
encoder_metadata = (
|
| 191 |
+
json.loads(encoder_metadata_path.read_text())
|
| 192 |
+
if encoder_metadata_path.exists()
|
| 193 |
+
else None
|
| 194 |
+
)
|
| 195 |
+
report = {
|
| 196 |
+
"embedding_file": str(args.embeddings),
|
| 197 |
+
"manifest": str(args.manifest),
|
| 198 |
+
"rows": {name: len(indices) for name, indices in split_indices.items()},
|
| 199 |
+
"seed": args.seed,
|
| 200 |
+
"device": str(device),
|
| 201 |
+
"best_epoch": best_epoch,
|
| 202 |
+
"target_scaler": {"mean": scaler.mean, "std": scaler.std},
|
| 203 |
+
"head": {
|
| 204 |
+
"protein_dim": int(protein.shape[1]),
|
| 205 |
+
"ligand_dim": int(ligand.shape[1]),
|
| 206 |
+
"projection_dim": args.projection_dim,
|
| 207 |
+
"hidden_dim": args.hidden_dim,
|
| 208 |
+
"dropout": args.dropout,
|
| 209 |
+
},
|
| 210 |
+
"optimizer": {
|
| 211 |
+
"name": "AdamW",
|
| 212 |
+
"learning_rate": args.learning_rate,
|
| 213 |
+
"weight_decay": args.weight_decay,
|
| 214 |
+
"warmup_ratio": args.warmup_ratio,
|
| 215 |
+
"warmup_steps": warmup_steps,
|
| 216 |
+
"total_steps": total_steps,
|
| 217 |
+
},
|
| 218 |
+
"validation": validation_metrics,
|
| 219 |
+
"test": test_metrics,
|
| 220 |
+
"encoders": encoder_metadata,
|
| 221 |
+
"history": history,
|
| 222 |
+
}
|
| 223 |
+
(args.output_dir / "report.json").write_text(json.dumps(report, indent=2) + "\n")
|
| 224 |
+
(args.output_dir / "config.json").write_text(
|
| 225 |
+
json.dumps(
|
| 226 |
+
{
|
| 227 |
+
"format": "MitoInteract-v2-head",
|
| 228 |
+
"target": "pAffinity",
|
| 229 |
+
"target_scaler": report["target_scaler"],
|
| 230 |
+
"head": report["head"],
|
| 231 |
+
"encoders": encoder_metadata,
|
| 232 |
+
},
|
| 233 |
+
indent=2,
|
| 234 |
+
)
|
| 235 |
+
+ "\n"
|
| 236 |
+
)
|
| 237 |
+
print(
|
| 238 |
+
json.dumps(
|
| 239 |
+
{key: value for key, value in report.items() if key != "history"}, indent=2
|
| 240 |
+
)
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
if __name__ == "__main__":
|
| 245 |
+
main()
|
recovery/src/mitointeract_recovery/__init__.py
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
-
from .affinity import
|
| 2 |
|
| 3 |
-
__all__ = ["
|
|
|
|
| 1 |
+
from .affinity import micromolar_to_paffinity, paffinity_to_micromolar
|
| 2 |
|
| 3 |
+
__all__ = ["micromolar_to_paffinity", "paffinity_to_micromolar"]
|
recovery/src/mitointeract_recovery/affinity.py
CHANGED
|
@@ -1,18 +1,17 @@
|
|
| 1 |
-
"""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
|
| 4 |
import math
|
| 5 |
|
| 6 |
|
| 7 |
-
def
|
| 8 |
-
"""Convert
|
| 9 |
if not math.isfinite(affinity_um) or affinity_um <= 0:
|
| 10 |
-
raise ValueError("affinity_um must be
|
| 11 |
return -math.log10(affinity_um * 1e-6)
|
| 12 |
|
| 13 |
|
| 14 |
-
def
|
| 15 |
-
"""Convert
|
| 16 |
-
if not math.isfinite(
|
| 17 |
-
raise ValueError("
|
| 18 |
-
return (10 ** (-
|
|
|
|
| 1 |
+
"""Conversions for the dataset's generic molar affinity scale."""
|
|
|
|
| 2 |
|
| 3 |
import math
|
| 4 |
|
| 5 |
|
| 6 |
+
def micromolar_to_paffinity(affinity_um: float) -> float:
|
| 7 |
+
"""Convert a positive micromolar affinity value to -log10(value in M)."""
|
| 8 |
if not math.isfinite(affinity_um) or affinity_um <= 0:
|
| 9 |
+
raise ValueError("affinity_um must be finite and greater than zero")
|
| 10 |
return -math.log10(affinity_um * 1e-6)
|
| 11 |
|
| 12 |
|
| 13 |
+
def paffinity_to_micromolar(paffinity: float) -> float:
|
| 14 |
+
"""Convert generic pAffinity to a micromolar affinity value."""
|
| 15 |
+
if not math.isfinite(paffinity):
|
| 16 |
+
raise ValueError("paffinity must be finite")
|
| 17 |
+
return (10 ** (-paffinity)) * 1e6
|
recovery/src/mitointeract_recovery/chemistry.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ligand canonicalization and grouping helpers."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
|
| 7 |
+
from rdkit import Chem
|
| 8 |
+
from rdkit.Chem.Scaffolds import MurckoScaffold
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def canonicalize_smiles(smiles: str) -> str:
|
| 12 |
+
"""Return isomeric canonical SMILES or raise for an invalid molecule."""
|
| 13 |
+
molecule = Chem.MolFromSmiles(smiles)
|
| 14 |
+
if molecule is None:
|
| 15 |
+
raise ValueError(f"invalid SMILES: {smiles!r}")
|
| 16 |
+
return Chem.MolToSmiles(molecule, canonical=True, isomericSmiles=True)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def stable_id(namespace: str, value: str) -> str:
|
| 20 |
+
digest = hashlib.sha256(value.encode("utf-8")).hexdigest()[:20]
|
| 21 |
+
return f"{namespace}_{digest}"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def scaffold_id(smiles: str) -> str:
|
| 25 |
+
"""Return a stable Bemis-Murcko scaffold group, preserving acyclic diversity."""
|
| 26 |
+
canonical = canonicalize_smiles(smiles)
|
| 27 |
+
molecule = Chem.MolFromSmiles(canonical)
|
| 28 |
+
scaffold = MurckoScaffold.GetScaffoldForMol(molecule)
|
| 29 |
+
scaffold_smiles = Chem.MolToSmiles(scaffold, canonical=True, isomericSmiles=True)
|
| 30 |
+
if scaffold_smiles:
|
| 31 |
+
return stable_id("scaffold", scaffold_smiles)
|
| 32 |
+
# Empty Murcko scaffolds are common for acyclic molecules. Treat each
|
| 33 |
+
# canonical molecule as its own group instead of collapsing all of them.
|
| 34 |
+
return stable_id("acyclic", canonical)
|
recovery/src/mitointeract_recovery/metrics.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Regression metrics with JSON-safe handling for undefined correlations."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
from collections.abc import Sequence
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
from scipy.stats import pearsonr, spearmanr
|
| 10 |
+
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _correlation(function, y_true: np.ndarray, y_pred: np.ndarray) -> float | None:
|
| 14 |
+
if len(y_true) < 2 or np.ptp(y_true) == 0 or np.ptp(y_pred) == 0:
|
| 15 |
+
return None
|
| 16 |
+
value = float(function(y_true, y_pred).statistic)
|
| 17 |
+
return value if math.isfinite(value) else None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def regression_metrics(
|
| 21 |
+
y_true: Sequence[float], y_pred: Sequence[float]
|
| 22 |
+
) -> dict[str, float | None]:
|
| 23 |
+
truth = np.asarray(y_true, dtype=np.float64)
|
| 24 |
+
predicted = np.asarray(y_pred, dtype=np.float64)
|
| 25 |
+
if truth.shape != predicted.shape or truth.ndim != 1 or not len(truth):
|
| 26 |
+
raise ValueError(
|
| 27 |
+
"y_true and y_pred must be non-empty one-dimensional sequences of equal length"
|
| 28 |
+
)
|
| 29 |
+
if not np.isfinite(truth).all() or not np.isfinite(predicted).all():
|
| 30 |
+
raise ValueError("metrics inputs must be finite")
|
| 31 |
+
return {
|
| 32 |
+
"rmse": float(math.sqrt(mean_squared_error(truth, predicted))),
|
| 33 |
+
"mae": float(mean_absolute_error(truth, predicted)),
|
| 34 |
+
"pearson_r": _correlation(pearsonr, truth, predicted),
|
| 35 |
+
"spearman_r": _correlation(spearmanr, truth, predicted),
|
| 36 |
+
"r2": float(r2_score(truth, predicted)) if len(truth) >= 2 else None,
|
| 37 |
+
}
|
recovery/src/mitointeract_recovery/model.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Corrected trainable MitoInteract v2 regression head.
|
| 2 |
+
|
| 3 |
+
The frozen protein and molecule encoders are intentionally kept outside this
|
| 4 |
+
module. Their pooled embeddings can be cached, versioned, and reused across
|
| 5 |
+
head experiments without serializing hundreds of megabytes of frozen weights.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
from torch import nn
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass(frozen=True)
|
| 17 |
+
class TargetScaler:
|
| 18 |
+
mean: float
|
| 19 |
+
std: float
|
| 20 |
+
|
| 21 |
+
@classmethod
|
| 22 |
+
def fit(cls, values: torch.Tensor) -> "TargetScaler":
|
| 23 |
+
mean = float(values.mean())
|
| 24 |
+
std = float(values.std(unbiased=False))
|
| 25 |
+
if not std > 0:
|
| 26 |
+
raise ValueError("target standard deviation must be positive")
|
| 27 |
+
return cls(mean=mean, std=std)
|
| 28 |
+
|
| 29 |
+
def encode(self, values: torch.Tensor) -> torch.Tensor:
|
| 30 |
+
return (values - self.mean) / self.std
|
| 31 |
+
|
| 32 |
+
def decode(self, values: torch.Tensor) -> torch.Tensor:
|
| 33 |
+
return values * self.std + self.mean
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class MitoInteractHead(nn.Module):
|
| 37 |
+
"""Fuse pooled protein/ligand embeddings and regress standardized pAffinity."""
|
| 38 |
+
|
| 39 |
+
def __init__(
|
| 40 |
+
self,
|
| 41 |
+
protein_dim: int,
|
| 42 |
+
ligand_dim: int,
|
| 43 |
+
*,
|
| 44 |
+
projection_dim: int = 256,
|
| 45 |
+
hidden_dim: int = 512,
|
| 46 |
+
dropout: float = 0.1,
|
| 47 |
+
) -> None:
|
| 48 |
+
super().__init__()
|
| 49 |
+
self.protein_projection = nn.Sequential(
|
| 50 |
+
nn.LayerNorm(protein_dim),
|
| 51 |
+
nn.Linear(protein_dim, projection_dim),
|
| 52 |
+
nn.GELU(),
|
| 53 |
+
)
|
| 54 |
+
self.ligand_projection = nn.Sequential(
|
| 55 |
+
nn.LayerNorm(ligand_dim),
|
| 56 |
+
nn.Linear(ligand_dim, projection_dim),
|
| 57 |
+
nn.GELU(),
|
| 58 |
+
)
|
| 59 |
+
interaction_dim = projection_dim * 4
|
| 60 |
+
self.regressor = nn.Sequential(
|
| 61 |
+
nn.LayerNorm(interaction_dim),
|
| 62 |
+
nn.Linear(interaction_dim, hidden_dim),
|
| 63 |
+
nn.GELU(),
|
| 64 |
+
nn.Dropout(dropout),
|
| 65 |
+
nn.Linear(hidden_dim, hidden_dim // 2),
|
| 66 |
+
nn.GELU(),
|
| 67 |
+
nn.Dropout(dropout),
|
| 68 |
+
nn.Linear(hidden_dim // 2, 1),
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
def forward(
|
| 72 |
+
self, protein_embedding: torch.Tensor, ligand_embedding: torch.Tensor
|
| 73 |
+
) -> torch.Tensor:
|
| 74 |
+
if protein_embedding.ndim != 2 or ligand_embedding.ndim != 2:
|
| 75 |
+
raise ValueError("encoder embeddings must be rank-2 [batch, features]")
|
| 76 |
+
if protein_embedding.shape[0] != ligand_embedding.shape[0]:
|
| 77 |
+
raise ValueError("protein and ligand batch sizes must match")
|
| 78 |
+
protein = self.protein_projection(protein_embedding)
|
| 79 |
+
ligand = self.ligand_projection(ligand_embedding)
|
| 80 |
+
interaction = torch.cat(
|
| 81 |
+
[protein, ligand, protein * ligand, torch.abs(protein - ligand)],
|
| 82 |
+
dim=-1,
|
| 83 |
+
)
|
| 84 |
+
return self.regressor(interaction).squeeze(-1)
|
recovery/src/mitointeract_recovery/splits.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic entity-disjoint split assignment."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
from collections import defaultdict
|
| 7 |
+
from collections.abc import Mapping, Sequence
|
| 8 |
+
|
| 9 |
+
SPLIT_RATIOS = {"train": 0.8, "validation": 0.1, "test": 0.1}
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _tie_breaker(seed: int, group: str) -> str:
|
| 13 |
+
return hashlib.sha256(f"{seed}:{group}".encode()).hexdigest()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def assign_random_splits(
|
| 17 |
+
rows: Sequence[Mapping[str, object]],
|
| 18 |
+
*,
|
| 19 |
+
id_key: str = "pair_id",
|
| 20 |
+
seed: int = 42,
|
| 21 |
+
ratios: Mapping[str, float] = SPLIT_RATIOS,
|
| 22 |
+
) -> dict[str, str]:
|
| 23 |
+
"""Assign individual rows deterministically with exact approximate counts."""
|
| 24 |
+
if abs(sum(ratios.values()) - 1.0) > 1e-9 or any(
|
| 25 |
+
value <= 0 for value in ratios.values()
|
| 26 |
+
):
|
| 27 |
+
raise ValueError("split ratios must be positive and sum to 1")
|
| 28 |
+
ordered = sorted(
|
| 29 |
+
(str(row[id_key]) for row in rows),
|
| 30 |
+
key=lambda pair_id: _tie_breaker(seed, pair_id),
|
| 31 |
+
)
|
| 32 |
+
assignments: dict[str, str] = {}
|
| 33 |
+
cursor = 0
|
| 34 |
+
names = list(ratios)
|
| 35 |
+
for index, name in enumerate(names):
|
| 36 |
+
if index == len(names) - 1:
|
| 37 |
+
stop = len(ordered)
|
| 38 |
+
else:
|
| 39 |
+
stop = cursor + round(len(ordered) * ratios[name])
|
| 40 |
+
for pair_id in ordered[cursor:stop]:
|
| 41 |
+
assignments[pair_id] = name
|
| 42 |
+
cursor = stop
|
| 43 |
+
return assignments
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def assign_grouped_splits(
|
| 47 |
+
rows: Sequence[Mapping[str, object]],
|
| 48 |
+
group_key: str,
|
| 49 |
+
*,
|
| 50 |
+
id_key: str = "pair_id",
|
| 51 |
+
seed: int = 42,
|
| 52 |
+
ratios: Mapping[str, float] = SPLIT_RATIOS,
|
| 53 |
+
) -> dict[str, str]:
|
| 54 |
+
"""Assign whole groups to train/validation/test with approximate row balance."""
|
| 55 |
+
if not rows:
|
| 56 |
+
return {}
|
| 57 |
+
if abs(sum(ratios.values()) - 1.0) > 1e-9 or any(
|
| 58 |
+
value <= 0 for value in ratios.values()
|
| 59 |
+
):
|
| 60 |
+
raise ValueError("split ratios must be positive and sum to 1")
|
| 61 |
+
|
| 62 |
+
groups: dict[str, list[str]] = defaultdict(list)
|
| 63 |
+
for row in rows:
|
| 64 |
+
pair_id = str(row[id_key])
|
| 65 |
+
group = str(row[group_key])
|
| 66 |
+
groups[group].append(pair_id)
|
| 67 |
+
|
| 68 |
+
targets = {name: len(rows) * ratio for name, ratio in ratios.items()}
|
| 69 |
+
counts = {name: 0 for name in ratios}
|
| 70 |
+
assignments: dict[str, str] = {}
|
| 71 |
+
ordered = sorted(
|
| 72 |
+
groups.items(), key=lambda item: (-len(item[1]), _tie_breaker(seed, item[0]))
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
for _, pair_ids in ordered:
|
| 76 |
+
# Choose the split with the largest normalized deficit. Once all are
|
| 77 |
+
# over target, choose the least-filled split relative to its target.
|
| 78 |
+
def priority(name: str) -> tuple[float, float, str]:
|
| 79 |
+
deficit = (targets[name] - counts[name]) / targets[name]
|
| 80 |
+
fill = counts[name] / targets[name]
|
| 81 |
+
return deficit, -fill, name
|
| 82 |
+
|
| 83 |
+
selected = max(ratios, key=priority)
|
| 84 |
+
for pair_id in pair_ids:
|
| 85 |
+
assignments[pair_id] = selected
|
| 86 |
+
counts[selected] += len(pair_ids)
|
| 87 |
+
|
| 88 |
+
return assignments
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def assert_group_disjoint(
|
| 92 |
+
rows: Sequence[Mapping[str, object]],
|
| 93 |
+
assignments: Mapping[str, str],
|
| 94 |
+
group_key: str,
|
| 95 |
+
*,
|
| 96 |
+
id_key: str = "pair_id",
|
| 97 |
+
) -> None:
|
| 98 |
+
"""Raise when an entity group occurs in more than one split."""
|
| 99 |
+
seen: dict[str, str] = {}
|
| 100 |
+
for row in rows:
|
| 101 |
+
pair_id = str(row[id_key])
|
| 102 |
+
group = str(row[group_key])
|
| 103 |
+
split = assignments[pair_id]
|
| 104 |
+
previous = seen.setdefault(group, split)
|
| 105 |
+
if previous != split:
|
| 106 |
+
raise AssertionError(
|
| 107 |
+
f"group {group!r} appears in both {previous!r} and {split!r}"
|
| 108 |
+
)
|
recovery/tests/test_affinity.py
CHANGED
|
@@ -1,28 +1,31 @@
|
|
| 1 |
import math
|
|
|
|
| 2 |
import pytest
|
| 3 |
|
| 4 |
-
from mitointeract_recovery import
|
| 5 |
|
| 6 |
|
| 7 |
-
def
|
| 8 |
-
assert
|
| 9 |
|
| 10 |
|
| 11 |
def test_dataset_example_500_micromolar():
|
| 12 |
-
assert
|
| 13 |
|
| 14 |
|
| 15 |
-
@pytest.mark.parametrize("
|
| 16 |
-
def test_round_trip(
|
| 17 |
-
assert
|
|
|
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
-
@pytest.mark.parametrize("bad", [0.0, -1.0, math.inf,
|
| 21 |
-
def
|
| 22 |
with pytest.raises(ValueError):
|
| 23 |
-
|
| 24 |
|
| 25 |
|
| 26 |
-
def
|
| 27 |
with pytest.raises(ValueError):
|
| 28 |
-
|
|
|
|
| 1 |
import math
|
| 2 |
+
|
| 3 |
import pytest
|
| 4 |
|
| 5 |
+
from mitointeract_recovery import micromolar_to_paffinity, paffinity_to_micromolar
|
| 6 |
|
| 7 |
|
| 8 |
+
def test_one_micromolar_is_paffinity_six():
|
| 9 |
+
assert micromolar_to_paffinity(1.0) == pytest.approx(6.0)
|
| 10 |
|
| 11 |
|
| 12 |
def test_dataset_example_500_micromolar():
|
| 13 |
+
assert micromolar_to_paffinity(500.0) == pytest.approx(3.3010299957)
|
| 14 |
|
| 15 |
|
| 16 |
+
@pytest.mark.parametrize("paffinity", [2.0, 3.3010299957, 6.0, 7.5850267, 9.0])
|
| 17 |
+
def test_round_trip(paffinity):
|
| 18 |
+
assert micromolar_to_paffinity(paffinity_to_micromolar(paffinity)) == pytest.approx(
|
| 19 |
+
paffinity
|
| 20 |
+
)
|
| 21 |
|
| 22 |
|
| 23 |
+
@pytest.mark.parametrize("bad", [0.0, -1.0, math.inf, math.nan])
|
| 24 |
+
def test_invalid_affinity_rejected(bad):
|
| 25 |
with pytest.raises(ValueError):
|
| 26 |
+
micromolar_to_paffinity(bad)
|
| 27 |
|
| 28 |
|
| 29 |
+
def test_nonfinite_paffinity_rejected():
|
| 30 |
with pytest.raises(ValueError):
|
| 31 |
+
paffinity_to_micromolar(math.nan)
|
recovery/tests/test_checked_reports.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
REPORTS = Path(__file__).parents[1] / "reports"
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def load(name: str) -> dict:
|
| 9 |
+
return json.loads((REPORTS / name).read_text())
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def model(report: dict, split: str, name: str) -> dict:
|
| 13 |
+
return next(
|
| 14 |
+
item for item in report["splits"][split]["models"] if item["name"] == name
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_checked_sample_and_manifests():
|
| 19 |
+
audit = load("dev-10k-audit.json")
|
| 20 |
+
assert audit["dataset_revision"] == "11e49b7ece33d62afd7f65bc05ce60ad37f9ba7b"
|
| 21 |
+
assert (
|
| 22 |
+
audit["source_sha256"]
|
| 23 |
+
== "d50f4cae794298f32c39f75a9e8603528d20e50e4fa757025def412f5b2a78d8"
|
| 24 |
+
)
|
| 25 |
+
assert audit["sample_rows"] == 10_000
|
| 26 |
+
assert (
|
| 27 |
+
audit["external_split_audits"]["protein_similarity_50"][
|
| 28 |
+
"protein_cluster_overlap"
|
| 29 |
+
]
|
| 30 |
+
== 0
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_combined_baseline_beats_train_mean_on_every_checked_split():
|
| 35 |
+
report = load("dev-10k-baselines.json")
|
| 36 |
+
assert report["target"] == "pAffinity"
|
| 37 |
+
for split_name, split in report["splits"].items():
|
| 38 |
+
combined = model(report, split_name, "combined_char_ridge")["test"]
|
| 39 |
+
mean = split["mean_baseline"]["test"]
|
| 40 |
+
assert combined["rmse"] < mean["rmse"]
|
| 41 |
+
assert combined["pearson_r"] > 0
|
| 42 |
+
assert combined["spearman_r"] > 0
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_similarity_split_passes_initial_recovery_gate():
|
| 46 |
+
report = load("dev-10k-baselines.json")
|
| 47 |
+
result = model(report, "protein_similarity_50", "combined_char_ridge")["test"]
|
| 48 |
+
assert result["pearson_r"] > 0.30
|
| 49 |
+
assert result["spearman_r"] > 0.30
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def test_actual_encoder_overfit_sanity_passed():
|
| 53 |
+
report = load("overfit-actual-embeddings.json")
|
| 54 |
+
assert report["passed"] is True
|
| 55 |
+
assert report["rmse_paffinity"] <= report["acceptance_max_rmse_paffinity"]
|
recovery/tests/test_chemistry.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
from mitointeract_recovery.chemistry import canonicalize_smiles, scaffold_id
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_canonical_smiles_normalizes_equivalent_strings():
|
| 7 |
+
assert canonicalize_smiles("C(C)O") == canonicalize_smiles("CCO")
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_bemis_murcko_scaffold_groups_ring_substitutions():
|
| 11 |
+
assert scaffold_id("Cc1ccccc1") == scaffold_id("Oc1ccccc1")
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_acyclic_scaffolds_do_not_collapse_into_one_group():
|
| 15 |
+
assert scaffold_id("CCO") != scaffold_id("CCCC")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_invalid_smiles_rejected():
|
| 19 |
+
with pytest.raises(ValueError):
|
| 20 |
+
canonicalize_smiles("not-a-smiles")
|
recovery/tests/test_legacy_guard.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib.util
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
pytest.importorskip("torch")
|
| 7 |
+
pytest.importorskip("transformers")
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def load_legacy_module():
|
| 11 |
+
path = Path(__file__).parents[2] / "model.py"
|
| 12 |
+
spec = importlib.util.spec_from_file_location("mitointeract_legacy_model", path)
|
| 13 |
+
assert spec is not None
|
| 14 |
+
assert spec.loader is not None
|
| 15 |
+
module = importlib.util.module_from_spec(spec)
|
| 16 |
+
spec.loader.exec_module(module)
|
| 17 |
+
return module
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_legacy_pickle_loading_is_blocked_by_default():
|
| 21 |
+
legacy = load_legacy_module()
|
| 22 |
+
with pytest.raises(RuntimeError, match="unsafe pickle"):
|
| 23 |
+
legacy.load_model("not-opened.pt")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_legacy_output_cannot_be_called_pkd_by_default():
|
| 27 |
+
legacy = load_legacy_module()
|
| 28 |
+
with pytest.raises(RuntimeError, match="cannot be interpreted as pKd"):
|
| 29 |
+
legacy.predict_binding(None, "PROTEIN", "CC")
|
recovery/tests/test_metrics.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
from mitointeract_recovery.metrics import regression_metrics
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_perfect_predictions():
|
| 7 |
+
metrics = regression_metrics([1.0, 2.0, 3.0], [1.0, 2.0, 3.0])
|
| 8 |
+
assert metrics["rmse"] == pytest.approx(0.0)
|
| 9 |
+
assert metrics["mae"] == pytest.approx(0.0)
|
| 10 |
+
assert metrics["pearson_r"] == pytest.approx(1.0)
|
| 11 |
+
assert metrics["spearman_r"] == pytest.approx(1.0)
|
| 12 |
+
assert metrics["r2"] == pytest.approx(1.0)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_constant_predictions_have_null_correlations():
|
| 16 |
+
metrics = regression_metrics([1.0, 2.0, 3.0], [2.0, 2.0, 2.0])
|
| 17 |
+
assert metrics["pearson_r"] is None
|
| 18 |
+
assert metrics["spearman_r"] is None
|
recovery/tests/test_model.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
torch = pytest.importorskip("torch")
|
| 4 |
+
from torch import nn # noqa: E402
|
| 5 |
+
|
| 6 |
+
from mitointeract_recovery.model import MitoInteractHead, TargetScaler # noqa: E402
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_head_output_shape():
|
| 10 |
+
model = MitoInteractHead(16, 12, projection_dim=8, hidden_dim=16, dropout=0.0)
|
| 11 |
+
output = model(torch.randn(4, 16), torch.randn(4, 12))
|
| 12 |
+
assert output.shape == (4,)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_head_uses_no_batch_norm():
|
| 16 |
+
model = MitoInteractHead(16, 12, projection_dim=8, hidden_dim=16)
|
| 17 |
+
assert not any(
|
| 18 |
+
isinstance(module, nn.modules.batchnorm._BatchNorm)
|
| 19 |
+
for module in model.modules()
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def test_target_scaler_round_trip():
|
| 24 |
+
values = torch.tensor([3.0, 5.0, 7.0, 9.0])
|
| 25 |
+
scaler = TargetScaler.fit(values)
|
| 26 |
+
assert torch.allclose(scaler.decode(scaler.encode(values)), values)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_mismatched_batches_rejected():
|
| 30 |
+
model = MitoInteractHead(16, 12, projection_dim=8, hidden_dim=16)
|
| 31 |
+
with pytest.raises(ValueError):
|
| 32 |
+
model(torch.randn(3, 16), torch.randn(4, 12))
|
recovery/tests/test_splits.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from mitointeract_recovery.splits import (
|
| 2 |
+
assign_grouped_splits,
|
| 3 |
+
assign_random_splits,
|
| 4 |
+
assert_group_disjoint,
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
ROWS = [
|
| 9 |
+
{"pair_id": "a1", "protein_id": "p1", "scaffold_id": "s1"},
|
| 10 |
+
{"pair_id": "a2", "protein_id": "p1", "scaffold_id": "s2"},
|
| 11 |
+
{"pair_id": "b1", "protein_id": "p2", "scaffold_id": "s1"},
|
| 12 |
+
{"pair_id": "c1", "protein_id": "p3", "scaffold_id": "s3"},
|
| 13 |
+
{"pair_id": "d1", "protein_id": "p4", "scaffold_id": "s4"},
|
| 14 |
+
{"pair_id": "e1", "protein_id": "p5", "scaffold_id": "s5"},
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_random_split_has_expected_counts():
|
| 19 |
+
rows = [{"pair_id": str(index)} for index in range(100)]
|
| 20 |
+
assignments = assign_random_splits(rows, seed=42)
|
| 21 |
+
assert list(assignments.values()).count("train") == 80
|
| 22 |
+
assert list(assignments.values()).count("validation") == 10
|
| 23 |
+
assert list(assignments.values()).count("test") == 10
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_grouped_split_is_deterministic():
|
| 27 |
+
first = assign_grouped_splits(ROWS, "protein_id", seed=42)
|
| 28 |
+
second = assign_grouped_splits(ROWS, "protein_id", seed=42)
|
| 29 |
+
assert first == second
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_cold_protein_groups_are_disjoint():
|
| 33 |
+
assignments = assign_grouped_splits(ROWS, "protein_id", seed=42)
|
| 34 |
+
assert_group_disjoint(ROWS, assignments, "protein_id")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def test_cold_scaffold_groups_are_disjoint():
|
| 38 |
+
assignments = assign_grouped_splits(ROWS, "scaffold_id", seed=7)
|
| 39 |
+
assert_group_disjoint(ROWS, assignments, "scaffold_id")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def test_every_pair_is_assigned_once():
|
| 43 |
+
assignments = assign_grouped_splits(ROWS, "protein_id", seed=42)
|
| 44 |
+
assert set(assignments) == {row["pair_id"] for row in ROWS}
|
| 45 |
+
assert set(assignments.values()) <= {"train", "validation", "test"}
|
recovery/uv.lock
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|