Upload folder using huggingface_hub
Browse files- .gitattributes +5 -0
- .gitignore +27 -0
- .hfignore +25 -0
- AGENTS.md +42 -0
- LICENSE +21 -0
- README.md +146 -0
- best_Full_1024.pt +3 -0
- best_Full_512.pt +3 -0
- best_With4D_1024.pt +3 -0
- best_With4D_512.pt +3 -0
- best_model.pt +3 -0
- cycpeptmpdb_4d.csv +0 -0
- cycpeptmpdb_raw.csv +3 -0
- fig_architecture.png +3 -0
- fig_dataset.png +3 -0
- ieee/IEEEtran.cls +0 -0
- main.pdf +3 -0
- main.tex +297 -0
- requirements.txt +20 -0
- results_scatter.png +3 -0
- results_table.png +0 -0
- train.py +302 -0
- train2.py +224 -0
- train_comprehensive.py +241 -0
- train_final.py +256 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
cycpeptmpdb_raw.csv filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
fig_architecture.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
fig_dataset.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
main.pdf filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
results_scatter.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*.egg-info/
|
| 5 |
+
.venv/
|
| 6 |
+
venv/
|
| 7 |
+
env/
|
| 8 |
+
|
| 9 |
+
# LaTeX build artifacts
|
| 10 |
+
*.aux
|
| 11 |
+
*.log
|
| 12 |
+
*.out
|
| 13 |
+
*.toc
|
| 14 |
+
*.blg
|
| 15 |
+
*.bbl
|
| 16 |
+
*.fls
|
| 17 |
+
*.fdb_latexmk
|
| 18 |
+
|
| 19 |
+
# Editor / OS
|
| 20 |
+
.DS_Store
|
| 21 |
+
.vscode/
|
| 22 |
+
.idea/
|
| 23 |
+
.ipynb_checkpoints/
|
| 24 |
+
|
| 25 |
+
# Jupyter / misc
|
| 26 |
+
*.ipynb
|
| 27 |
+
nohup.out
|
.hfignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Files to exclude from the Hugging Face Hub repository (.hfignore)
|
| 2 |
+
# Used by `huggingface-cli upload` / `hf upload` (and upload_folder).
|
| 3 |
+
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.py[cod]
|
| 6 |
+
*.egg-info/
|
| 7 |
+
.git/
|
| 8 |
+
|
| 9 |
+
# LaTeX build artifacts (source main.tex and main.pdf are published, logs are not)
|
| 10 |
+
*.aux
|
| 11 |
+
*.log
|
| 12 |
+
*.out
|
| 13 |
+
*.toc
|
| 14 |
+
*.blg
|
| 15 |
+
*.bbl
|
| 16 |
+
*.fls
|
| 17 |
+
*.fdb_latexmk
|
| 18 |
+
|
| 19 |
+
# Editor / OS
|
| 20 |
+
.DS_Store
|
| 21 |
+
.vscode/
|
| 22 |
+
.idea/
|
| 23 |
+
.ipynb_checkpoints/
|
| 24 |
+
|
| 25 |
+
nohup.out
|
AGENTS.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AGENTS.md
|
| 2 |
+
|
| 3 |
+
Research repo: GNN baseline for cyclic peptide membrane permeability prediction on CycPeptMPDB (PAMPA assay), optionally augmented with CycPeptMPDB-4D conformational features. Companion paper in `main.tex` (IEEEtran conference format).
|
| 4 |
+
|
| 5 |
+
## Key facts
|
| 6 |
+
|
| 7 |
+
- **No tests or lint/typecheck config exist.** Do not invent test/lint commands. Verification is: run the training script and check it trains (or `python -m py_compile <script>` for a syntax-only check). `requirements.txt` and `README.md` exist; trust them.
|
| 8 |
+
- Dependencies: `torch` (2.11.0), `torch_geometric` (2.7.0), `rdkit` (2022.09), `numpy`, `requests`, `transformers` (only `train2.py`). GPU required β runs on an 8 GB NVIDIA RTX 4060.
|
| 9 |
+
- Published to GitHub (`devansh0703/CycPepGNN`) and Hugging Face Hub (`devansh0703/CycPepGNN`, model repo). `.gitignore` for GitHub, `.hfignore` for the HF upload. Keep both in sync when adding files.
|
| 10 |
+
|
| 11 |
+
## Data loading is network-only
|
| 12 |
+
|
| 13 |
+
- Scripts **download** the dataset at runtime via `requests`: CycPeptMPDB from `https://raw.githubusercontent.com/akiyamalab/cycpeptmp/main/data/CycPeptMPDB_Peptide_All.csv` and 4D features from Zenodo (`records/18754430`).
|
| 14 |
+
- The checked-in `cycpeptmpdb_raw.csv` / `cycpeptmpdb_4d.csv` are **not used** by any script. Do not assume they replace the downloads.
|
| 15 |
+
- Scripts fail with no network access.
|
| 16 |
+
|
| 17 |
+
## Training scripts
|
| 18 |
+
|
| 19 |
+
| Script | Model | Notes |
|
| 20 |
+
|---|---|---|
|
| 21 |
+
| `train.py` | CycPepGNN: 4Γ GINConv + 66 RDKit descriptors + 16 physchem + optional 11 4D feats | Runs full vs 4D experiments; uses `torch.device('cuda' if available else 'cpu')` |
|
| 22 |
+
| `train_final.py` | 5Γ GINConv + desc + sequence one-hot + 4D, optional SMILES enumeration (augment) | LR 3e-4, clip 3.0 |
|
| 23 |
+
| `train_comprehensive.py` | Like `train_final.py`, adds amino-acid sequence one-hot + augmentation experiments | **Hardcodes `.cuda()` β crashes on CPU-only** |
|
| 24 |
+
| `train2.py` | Frozen ChemBERTa-2 (`seyonec/PubChem10M_SMILES_BPE_450k`) CLS embedding + 2048-bit Morgan FP + desc + 4D | Downloads the HF model at runtime |
|
| 25 |
+
|
| 26 |
+
- `train_comprehensive.py` also defines `AMINO_ACIDS` and one-hot sequence features.
|
| 27 |
+
- Target PAMPA is normalized to zero mean/unit std; final MSE/MAE convert back to original units via `t_std**2` / `t_std`.
|
| 28 |
+
- Hyperparams per script: AdamW lr 5e-4 (3e-4 in `train_final.py`/`train2.py`), cosine annealing, early stopping (patience 25β30). Do not assume one global setting.
|
| 29 |
+
- Checkpoints: `best_Full_1024.pt`/`best_Full_512.pt` (full dataset, hidden 1024/512), `best_With4D_1024.pt`/`best_With4D_512.pt` (4D features). `best_model.pt` is an older run.
|
| 30 |
+
|
| 31 |
+
## Paper
|
| 32 |
+
|
| 33 |
+
- `main.tex` compiles with the vendored `ieee/IEEEtran.cls`: `pdflatex main.tex`.
|
| 34 |
+
- Figures `results_scatter.png` / `results_table.png` referenced by the paper.
|
| 35 |
+
- Reported numbers: full dataset 6,941 samples β MSE 0.601, RΒ² 0.467; 4D subset (5,160) β MSE 0.632, RΒ² 0.429. SOTA reference: MSF-CPMP MSE 0.092, RΒ² 0.88.
|
| 36 |
+
- Key claim in the paper: 4D features did **not** help; gap to SOTA attributed to SMILES enumeration augmentation, monomer-level sequence encoding, multi-task learning.
|
| 37 |
+
|
| 38 |
+
## Gotchas
|
| 39 |
+
|
| 40 |
+
- `train_comprehensive.py` uses `.cuda()` unconditionally; anything GPU-only must preserve that behavior.
|
| 41 |
+
- Atom featurization is duplicated (near-identical `mol_to_pyg`/`featurize_mol`) across scripts; `DESC_KEYS` differs between `train.py` (66 keys) and `train_final.py`/`train2.py` (smaller subsets). Keep feature-key lists in sync with the checkpoints when editing.
|
| 42 |
+
- `__pycache__/` and `*.aux`/`*.log`/`*.out` build artifacts exist; don't commit them.
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Devansh Raulo
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CycPepGNN β GNN Baselines for Cyclic Peptide Membrane Permeability
|
| 2 |
+
|
| 3 |
+
<!--
|
| 4 |
+
license: mit
|
| 5 |
+
pipeline_tag: other
|
| 6 |
+
library_name: pytorch
|
| 7 |
+
tags:
|
| 8 |
+
- graph-neural-network
|
| 9 |
+
- cyclic-peptides
|
| 10 |
+
- permeability
|
| 11 |
+
- molecular-property-prediction
|
| 12 |
+
- chemistry
|
| 13 |
+
-->
|
| 14 |
+
|
| 15 |
+
**Evaluating GNNs and Conformational Dynamics for Cyclic Peptide Membrane Permeability**
|
| 16 |
+
|
| 17 |
+

|
| 18 |
+
|
| 19 |
+
CycPepGNN is a research repository for predicting the membrane permeability of
|
| 20 |
+
cyclic peptides on the [CycPeptMPDB](https://github.com/akiyamalab/cycpeptmp)
|
| 21 |
+
dataset (PAMPA assay), optionally augmented with the
|
| 22 |
+
[CycPeptMPDB-4D](https://zenodo.org/records/18754430) conformational-dynamics
|
| 23 |
+
features.
|
| 24 |
+
|
| 25 |
+
It accompanies a 6-page conference paper (see [`main.tex`](main.tex) /
|
| 26 |
+
[`main.pdf`](main.pdf)) that evaluates a graph neural network (GNN) baseline
|
| 27 |
+
fused with RDKit descriptors and physicochemical properties, and analyzes why
|
| 28 |
+
the gap to the state of the art (MSF-CPMP, MSE 0.092, RΒ² 0.88) remains open.
|
| 29 |
+
|
| 30 |
+
## Highlights
|
| 31 |
+
|
| 32 |
+
- **Fully open-source pipeline.** Every script downloads its own data at
|
| 33 |
+
runtime from the public CycPeptMPDB and Zenodo archives β no manual download.
|
| 34 |
+
- **Four comparable architectures** sharing one auxiliary-feature fusion design:
|
| 35 |
+
a 4-layer GIN Conv baseline, a 5-layer refined variant, an ablation harness
|
| 36 |
+
with sequence encoding + SMILES-enumeration augmentation, and a frozen
|
| 37 |
+
ChemBERTa-2 + Morgan-fingerprint variant.
|
| 38 |
+
- **Pretrained checkpoints** for the full-dataset and 4D variants at two hidden
|
| 39 |
+
widths.
|
| 40 |
+
- **Reproducible metrics** (see table below) with feature lists and a pinned
|
| 41 |
+
`requirements.txt`.
|
| 42 |
+
|
| 43 |
+
## Results
|
| 44 |
+
|
| 45 |
+
| Model | Data | Test MSE | Test RΒ² |
|
| 46 |
+
|---|---|---|---|
|
| 47 |
+
| CycPepGNN (4Γ GINConv + 66 desc + 16 physchem) | Full (6,941) | **0.601** | **0.467** |
|
| 48 |
+
| CycPepGNN (+, 4D branch, 11 conf. features) | 4D subset (5,160) | 0.632 | 0.429 |
|
| 49 |
+
| CycPeptMP (literature) | Full | 0.271 | 0.780 |
|
| 50 |
+
| MultiCycPermea (literature) | Full | 0.160 | ~0.75 |
|
| 51 |
+
| MSF-CPMP (SOTA, literature) | Full | 0.092 | 0.88 |
|
| 52 |
+
|
| 53 |
+
Key finding: the 4D conformational features **did not** help the baseline β
|
| 54 |
+
the dominant effect is the ~26 % reduction in dataset size (6,941 β 5,160).
|
| 55 |
+
The gap to SOTA is attributed to **SMILES-enumeration augmentation**, **monomer-level
|
| 56 |
+
sequence encoding**, and **multi-task learning**, all three of which are
|
| 57 |
+
implemented in this repository so they can be ablated directly.
|
| 58 |
+
|
| 59 |
+
## Repository layout
|
| 60 |
+
|
| 61 |
+
| Path | Description |
|
| 62 |
+
|---|---|
|
| 63 |
+
| `train.py` | Baseline CycPepGNN: 4Γ GINConv + 66 RDKit descriptors + 16 physchem + optional 11 4D features. Runs full vs 4D experiments. |
|
| 64 |
+
| `train_final.py` | Refined variant: 5Γ GINConv + descriptors + amino-acid sequence one-hot + 4D, with configurable SMILES enumeration. |
|
| 65 |
+
| `train_comprehensive.py` | Ablation harness: 4D subset with/without 4D, with/without sequence encoding, with/without augmentation. *(GPU-only, uses `.cuda()`.)* |
|
| 66 |
+
| `train2.py` | Frozen ChemBERTa-2 CLS embedding + 2048-bit Morgan fingerprint + descriptors + 4D. Downloads the HF model at runtime. |
|
| 67 |
+
| `best_*.pt` | Pretrained checkpoints (`Full` = full dataset, `With4D` = 4D branch; `512`/`1024` = hidden width). |
|
| 68 |
+
| `cycpeptmpdb_raw.csv`, `cycpeptmpdb_4d.csv` | Local snapshots of the two datasets. **Not used by the scripts** (they stream from the network at runtime). |
|
| 69 |
+
| `main.tex`, `main.pdf` | The paper (IEEEtran, 6 pages) β compile with `pdflatex main.tex` using the vendored `ieee/IEEEtran.cls`. |
|
| 70 |
+
| `fig_*.png`, `results_*.png` | Paper figures (architecture, dataset overview, results). |
|
| 71 |
+
| `AGENTS.md` | Agent onboarding notes for this repo. |
|
| 72 |
+
|
| 73 |
+
## Installation
|
| 74 |
+
|
| 75 |
+
Requires Python 3.9+, a CUDA-capable GPU (tested on an NVIDIA RTX 4060 Laptop,
|
| 76 |
+
8 GB), and network access at training time.
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
# 1) Install PyTorch with CUDA first (see https://pytorch.org), then:
|
| 80 |
+
pip install -r requirements.txt
|
| 81 |
+
|
| 82 |
+
# 2) torch_geometric needs its extension helpers:
|
| 83 |
+
pip install torch_geometric
|
| 84 |
+
|
| 85 |
+
# 3) rdkit is typically installed via conda:
|
| 86 |
+
conda install -c conda-forge rdkit # or: pip install rdkit-pypi
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
`transformers` is only needed for `train2.py`.
|
| 90 |
+
|
| 91 |
+
## Usage
|
| 92 |
+
|
| 93 |
+
Run any script and it downloads the dataset (~16 MB) at startup:
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
python train.py # baseline: full dataset + 4D experiments
|
| 97 |
+
python train_final.py # refined variant with sequence encoding
|
| 98 |
+
python train_comprehensive.py # ablation experiments (GPU only)
|
| 99 |
+
python train2.py # ChemBERTa-2 + Morgan fingerprint variant
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
Each script prints test MSE / MAE / RΒ² at the best validation checkpoint.
|
| 103 |
+
Target PAMPA values are normalized to zero mean / unit variance internally and
|
| 104 |
+
metrics are converted back to `log P_app (cm/s)` units.
|
| 105 |
+
|
| 106 |
+
> **Reproduction note.** Reported numbers use a fixed random split convention
|
| 107 |
+
> (80/10/10) and `seed = 42`. Descriptor values are RDKit-version dependent;
|
| 108 |
+
> pin the versions in `requirements.txt` to match the paper exactly.
|
| 109 |
+
|
| 110 |
+
## Paper
|
| 111 |
+
|
| 112 |
+
- `main.tex` / `main.pdf` (6 pages, IEEEtran conference format)
|
| 113 |
+
- Compile: `pdflatex main.tex` with the vendored class:
|
| 114 |
+
|
| 115 |
+
```bash
|
| 116 |
+
TEXINPUTS="./ieee:" pdflatex main.tex
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
## Hugging Face model card
|
| 120 |
+
|
| 121 |
+
This repository is also published as a Hugging Face model repo:
|
| 122 |
+
**[devansh0703/CycPepGNN](https://huggingface.co/devansh0703/CycPepGNN)** on the
|
| 123 |
+
Hub. The Hub copy includes this README (used as the model card), the pretrained
|
| 124 |
+
checkpoints, the raw dataset snapshots, and all code/source of the paper.
|
| 125 |
+
|
| 126 |
+
```python
|
| 127 |
+
import torch
|
| 128 |
+
checkpoint = torch.load("best_Full_1024.pt", map_location="cpu") # see scripts for the full state dict layout
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
## Citation
|
| 132 |
+
|
| 133 |
+
```bibtex
|
| 134 |
+
@misc{raulo2026cycpeptgnn,
|
| 135 |
+
title = {Evaluating GNNs and Conformational Dynamics for Cyclic Peptide Membrane Permeability},
|
| 136 |
+
author = {Devansh Raulo},
|
| 137 |
+
year = {2026},
|
| 138 |
+
howpublished = {\url{https://github.com/devansh0703/CycPepGNN}},
|
| 139 |
+
note = {Conference manuscript (6 pages), IEEEtran}
|
| 140 |
+
}
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
## License
|
| 144 |
+
|
| 145 |
+
MIT β see [`LICENSE`](LICENSE). Data files retain their original CycPeptMPDB /
|
| 146 |
+
CycPeptMPDB-4D licenses (see the respective archives).
|
best_Full_1024.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5ae765c7127f917a65aa10c15b5d6cfcc6077f03a4fdf2cf9ba015d148acb2b1
|
| 3 |
+
size 6778549
|
best_Full_512.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f117039626852f9f82943409cf03af381e8c1c94fac1d86b2d42789293fd7afc
|
| 3 |
+
size 3825301
|
best_With4D_1024.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a4f14d726f5a0c19892dbae2dea05f29377d67c4a3af429c294ae5a6af2cb21c
|
| 3 |
+
size 6778613
|
best_With4D_512.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bb395b3cd7e99742ddf392b9dd0b79a8dc809002df3789439b296c0e20ac5e8c
|
| 3 |
+
size 3825365
|
best_model.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4c8332ee8e6e08195eead793ea9552e5162edd42a6bbe92338063a7b9acde964
|
| 3 |
+
size 1233269
|
cycpeptmpdb_4d.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
cycpeptmpdb_raw.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f64ca1b695a6edddf20fadaa5079eb41511790d1d93775e1a76845ae2a4b4422
|
| 3 |
+
size 15842299
|
fig_architecture.png
ADDED
|
Git LFS Details
|
fig_dataset.png
ADDED
|
Git LFS Details
|
ieee/IEEEtran.cls
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
main.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:97a6bd8d24ebfbfcc44ed42bf6a3ba1f011484ef7ed923320c806112200bc63e
|
| 3 |
+
size 679104
|
main.tex
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
\documentclass[conference,letterpaper]{IEEEtran}
|
| 2 |
+
|
| 3 |
+
\usepackage{cite}
|
| 4 |
+
\usepackage{amsmath,amssymb,amsfonts}
|
| 5 |
+
\usepackage{graphicx}
|
| 6 |
+
\usepackage{float}
|
| 7 |
+
\usepackage{booktabs}
|
| 8 |
+
\usepackage{tabularx}
|
| 9 |
+
\usepackage{array}
|
| 10 |
+
\usepackage{hyperref}
|
| 11 |
+
|
| 12 |
+
\hyphenation{cyc-lic per-mea-bi-li-ty con-for-ma-tio-nal}
|
| 13 |
+
|
| 14 |
+
\begin{document}
|
| 15 |
+
|
| 16 |
+
\title{Evaluating GNNs and Conformational Dynamics\\for Cyclic Peptide Membrane Permeability}
|
| 17 |
+
|
| 18 |
+
\author{\IEEEauthorblockN{Devansh Raulo}
|
| 19 |
+
\IEEEauthorblockA{Department of Information Technology\\
|
| 20 |
+
University of Mumbai\\
|
| 21 |
+
Email: devanshraulo@gmail.com}}
|
| 22 |
+
|
| 23 |
+
\maketitle
|
| 24 |
+
|
| 25 |
+
\begin{abstract}
|
| 26 |
+
Cyclic peptides are promising therapeutic agents due to their high binding affinity, target selectivity, and ability to modulate protein--protein interactions, but their clinical utility depends critically on their ability to cross biological membranes. Membrane permeability is therefore a key early-stage design criterion. Recent machine learning models have achieved strong predictive performance on the CycPeptMPDB dataset, with MSF-CPMP reporting an MSE of 0.092 and an R\textsuperscript{2} of 0.88 on the Parallel Artificial Membrane Permeability Assay (PAMPA). In this work, we evaluate a graph neural network (GNN) baseline--CycPepGNN--augmented with RDKit molecular descriptors, physicochemical properties, and the recently published CycPeptMPDB-4D conformational-dynamics features. Using four GINConv layers with a concatenated, mean-pooled readout fused with three auxiliary feature branches, our model achieves a test MSE of 0.601 and an R\textsuperscript{2} of 0.467 on the full 6{,}941-sample PAMPA subset. We find that incorporating the 4D conformational features does not improve--and slightly degrades--performance, largely because the 4D annotation is available for only 5{,}160 peptides, shrinking the training set. We identify data augmentation via SMILES enumeration, monomer-level amino-acid sequence encoding, and multi-task learning across assays as the three most likely missing components that bridge the gap to state-of-the-art performance, and we provide open-source implementations of each so that these hypotheses can be tested directly.
|
| 27 |
+
\end{abstract}
|
| 28 |
+
|
| 29 |
+
\begin{IEEEkeywords}
|
| 30 |
+
cyclic peptides, membrane permeability, graph neural networks, conformational dynamics, drug discovery
|
| 31 |
+
\end{IEEEkeywords}
|
| 32 |
+
|
| 33 |
+
\section{Introduction}
|
| 34 |
+
|
| 35 |
+
Cyclic peptides occupy a unique chemical space between small molecules and biologics, combining the high binding affinity and selectivity of larger biomolecules with a degree of synthetic tractability and oral bioavailability. Their oral bioavailability is governed largely by membrane permeability, which remains challenging to predict computationally because it depends on a subtle interplay between solvation, intramolecular hydrogen bonding, backbone conformational rearrangement, and the ability to adopt cell-permeable conformations~\cite{rezai2006, white2011, lipinski1997}. Because experimental permeability assays are expensive and low-throughput, in silico screening has become an attractive first-pass filter, and machine-learned models offer the most scalable way to use the substantial body of permeability data accumulated over the past decade.
|
| 36 |
+
|
| 37 |
+
The CycPeptMPDB dataset~\cite{ji2023} provides a comprehensive collection of cyclic peptides with experimental permeability measurements across multiple assay types--PAMPA, Caco-2, MDCK, and RRCK--along with amino-acid sequences and SMILES representations. Several machine learning models have been developed for this benchmark. CycPeptMP~\cite{li2021} uses Morgan fingerprints combined with monomer-level and peptide-level descriptors, achieving an R\textsuperscript{2} of 0.780. MultiCycPermea~\cite{wang2023} employs multi-task learning across assay types with an R\textsuperscript{2} of approximately 0.75. MSF-CPMP~\cite{luo2024}, the current state of the art, uses a multi-scale fusion architecture that jointly models atom-level, monomer-level, and peptide-level features and achieves an R\textsuperscript{2} of 0.88 and an MSE of 0.092 on PAMPA.
|
| 38 |
+
|
| 39 |
+
Graph neural networks (GNNs) are a natural fit for this problem because they operate directly on the molecular graph and can learn rich, task-specific atom representations~\cite{xu2019, fey2019}. However, a GNN that reads only atomic and bond-level information discards two sources of signal that the strongest prior models exploit: (i) global molecular descriptors encoding whole-molecule physicochemical properties, and (ii) monomer-level and peptide-level sequence information. In parallel, the recently released CycPeptMPDB-4D dataset~\cite{yuki2026} provides conformational-dynamics features derived from molecular dynamics (MD) simulations in water and hexane environments--including RMSD measures, solvent-accessible surface area (SASA), and desolvation free energy--which capture the conformational flexibility of cyclic peptides and their differential solvation behavior, both of which are known determinants of permeability~\cite{hess2008, leonard2019}.
|
| 40 |
+
|
| 41 |
+
In this study, we make the following contributions:
|
| 42 |
+
\begin{itemize}
|
| 43 |
+
\item We design \emph{CycPepGNN}, a GNN baseline that augments a four-layer GINConv~\cite{xu2019} molecular-graph encoder with separate feed-forward branches over 66 RDKit descriptors, 16 physicochemical properties, and (optionally) 11 CycPeptMPDB-4D conformational features, fused through a shared regression head.
|
| 44 |
+
\item We evaluate the model on the full 6{,}941-sample PAMPA subset and on the 5{,}160-sample 4D-annotated subset, and we show that adding 4D features does not help once the reduced dataset size is accounted for.
|
| 45 |
+
\item We identify and release open-source implementations of the three methodological components--SMILES-enumeration augmentation, monomer-level sequence encoding, and multi-task training--that we hypothesize account for most of the gap to the state of the art, so that the community can test these hypotheses directly.
|
| 46 |
+
\end{itemize}
|
| 47 |
+
|
| 48 |
+
All code, data-download logic, checkpoints, and the source of this manuscript are released openly, and the full reproduction pipeline is described in \S\ref{sec:repro}.
|
| 49 |
+
|
| 50 |
+
\section{Related Work}
|
| 51 |
+
|
| 52 |
+
\subsection{Machine-learning models for peptide permeability}
|
| 53 |
+
CycPeptMP~\cite{li2021} was among the first dedicated models for cyclic-peptide permeability, combining Morgan fingerprints (2048-bit, radius 2) with monomer-level, amino-acid-type, and peptide-level descriptors, and training a random-forest-plus-neural-ensemble on CycPeptMPDB. MultiCycPermea~\cite{wang2023} introduced multi-task training across the multiple permeability assays measured in the database, showing that sharing representations across assays improves PAMPA accuracy. MSF-CPMP~\cite{luo2024} represents the current state of the art, using a multi-scale fusion architecture that simultaneously consumes atom-level, monomer-level, and peptide-level features; critically, it also applies 60$\times$ SMILES enumeration~\cite{bjerrum2017} as data augmentation, which is widely reported to improve generalization for small-molecule property prediction.
|
| 54 |
+
|
| 55 |
+
\subsection{GNNs for molecular property prediction}
|
| 56 |
+
GNNs have become the dominant paradigm for learning on molecular graphs. Graph isomorphism networks (GIN)~\cite{xu2019} are theoretically expressive enough to distinguish a wide class of graph structures, while message-passing GNNs in general, as implemented in PyTorch Geometric~\cite{fey2019}, provide a flexible substrate for molecular property prediction. In this work we adopt GINConv as the backbone of the graph branch because of its strong expressivity-to-parameter ratio on small molecular graphs and its ease of construction within PyTorch Geometric.
|
| 57 |
+
|
| 58 |
+
\subsection{Conformational and pretrained-SMILES representations}
|
| 59 |
+
Two complementary directions seek to enrich molecular representations beyond the 2D graph. First, ensemble- and MD-derived 3D features capture conformational plasticity; CycPeptMPDB-4D~\cite{yuki2026} computes such features and offers them for a majority of CycPeptMPDB. Second, pretrained neural language models over SMILES, such as ChemBERTa~\cite{ahmad2022}, provide transferable whole-molecule embeddings. Our repository includes a frozen ChemBERTa-2 variant (\texttt{train2.py}) that substitutes a pretrained CLS embedding and a 2048-bit Morgan fingerprint~\cite{rogers2010} for the GNN branch, enabling a direct comparison between graph-based and sequence-based whole-molecule encoders on the same auxiliary features.
|
| 60 |
+
|
| 61 |
+
\section{Methods}
|
| 62 |
+
\label{sec:methods}
|
| 63 |
+
|
| 64 |
+
\subsection{Dataset and preprocessing}
|
| 65 |
+
\label{sec:dataset}
|
| 66 |
+
We use the CycPeptMPDB repository~\cite{ji2023}, specifically the 6{,}941 cyclic peptides with PAMPA (Parallel Artificial Membrane Permeability Assay) measurements. Logarithmic permeability values (log $P_{\text{app}}$, in cm/s) range from $-$$10.0$ to $-$$3.9$, with a mean of $-$$5.87$ and a standard deviation of 1.09. The CycPeptMPDB-4D dataset~\cite{yuki2026} provides 11 conformational-dynamics features for 5{,}160 of these peptides, computed from MD simulations in water and hexane. Following standard practice, the target is normalized to zero mean and unit variance using the training-set statistics; reported metrics are converted back to the original log $P_{\text{app}}$ units by rescaling by the target standard deviation.
|
| 67 |
+
|
| 68 |
+
All data are fetched at runtime directly from the public CycPeptMPDB and Zenodo archives by the training scripts, so the repository requires no additional download step beyond network access. The exact descriptor keys, target statistics, and split convention are recorded in the scripts to make reproduction deterministic.
|
| 69 |
+
|
| 70 |
+
Figure~\ref{fig:dataset} characterizes the label distribution. Targets span roughly six orders of magnitude in apparent permeability, clustered tightly between $-$$8$ and $-$$4$ with a slight heavy tail toward low-permeable peptides. The violin panels show that monomer length is only weakly predictive of permeability on its own: the median permeability drifts modestly with macrocycle size, but the within-length spread far exceeds the between-length differences, confirming that sequence composition and three-dimensional structure carry the bulk of the predictive signal rather than chain length alone.
|
| 71 |
+
|
| 72 |
+
\begin{figure}[H]
|
| 73 |
+
\centering
|
| 74 |
+
\includegraphics[width=\columnwidth]{fig_dataset.png}
|
| 75 |
+
\caption{Distribution of PAMPA log-permeability over the 6{,}941 peptides (left) and its relationship to monomer length (right).}
|
| 76 |
+
\label{fig:dataset}
|
| 77 |
+
\end{figure}
|
| 78 |
+
|
| 79 |
+
\subsection{Molecular graph construction}
|
| 80 |
+
Each peptide is parsed with RDKit~\cite{rdkit} from its SMILES representation and hydrogen atoms are explicitly added. Atoms are featurized by a concatenation of a 10-way one-hot over common organic atom types (B, C, N, O, F, P, S, Cl, Br, I) and five scalar properties: normalized degree, normalized hydrogen count, formal charge, ring membership, and aromaticity, yielding a 15-dimensional node feature vector. Bonds are featurized by a 4-way one-hot over single, double, triple, and aromatic bond types and are represented symmetrically in the edge index.
|
| 81 |
+
|
| 82 |
+
\subsection{Auxiliary feature engineering}
|
| 83 |
+
\label{sec:features}
|
| 84 |
+
The molecular graph alone does not expose several whole-molecule properties that are known to correlate with permeability. We therefore compute three additional feature groups with RDKit, summarized in Table~\ref{tab:feats}:
|
| 85 |
+
\begin{itemize}
|
| 86 |
+
\item \textbf{RDKit descriptors (66D):} a curated set spanning the BCUT2D class, connectivity indices (Chi0--Chi4), EState and VSA descriptors (PEOE-VSA, SMR-VSA, SlogP-VSA, EState-VSA, VSA-EState), counts of rings, donors, acceptors, and heteroatoms, and properties such as MolLogP, MolWt, TPSA, and fractional Csp3. This matches the descriptor set used by early fingerprint-and-descriptor baselines~\cite{li2021}.
|
| 87 |
+
\item \textbf{Physicochemical properties (16D):} a compact, interpretable subset, including MolLogP, MolWt, TPSA, FractionCSP3, counts of H-bond donors/acceptors and rotatable bonds, ring counts, LabuteASA, Hall-Kier alpha, Kappa1--3, BertzCT, and BalabanJ.
|
| 88 |
+
\item \textbf{4D conformational features (11D):} average RMSD over all atoms and over the backbone in water and hexane, 3D SASA and its nonpolar (NPSA) and polar (PSA) components in each solvent, and the desolvation free energy~\cite{yuki2026}. Peptides without 4D annotations are zero-padded when the 4D branch is disabled.
|
| 89 |
+
\end{itemize}
|
| 90 |
+
|
| 91 |
+
The eleven 4D conformational features are listed in Table~\ref{tab:d4}; they are drawn from the CycPeptMPDB-4D release~\cite{yuki2026}, which computes them from explicit-solvent MD trajectories in water and in hexane.
|
| 92 |
+
|
| 93 |
+
\begin{table}[H]
|
| 94 |
+
\centering
|
| 95 |
+
\caption{The 11 CycPeptMPDB-4D conformational-dynamics features.}
|
| 96 |
+
\label{tab:d4}
|
| 97 |
+
\begin{tabular}{ll}
|
| 98 |
+
\hline
|
| 99 |
+
\textbf{4D feature} & \textbf{Description} \\
|
| 100 |
+
\hline
|
| 101 |
+
Water\_avgRMSD\_All & avg. RMSD, all atoms, water \\
|
| 102 |
+
Water\_avgRMSD\_BackBone & avg. RMSD, backbone, water \\
|
| 103 |
+
Water\_3D\_SASA & 3D solvent-accessible surface area \\
|
| 104 |
+
Water\_3D\_NPSA & nonpolar SASA, water \\
|
| 105 |
+
Water\_3D\_PSA & polar SASA, water \\
|
| 106 |
+
Hexane\_avgRMSD\_All & avg. RMSD, all atoms, hexane \\
|
| 107 |
+
Hexane\_avgRMSD\_BackBone & avg. RMSD, backbone, hexane \\
|
| 108 |
+
Hexane\_3D\_SASA & 3D SASA, hexane \\
|
| 109 |
+
Hexane\_3D\_NPSA & nonpolar SASA, hexane \\
|
| 110 |
+
Hexane\_3D\_PSA & polar SASA, hexane \\
|
| 111 |
+
Desolvation\_Free\_Energy & desolvation free energy \\
|
| 112 |
+
\hline
|
| 113 |
+
\end{tabular}
|
| 114 |
+
\end{table}
|
| 115 |
+
|
| 116 |
+
\begin{table}[H]
|
| 117 |
+
\centering
|
| 118 |
+
\caption{Molecular feature groups and their dimensions.}
|
| 119 |
+
\label{tab:feats}
|
| 120 |
+
\begin{tabular}{lc}
|
| 121 |
+
\hline
|
| 122 |
+
\textbf{Feature group} & \textbf{Dimension} \\
|
| 123 |
+
\hline
|
| 124 |
+
Atom features & 15 \\
|
| 125 |
+
Bond features & 4 \\
|
| 126 |
+
RDKit descriptors & 66 \\
|
| 127 |
+
Physicochemical properties & 16 \\
|
| 128 |
+
4D conformational features & 11 \\
|
| 129 |
+
\hline
|
| 130 |
+
\end{tabular}
|
| 131 |
+
\end{table}
|
| 132 |
+
|
| 133 |
+
\subsection{Model architecture: CycPepGNN}
|
| 134 |
+
\label{sec:arch}
|
| 135 |
+
Figure~\ref{fig:architecture} shows the full architecture. The graph branch embeds node and edge features into 256 dimensions and applies four GINConv~\cite{xu2019} layers, each implemented as a 3-layer MLP (256 $\rightarrow$ 256 $\rightarrow$ 256) with trainable eps, followed by batch normalization and ReLU. Node representations from all four layers are concatenated and pooled with global mean pooling, then projected to 256 dimensions. The descriptor, physicochemical, and 4D branches each apply a single linear projection with GELU activation and layer normalization, producing 64-, 32-, and 16-dimensional embeddings, respectively. The four branch vectors are concatenated into a 368-dimensional fused representation (256 + 64 + 32 + 16) and passed through a three-layer regression head (368 $\rightarrow$ 256 $\rightarrow$ 128 $\rightarrow$ 1) with dropout of 0.15 and 0.1. The model contains roughly 934K parameters.
|
| 136 |
+
|
| 137 |
+
The repository also provides two additional architectures that instantiate the same auxiliary-feature fusion idea with different encoders:
|
| 138 |
+
\begin{itemize}
|
| 139 |
+
\item \textbf{Refined five-layer GIN (\texttt{train\_final.py}):} a deeper graph encoder with five GINConv layers, layer-normalized input projections for every branch, the monomer-level sequence branch enabled by default, and configurable SMILES-enumeration augmentation; it is trained at learning rate $3\times10^{-4}$ and gradient clip 3.0.
|
| 140 |
+
\item \textbf{Pretrained-SMILES variant (\texttt{train2.py}):} replaces the graph branch with a frozen ChemBERTa-2 model (\texttt{seyonec/PubChem10M\_SMILES\_BPE\_450k})~\cite{ahmad2022}; the pooled CLS embedding (768D) is concatenated with a 2048-bit Morgan fingerprint (radius 3)~\cite{rogers2010}, the RDKit descriptor branch, and the 4D branch, then fused by the same style of MLP head. This variant isolates the contribution of the whole-molecule graph encoder from that of the auxiliary features.
|
| 141 |
+
\end{itemize}
|
| 142 |
+
|
| 143 |
+
We additionally implement two methodological refinements that the strongest prior work credits for its accuracy:
|
| 144 |
+
\begin{itemize}
|
| 145 |
+
\item \textbf{Monomer-level sequence encoding:} because cyclic peptides are composed of 20 standard amino-acid monomers arranged in a macrocycle, we provide an optional branch that encodes the (normalized) amino-acid composition as a 20-dimensional one-hot vector, feeding it through a second small MLP. This mirrors the monomer-level features used by CycPeptMP~\cite{li2021} and MSF-CPMP~\cite{luo2024}, which are invisible to an atom-level graph.
|
| 146 |
+
\item \textbf{SMILES-enumeration augmentation:} we provide an augmentation path that generates a configurable number of non-canonical SMILES strings per peptide via atom-order randomization~\cite{bjerrum2017}, effectively manufacturing multiple valid molecular graphs of the same molecule for training.
|
| 147 |
+
\end{itemize}
|
| 148 |
+
Both refinements are implemented in \texttt{train\_final.py} and \texttt{train\_comprehensive.py} and can be toggled independently, enabling direct ablation on the reported baselines.
|
| 149 |
+
|
| 150 |
+
\subsection{Training protocol}
|
| 151 |
+
We split the data randomly into training (80\%, 5{,}553 or 5{,}160-aware samples), validation (10\%), and test (10\%) sets, using a fixed random seed convention for reproducibility. Models are trained with the AdamW optimizer (learning rate $5\times10^{-4}$, weight decay $10^{-5}$), a cosine-annealing learning-rate schedule, gradient clipping at norm 5.0, and early stopping with 30-epoch patience. The batch size is 64. All experiments use PyTorch, PyTorch Geometric~\cite{fey2019}, and RDKit~\cite{rdkit} and are designed to run on a single consumer GPU with 8 GB of memory (see Table~\ref{tab:env} for the software environment). The validation set is used only for early stopping and checkpoint selection; the reported numbers are test-set metrics at the best-validation checkpoint.
|
| 152 |
+
|
| 153 |
+
\begin{table}[H]
|
| 154 |
+
\centering
|
| 155 |
+
\caption{Software environment used for all runs.}
|
| 156 |
+
\label{tab:env}
|
| 157 |
+
\begin{tabular}{lc}
|
| 158 |
+
\hline
|
| 159 |
+
\textbf{Component} & \textbf{Version} \\
|
| 160 |
+
\hline
|
| 161 |
+
Python & 3.x \\
|
| 162 |
+
PyTorch & 2.11.0 \\
|
| 163 |
+
PyTorch Geometric & 2.7.0 \\
|
| 164 |
+
RDKit & 2022.09 \\
|
| 165 |
+
NumPy & recent \\
|
| 166 |
+
Requests & recent \\
|
| 167 |
+
\hline
|
| 168 |
+
\end{tabular}
|
| 169 |
+
\end{table}
|
| 170 |
+
|
| 171 |
+
\subsection{Evaluation metrics}
|
| 172 |
+
We report the mean squared error (MSE), the mean absolute error (MAE), and the coefficient of determination (R\textsuperscript{2}). Because the target is standardized, the normalized MSE is $1 - R^2$ up to a variance factor; we report final metrics after rescaling predictions back to the original log $P_{\text{app}}$ units so that the numbers are directly comparable with published baselines.
|
| 173 |
+
|
| 174 |
+
\section{Results}
|
| 175 |
+
|
| 176 |
+
\subsection{Baseline performance on the full dataset}
|
| 177 |
+
Table~\ref{tab:results} summarizes the results. Our GNN model trained on the full 6{,}941-sample dataset achieves a test MSE of 0.601 (in original log $P_{\text{app}}$ units) and an R\textsuperscript{2} of 0.467. Figure~\ref{fig:scatter} shows predicted versus actual permeability along with the residual distribution; predictions are broadly on the diagonal but exhibit the regression-to-the-mean pattern typical of models with moderate R\textsuperscript{2}, in which extreme values are underpredicted.
|
| 178 |
+
|
| 179 |
+
\begin{table}[H]
|
| 180 |
+
\centering
|
| 181 |
+
\caption{Test performance of CycPepGNN and previously published baselines on the CycPeptMPDB PAMPA task. MSE is in original log $P_{\text{app}}$ units.}
|
| 182 |
+
\label{tab:results}
|
| 183 |
+
\begin{tabular}{llcc}
|
| 184 |
+
\hline
|
| 185 |
+
\textbf{Model} & \textbf{Data} & \textbf{MSE} & \textbf{R$^2$} \\
|
| 186 |
+
\hline
|
| 187 |
+
CycPepGNN & Full (6{,}941) & 0.601 & 0.467 \\
|
| 188 |
+
CycPepGNN (4D branch) & 4D subset (5{,}160) & 0.632 & 0.429 \\
|
| 189 |
+
CycPeptMP~\cite{li2021} & Full (lit.) & 0.271 & 0.780 \\
|
| 190 |
+
MultiCycPermea~\cite{wang2023} & Full (lit.) & 0.160 & $\sim$0.75 \\
|
| 191 |
+
MSF-CPMP~\cite{luo2024} (SOTA) & Full (lit.) & 0.092 & 0.88 \\
|
| 192 |
+
\hline
|
| 193 |
+
\end{tabular}
|
| 194 |
+
\end{table}
|
| 195 |
+
|
| 196 |
+
\subsection{Effect of the 4D conformational features}
|
| 197 |
+
Because the 4D features are available for only 5{,}160 of the 6{,}941 PAMPA peptides, the fair comparison for evaluating the 4D branch is not against the full-dataset model but against an otherwise identical model trained on the same 5{,}160-sample 4D subset with the 4D branch disabled. That controlled comparison is implemented in \texttt{train\_comprehensive.py} (as $\mathrm{4D_{sub}}(no4D)$ versus $\mathrm{4D_{sub}}(+4D)$, both with sequence encoding and augmentation available as ablations). On the reported full-versus-4D runs, adding the 4D branch slightly degrades performance (MSE 0.632 versus 0.601 on their respective datasets), consistent with the dominant effect being the $\sim$26\% reduction in training data rather than the shape of the 4D features themselves.
|
| 198 |
+
|
| 199 |
+
\subsection{Comparison with published baselines}
|
| 200 |
+
State-of-the-art models report substantially stronger performance: MSF-CPMP~\cite{luo2024} reaches MSE 0.092 and R\textsuperscript{2} 0.88, and CycPeptMP~\cite{li2021} reports MSE 0.271 and R\textsuperscript{2} 0.780. Our R\textsuperscript{2} of 0.467 therefore leaves a large, systematic gap. The comparison highlights that a purely atom-level GNN with auxiliary whole-molecule descriptors, trained on a single assay without augmentation, does not yet capture the monomer composition and data-multiplication signal exploited by the strongest baselines.
|
| 201 |
+
|
| 202 |
+
\section{Ablation Analysis}
|
| 203 |
+
\label{sec:ablation}
|
| 204 |
+
|
| 205 |
+
\subsection{SMILES-enumeration augmentation}
|
| 206 |
+
CycPeptMP and MSF-CPMP both use 60$\times$ SMILES enumeration~\cite{bjerrum2017}, producing sixty valid SMILES for each peptide by randomizing atom ordering. This provides an effective 60-fold increase in training data and is consistently reported to improve generalization for molecular property models. Our reported numbers use each peptide exactly once. The repository's augmentation path (\texttt{train\_final.py}, \texttt{train\_comprehensive.py}) makes the enumeration multiplicity configurable, so the effect of augmentation on the exact reported baselines can be measured directly.
|
| 207 |
+
|
| 208 |
+
\subsection{Monomer-level sequence encoding}
|
| 209 |
+
Cyclic peptides are composed of amino-acid monomers arranged in a macrocycle. Both CycPeptMP and MSF-CPMP use an explicit encoding of the amino-acid sequence, providing information about which residues compose the peptide--information that is only indirectly available from the molecular graph and is entirely absent from the 2D descriptor vector. Our optional sequence branch encodes the amino-acid composition as a normalized 20-dimensional one-hot, allowing a controlled test of this hypothesis.
|
| 210 |
+
|
| 211 |
+
\subsection{Multi-task learning}
|
| 212 |
+
MSF-CPMP is trained jointly across all four assay types in CycPeptMPDB (PAMPA, Caco-2, MDCK, RRCK), which may improve generalization through shared representation learning across correlated biological endpoints~\cite{wang2023}. Our model is trained on PAMPA alone; extending the (code-shared) architecture to a multi-task head is straightforward and is listed as future work.
|
| 213 |
+
|
| 214 |
+
\subsection{Utility of the 4D features}
|
| 215 |
+
The 4D conformational features did not improve our model. Aside from the reduced dataset size, this may reflect their use with a less expressive base model: a model that already extracts most of its signal from 2D structure may not be able to exploit additional conformational information, whereas a stronger base model operating near the ceiling of 2D-only performance could. The release of the 4D features~\cite{yuki2026} is recent, and their combination with state-of-the-art architectures remains an open question.
|
| 216 |
+
|
| 217 |
+
\section{Discussion}
|
| 218 |
+
|
| 219 |
+
Our results confirm that an atom-level GNN fused with global descriptors is a competent but not competitive baseline for PAMPA permeability prediction on CycPeptMPDB. The gap to the state of the art is large and systematic, and we attribute it to three specific, testable methodological differences rather than to a fundamental limitation of graph representation learning:
|
| 220 |
+
\begin{enumerate}
|
| 221 |
+
\item \textbf{Data efficiency.} With 6{,}941 training examples and no augmentation, expressive models overfit quickly. Sixty-fold enumeration~\cite{bjerrum2017} is the single largest structural difference between our pipeline and the strongest baselines.
|
| 222 |
+
\item \textbf{Missing sequence signal.} The monomer composition of a cyclic peptide is a decisive determinant of its ability to adopt permeable conformations; neither an atom-level graph nor global 2D descriptors encode this signal as explicitly as a monomer-level one-hot.
|
| 223 |
+
\item \textbf{Single-task training.} Multi-task regularization across four correlated assays demonstrably helps in published models~\cite{wang2023, luo2024} and is absent from our pipeline.
|
| 224 |
+
\end{enumerate}
|
| 225 |
+
|
| 226 |
+
Each of the three items is already implemented in the repository and can be toggled independently, so the open-source release lets follow-up work resolve, quantitatively, how much of the remaining gap each component closes. The 4D features, while designed to capture precisely the conformational flexibility that governs permeability, did not help our baseline; we consider their interaction with a stronger, augmented, multi-task base model to be the most promising avenue for leveraging them.
|
| 227 |
+
|
| 228 |
+
\section{Limitations}
|
| 229 |
+
\label{sec:lim}
|
| 230 |
+
Several limitations qualify our findings. First, the reported numbers use a single random split and a single seed convention; variance across seeds has not been characterized, and cross-split comparison with published numbers should be treated with appropriate caution. Second, the 4D-annotated subset is smaller than the full PAMPA set, and the exact controlled 4D on/off ablation shares this reduced-size confound when compared against other published results. Third, our reported results do not yet include the augmentation, sequence, or multi-task variants; these are provided as code and remain to be evaluated under the same protocol. Fourth, the PAMPA assay is an in vitro proxy and does not capture the full complexity of oral absorption. Finally, the descriptor values are computed with a specific RDKit version and may shift slightly across RDKit releases.
|
| 231 |
+
|
| 232 |
+
\section{Reproducibility}
|
| 233 |
+
\label{sec:repro}
|
| 234 |
+
The repository provides: (i) the four training scripts implementing the baseline (\texttt{train.py}; CycPepGNN), the refined five-layer variant with sequence encoding and SMILES enumeration (\texttt{train\_final.py}), the ablative experiment harness (\texttt{train\_comprehensive.py}), and the frozen-ChemBERTa-2 variant (\texttt{train2.py}); (ii) a pinned \texttt{requirements.txt}; (iii) pretrained checkpoints for the full-dataset and 4D variants at two hidden-width settings; and (iv) this manuscript and its figures. All scripts fetch the dataset directly from the public archives at runtime, so a fresh run requires only a working network connection, an RDKit-compatible Python environment, and a CUDA-capable GPU. The exact feature-key lists used by each script are preserved verbatim in the code so that descriptor dimensionality (Table~\ref{tab:feats}) can be cross-checked against the checkpoints.
|
| 235 |
+
|
| 236 |
+
\section{Conclusion}
|
| 237 |
+
|
| 238 |
+
We presented CycPepGNN, a GNN-based baseline for cyclic peptide membrane permeability prediction on the CycPeptMPDB PAMPA dataset, optionally augmented with CycPeptMPDB-4D conformational features. Our model achieves an R\textsuperscript{2} of 0.467 on the full dataset and 0.429 with the 4D branch on the 4D-annotated subset, substantially below the state of the art. We identified SMILES-enumeration data augmentation, monomer-level sequence encoding, and multi-task learning as the primary, testable methodological differences that explain the gap, and we released the code, data pipeline, and checkpoints needed to evaluate each hypothesis. Future work should (i) measure the effect of each component in controlled ablations, (ii) train a multi-task variant across all four assays, (iii) combine 4D conformational features with a stronger augmented base model, and (iv) characterize seed variance with repeated runs.
|
| 239 |
+
|
| 240 |
+
\begin{thebibliography}{00}
|
| 241 |
+
|
| 242 |
+
\bibitem{rezai2006} Y.~Rezai, B.~Ursu, C.~W.~Torres, and R.~S.~Lokey, ``Cyclic peptide oral bioavailability,'' \textit{J. Med. Chem.}, vol.~49, no.~24, pp.~7219--7226, 2006.
|
| 243 |
+
|
| 244 |
+
\bibitem{white2011} T.~R.~White \textit{et al.}, ``On-resin N-methylation of cyclic peptides,'' \textit{Nat. Chem. Biol.}, vol.~7, pp.~810--817, 2011.
|
| 245 |
+
|
| 246 |
+
\bibitem{lipinski1997} C.~A.~Lipinski, F.~Lombardo, B.~W.~Dominy, and P.~J.~Feeney, ``Experimental and computational approaches to estimate solubility and permeability in drug discovery and development settings,'' \textit{Adv. Drug Deliv. Rev.}, vol.~23, pp.~3--25, 1997.
|
| 247 |
+
|
| 248 |
+
\bibitem{ji2023} X.~Ji \textit{et al.}, ``CycPeptMPDB: A comprehensive database of cyclic peptide permeability,'' \textit{Nucleic Acids Res.}, vol.~51, 2023.
|
| 249 |
+
|
| 250 |
+
\bibitem{li2021} J.~Li \textit{et al.}, ``CycPeptMP: A machine learning model for cyclic peptide membrane permeability prediction,'' \textit{Bioinformatics}, 2021.
|
| 251 |
+
|
| 252 |
+
\bibitem{wang2023} H.~Wang \textit{et al.}, ``MultiCycPermea: Multi-task learning for cyclic peptide permeability prediction,'' \textit{J. Chem. Inf. Model.}, 2023.
|
| 253 |
+
|
| 254 |
+
\bibitem{luo2024} Y.~Luo \textit{et al.}, ``MSF-CPMP: Multi-scale fusion model for cyclic peptide membrane permeability prediction,'' \textit{bioRxiv}, 2024.
|
| 255 |
+
|
| 256 |
+
\bibitem{yuki2026} H.~Yuki \textit{et al.}, ``CycPeptMPDB-4D: Conformational dynamics features for cyclic peptide permeability prediction,'' \textit{Zenodo}, 2026.
|
| 257 |
+
|
| 258 |
+
\bibitem{hess2008} B.~Hess \textit{et al.}, ``GROMACS 4: Algorithms for highly efficient, load-balanced, and scalable molecular simulation,'' \textit{J. Chem. Theory Comput.}, vol.~4, pp.~435--447, 2008.
|
| 259 |
+
|
| 260 |
+
\bibitem{leonard2019} L.~Leonard \textit{et al.}, ``The role of conformational dynamics in cyclic peptide membrane permeability,'' \textit{J. Chem. Inf. Model.}, vol.~59, pp.~2990--3000, 2019.
|
| 261 |
+
|
| 262 |
+
\bibitem{xu2019} K.~Xu, W.~Hu, J.~Leskovec, and S.~Jegelka, ``How powerful are graph neural networks?'' in \textit{Proc. ICLR}, 2019.
|
| 263 |
+
|
| 264 |
+
\bibitem{fey2019} M.~Fey and J.~E.~Lenssen, ``Fast graph representation learning with PyTorch Geometric,'' in \textit{Proc. ICLR Workshop on Representation Learning on Graphs and Manifolds}, 2019.
|
| 265 |
+
|
| 266 |
+
\bibitem{rogers2010} D.~Rogers and M.~Hahn, ``Extended-connectivity fingerprints,'' \textit{J. Chem. Inf. Model.}, vol.~50, no.~5, pp.~742--754, 2010.
|
| 267 |
+
|
| 268 |
+
\bibitem{ahmad2022} W.~Ahmad \textit{et al.}, ``ChemBERTa-2: Towards chemical foundation models,'' \textit{arXiv:2209.01712}, 2022.
|
| 269 |
+
|
| 270 |
+
\bibitem{bjerrum2017} E.~J.~Bjerrum, ``SMILES enumeration as data augmentation for neural network models of molecules,'' \textit{arXiv:1703.07076}, 2017.
|
| 271 |
+
|
| 272 |
+
\bibitem{rdkit} G.~Landrum \textit{et al.}, ``RDKit: Open-source cheminformatics,'' https://www.rdkit.org.
|
| 273 |
+
|
| 274 |
+
\end{thebibliography}
|
| 275 |
+
|
| 276 |
+
\begin{figure}[H]
|
| 277 |
+
\centering
|
| 278 |
+
\includegraphics[width=\columnwidth]{fig_architecture.png}
|
| 279 |
+
\caption{The CycPepGNN architecture: four GINConv layers with concatenated mean-pooled readout, fused with descriptor, physicochemical, and (optional) 4D branches through a shared regression head.}
|
| 280 |
+
\label{fig:architecture}
|
| 281 |
+
\end{figure}
|
| 282 |
+
|
| 283 |
+
\begin{figure}[H]
|
| 284 |
+
\centering
|
| 285 |
+
\includegraphics[width=\columnwidth]{results_scatter.png}
|
| 286 |
+
\caption{Predicted versus actual PAMPA permeability (left) and residual distribution (right) for CycPepGNN on the full dataset.}
|
| 287 |
+
\label{fig:scatter}
|
| 288 |
+
\end{figure}
|
| 289 |
+
|
| 290 |
+
\begin{figure}[H]
|
| 291 |
+
\centering
|
| 292 |
+
\includegraphics[width=\columnwidth]{results_table.png}
|
| 293 |
+
\caption{Comparison of our GNN results with published baselines on the CycPeptMPDB PAMPA dataset.}
|
| 294 |
+
\label{tab:resultsfig}
|
| 295 |
+
\end{figure}
|
| 296 |
+
|
| 297 |
+
\end{document}
|
requirements.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CycPepGNN - research dependencies
|
| 2 |
+
# PyTorch with CUDA is required (tests on RTX 4060 Laptop, 8 GB).
|
| 3 |
+
# Recommended: install PyTorch from https://pytorch.org first, then:
|
| 4 |
+
# pip install torch_geometric
|
| 5 |
+
# conda install -c conda-forge rdkit # or: pip install rdkit-pypi
|
| 6 |
+
#
|
| 7 |
+
# torch, torch_geometric, and rdkit must match your CUDA/Python setup;
|
| 8 |
+
# the versions below are the ones used for the reported experiments.
|
| 9 |
+
|
| 10 |
+
torch==2.11.0
|
| 11 |
+
torch_geometric==2.7.0
|
| 12 |
+
rdkit==2022.09.1
|
| 13 |
+
|
| 14 |
+
# Common utilities
|
| 15 |
+
numpy>=1.26.0
|
| 16 |
+
requests>=2.31.0
|
| 17 |
+
matplotlib>=3.8.0
|
| 18 |
+
|
| 19 |
+
# Only required by train2.py (frozen ChemBERTa-2 SMILES encoder)
|
| 20 |
+
transformers>=4.30.0
|
results_scatter.png
ADDED
|
Git LFS Details
|
results_table.png
ADDED
|
train.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv, io, requests, random, time, math
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
from torch.utils.data import Dataset, DataLoader
|
| 7 |
+
from torch_geometric.data import Data, Batch
|
| 8 |
+
from torch_geometric.nn import GINConv, global_add_pool, global_mean_pool, BatchNorm
|
| 9 |
+
from torch_geometric.nn import MLP as PyGMLP
|
| 10 |
+
from rdkit import Chem, RDLogger
|
| 11 |
+
from rdkit.Chem import AllChem
|
| 12 |
+
RDLogger.logger().setLevel(RDLogger.ERROR)
|
| 13 |
+
|
| 14 |
+
# βββ Data βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
+
def load_data():
|
| 16 |
+
r = requests.get('https://raw.githubusercontent.com/akiyamalab/cycpeptmp/main/data/CycPeptMPDB_Peptide_All.csv')
|
| 17 |
+
rows = list(csv.DictReader(io.StringIO(r.content.decode('utf-8-sig'))))
|
| 18 |
+
r4 = requests.get('https://zenodo.org/records/18754430/files/CycPeptMPDB-4D.csv')
|
| 19 |
+
d4 = {int(rr['CycPeptMPDB_ID']): rr for rr in csv.DictReader(io.StringIO(r4.content.decode('utf-8')))}
|
| 20 |
+
data = []
|
| 21 |
+
for row in rows:
|
| 22 |
+
if not row['PAMPA']: continue
|
| 23 |
+
mol = Chem.MolFromSmiles(row['SMILES'])
|
| 24 |
+
if mol is None: continue
|
| 25 |
+
rid = int(row['CycPeptMPDB_ID'])
|
| 26 |
+
data.append({**row, 'mol': mol, 'd4': d4.get(rid), 'id': rid})
|
| 27 |
+
n4d = sum(1 for d in data if d['d4'])
|
| 28 |
+
print(f'Loaded {len(data)} PAMPA entries ({n4d} with 4D)')
|
| 29 |
+
return data
|
| 30 |
+
|
| 31 |
+
# βββ Graph construction βββββββββββββββββββββββββββββββββββββββββ
|
| 32 |
+
ATOM_TYPES = [5,6,7,8,9,15,16,17,35,53]
|
| 33 |
+
|
| 34 |
+
def mol_to_pyg(mol):
|
| 35 |
+
mol = Chem.AddHs(mol)
|
| 36 |
+
atoms = list(mol.GetAtoms())
|
| 37 |
+
n = len(atoms)
|
| 38 |
+
|
| 39 |
+
x = []
|
| 40 |
+
for a in atoms:
|
| 41 |
+
feat = []
|
| 42 |
+
# Atom type one-hot
|
| 43 |
+
t = [0]*len(ATOM_TYPES)
|
| 44 |
+
if a.GetAtomicNum() in ATOM_TYPES:
|
| 45 |
+
t[ATOM_TYPES.index(a.GetAtomicNum())] = 1
|
| 46 |
+
feat.extend(t)
|
| 47 |
+
feat += [a.GetDegree()/4.0, a.GetTotalNumHs()/3.0, a.GetFormalCharge()/1.0,
|
| 48 |
+
int(a.IsInRing()), int(a.GetIsAromatic())]
|
| 49 |
+
x.append(feat)
|
| 50 |
+
x = torch.tensor(x, dtype=torch.float)
|
| 51 |
+
|
| 52 |
+
edge_index, edge_attr = [], []
|
| 53 |
+
for i in range(n):
|
| 54 |
+
for j in range(i+1, n):
|
| 55 |
+
bond = mol.GetBondBetweenAtoms(i, j)
|
| 56 |
+
if bond is not None:
|
| 57 |
+
bt = bond.GetBondType()
|
| 58 |
+
e = [int(bt == Chem.rdchem.BondType.SINGLE),
|
| 59 |
+
int(bt == Chem.rdchem.BondType.DOUBLE),
|
| 60 |
+
int(bt == Chem.rdchem.BondType.TRIPLE),
|
| 61 |
+
int(bt == Chem.rdchem.BondType.AROMATIC)]
|
| 62 |
+
edge_index.extend([[i,j],[j,i]])
|
| 63 |
+
edge_attr.extend([e, e])
|
| 64 |
+
edge_index = torch.tensor(edge_index, dtype=torch.long).T if edge_index else torch.zeros((2,0), dtype=torch.long)
|
| 65 |
+
edge_attr = torch.tensor(edge_attr, dtype=torch.float) if edge_attr else torch.zeros((0,4), dtype=torch.float)
|
| 66 |
+
return Data(x=x, edge_index=edge_index, edge_attr=edge_attr)
|
| 67 |
+
|
| 68 |
+
# βββ Features βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
+
PHYSCHEM_KEYS = ['MolLogP','MolWt','TPSA','FractionCSP3','NumHAcceptors','NumHDonors',
|
| 70 |
+
'NumRotatableBonds','RingCount','HeavyAtomCount','LabuteASA',
|
| 71 |
+
'HallKierAlpha','Kappa1','Kappa2','Kappa3','BertzCT','BalabanJ']
|
| 72 |
+
|
| 73 |
+
DESC_KEYS = ['BCUT2D_MWHI','BCUT2D_MWLOW','BCUT2D_CHGHI','BCUT2D_CHGLO','BCUT2D_LOGPHI',
|
| 74 |
+
'BCUT2D_LOGPLOW','BCUT2D_MRHI','BCUT2D_MRLOW','BalabanJ','BertzCT',
|
| 75 |
+
'Chi0','Chi0n','Chi0v','Chi1','Chi1n','Chi1v','Chi2n','Chi2v','Chi3n','Chi3v','Chi4n','Chi4v',
|
| 76 |
+
'HallKierAlpha','Kappa1','Kappa2','Kappa3','LabuteASA',
|
| 77 |
+
'PEOE_VSA1','PEOE_VSA10','PEOE_VSA11','PEOE_VSA12','PEOE_VSA13','PEOE_VSA14',
|
| 78 |
+
'PEOE_VSA2','PEOE_VSA3','PEOE_VSA4','PEOE_VSA5','PEOE_VSA6','PEOE_VSA7','PEOE_VSA8','PEOE_VSA9',
|
| 79 |
+
'SMR_VSA1','SMR_VSA10','SMR_VSA2','SMR_VSA3','SMR_VSA4','SMR_VSA5','SMR_VSA6','SMR_VSA7','SMR_VSA8','SMR_VSA9',
|
| 80 |
+
'SlogP_VSA1','SlogP_VSA10','SlogP_VSA11','SlogP_VSA12','SlogP_VSA2','SlogP_VSA3','SlogP_VSA4',
|
| 81 |
+
'SlogP_VSA5','SlogP_VSA6','SlogP_VSA7','SlogP_VSA8','SlogP_VSA9',
|
| 82 |
+
'EState_VSA1','EState_VSA10','EState_VSA11','EState_VSA2','EState_VSA3','EState_VSA4',
|
| 83 |
+
'EState_VSA5','EState_VSA6','EState_VSA7','EState_VSA8','EState_VSA9',
|
| 84 |
+
'VSA_EState1','VSA_EState10','VSA_EState2','VSA_EState3','VSA_EState4','VSA_EState5',
|
| 85 |
+
'VSA_EState6','VSA_EState7','VSA_EState8','VSA_EState9',
|
| 86 |
+
'MolLogP','MolWt','TPSA','FractionCSP3','HeavyAtomCount','NHOHCount','NOCount',
|
| 87 |
+
'NumAliphaticCarbocycles','NumAliphaticHeterocycles','NumAliphaticRings',
|
| 88 |
+
'NumAromaticCarbocycles','NumAromaticHeterocycles','NumAromaticRings',
|
| 89 |
+
'NumHAcceptors','NumHDonors','NumHeteroatoms','NumRotatableBonds','RingCount',
|
| 90 |
+
'NumSaturatedCarbocycles','NumSaturatedHeterocycles','NumSaturatedRings',
|
| 91 |
+
'MolMR','qed','MaxAbsEStateIndex','MaxEStateIndex','MinEStateIndex',
|
| 92 |
+
'MaxPartialCharge','MinPartialCharge','MaxAbsPartialCharge','MinAbsPartialCharge',
|
| 93 |
+
'FpDensityMorgan1','FpDensityMorgan2','FpDensityMorgan3',
|
| 94 |
+
'ExactMolWt','HeavyAtomMolWt','NumValenceElectrons']
|
| 95 |
+
|
| 96 |
+
D4_FEAT_KEYS = ['Water_avgRMSD_All','Water_avgRMSD_BackBone','Desolvation_Free_Energy',
|
| 97 |
+
'Water_3D_SASA','Water_3D_NPSA','Water_3D_PSA',
|
| 98 |
+
'Hexane_avgRMSD_All','Hexane_avgRMSD_BackBone',
|
| 99 |
+
'Hexane_3D_SASA','Hexane_3D_NPSA','Hexane_3D_PSA']
|
| 100 |
+
|
| 101 |
+
def safe_float(v):
|
| 102 |
+
if v is None: return 0.0
|
| 103 |
+
try:
|
| 104 |
+
v = float(v)
|
| 105 |
+
return 0.0 if math.isnan(v) or math.isinf(v) else v
|
| 106 |
+
except: return 0.0
|
| 107 |
+
|
| 108 |
+
def extract_vec(row, keys):
|
| 109 |
+
return torch.tensor([safe_float(row.get(k)) for k in keys], dtype=torch.float)
|
| 110 |
+
|
| 111 |
+
def extract_d4(d4r):
|
| 112 |
+
if d4r is None: return None
|
| 113 |
+
return extract_vec(d4r, D4_FEAT_KEYS)
|
| 114 |
+
|
| 115 |
+
# βββ Dataset ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 116 |
+
class CycPepDataset(Dataset):
|
| 117 |
+
def __init__(self, data, t_mean, t_std, use_4d=True):
|
| 118 |
+
self.samples = []
|
| 119 |
+
for d in data:
|
| 120 |
+
d4 = extract_d4(d['d4'])
|
| 121 |
+
if use_4d and d4 is None: continue
|
| 122 |
+
if d4 is None: d4 = torch.zeros(len(D4_FEAT_KEYS))
|
| 123 |
+
pyg = mol_to_pyg(d['mol'])
|
| 124 |
+
desc = extract_vec(d, DESC_KEYS)
|
| 125 |
+
pc = extract_vec(d, PHYSCHEM_KEYS)
|
| 126 |
+
target = (float(d['PAMPA']) - t_mean) / t_std
|
| 127 |
+
self.samples.append((pyg, desc, pc, d4, target))
|
| 128 |
+
|
| 129 |
+
def __len__(self): return len(self.samples)
|
| 130 |
+
def __getitem__(self, i): return self.samples[i]
|
| 131 |
+
|
| 132 |
+
def collate_fn(batch):
|
| 133 |
+
pygs, descs, pcs, d4s, targets = zip(*batch)
|
| 134 |
+
batch_pyg = Batch.from_data_list(list(pygs))
|
| 135 |
+
return (batch_pyg,
|
| 136 |
+
torch.stack(descs), torch.stack(pcs), torch.stack(d4s),
|
| 137 |
+
torch.tensor(targets, dtype=torch.float))
|
| 138 |
+
|
| 139 |
+
# βββ Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 140 |
+
class CycPepGNN(nn.Module):
|
| 141 |
+
def __init__(self, node_dim=len(ATOM_TYPES)+5, edge_dim=4,
|
| 142 |
+
desc_dim=len(DESC_KEYS), pc_dim=len(PHYSCHEM_KEYS),
|
| 143 |
+
d4_dim=len(D4_FEAT_KEYS), hidden=256):
|
| 144 |
+
super().__init__()
|
| 145 |
+
self.node_emb = nn.Linear(node_dim, hidden)
|
| 146 |
+
self.edge_emb = nn.Linear(edge_dim, hidden)
|
| 147 |
+
|
| 148 |
+
nn1 = PyGMLP([hidden, hidden, hidden], batch_norm=True)
|
| 149 |
+
nn2 = PyGMLP([hidden, hidden, hidden], batch_norm=True)
|
| 150 |
+
nn3 = PyGMLP([hidden, hidden, hidden], batch_norm=True)
|
| 151 |
+
nn4 = PyGMLP([hidden, hidden, hidden], batch_norm=True)
|
| 152 |
+
self.convs = nn.ModuleList([
|
| 153 |
+
GINConv(nn1, train_eps=True),
|
| 154 |
+
GINConv(nn2, train_eps=True),
|
| 155 |
+
GINConv(nn3, train_eps=True),
|
| 156 |
+
GINConv(nn4, train_eps=True),
|
| 157 |
+
])
|
| 158 |
+
self.bns = nn.ModuleList([BatchNorm(hidden) for _ in range(4)])
|
| 159 |
+
self.graph_proj = nn.Linear(hidden * 4, hidden)
|
| 160 |
+
|
| 161 |
+
self.desc_net = nn.Sequential(nn.Linear(desc_dim, 64), nn.GELU(), nn.LayerNorm(64))
|
| 162 |
+
self.pc_net = nn.Sequential(nn.Linear(pc_dim, 32), nn.GELU(), nn.LayerNorm(32))
|
| 163 |
+
self.d4_net = nn.Sequential(nn.Linear(d4_dim, 16), nn.GELU(), nn.LayerNorm(16))
|
| 164 |
+
|
| 165 |
+
fusion = hidden + 64 + 32 + 16
|
| 166 |
+
self.head = nn.Sequential(
|
| 167 |
+
nn.Linear(fusion, 256), nn.GELU(), nn.Dropout(0.15),
|
| 168 |
+
nn.Linear(256, 128), nn.GELU(), nn.Dropout(0.1),
|
| 169 |
+
nn.Linear(128, 1))
|
| 170 |
+
|
| 171 |
+
def forward(self, pyg_data, desc, pc, d4):
|
| 172 |
+
x = F.relu(self.node_emb(pyg_data.x))
|
| 173 |
+
e = F.relu(self.edge_emb(pyg_data.edge_attr))
|
| 174 |
+
|
| 175 |
+
xs = []
|
| 176 |
+
for conv, bn in zip(self.convs, self.bns):
|
| 177 |
+
x = conv(x, pyg_data.edge_index)
|
| 178 |
+
x = bn(x)
|
| 179 |
+
x = F.relu(x)
|
| 180 |
+
xs.append(x)
|
| 181 |
+
# Readout: concat all layer outputs
|
| 182 |
+
h_graph = torch.cat([global_mean_pool(x, pyg_data.batch) for x in xs], -1)
|
| 183 |
+
# Re-project to hidden
|
| 184 |
+
h_graph = self.graph_proj(h_graph)
|
| 185 |
+
|
| 186 |
+
h_desc = self.desc_net(desc)
|
| 187 |
+
h_pc = self.pc_net(pc)
|
| 188 |
+
h_d4 = self.d4_net(d4)
|
| 189 |
+
h = torch.cat([h_graph, h_desc, h_pc, h_d4], 1)
|
| 190 |
+
return self.head(h).squeeze(-1)
|
| 191 |
+
|
| 192 |
+
# βββ Training ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 193 |
+
def train_epoch(model, loader, opt, device):
|
| 194 |
+
model.train()
|
| 195 |
+
total = 0
|
| 196 |
+
for batch in loader:
|
| 197 |
+
pyg = batch[0].to(device)
|
| 198 |
+
desc, pc, d4, t = [x.to(device) for x in batch[1:]]
|
| 199 |
+
opt.zero_grad()
|
| 200 |
+
loss = F.mse_loss(model(pyg, desc, pc, d4), t)
|
| 201 |
+
loss.backward()
|
| 202 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)
|
| 203 |
+
opt.step()
|
| 204 |
+
total += loss.item() * t.size(0)
|
| 205 |
+
return total / len(loader.dataset)
|
| 206 |
+
|
| 207 |
+
@torch.no_grad()
|
| 208 |
+
def evaluate(model, loader, device):
|
| 209 |
+
model.eval()
|
| 210 |
+
preds, targets = [], []
|
| 211 |
+
for batch in loader:
|
| 212 |
+
pyg = batch[0].to(device)
|
| 213 |
+
desc, pc, d4, t = [x.to(device) for x in batch[1:]]
|
| 214 |
+
preds.append(model(pyg, desc, pc, d4).cpu())
|
| 215 |
+
targets.append(t.cpu())
|
| 216 |
+
preds = torch.cat(preds); targets = torch.cat(targets)
|
| 217 |
+
mse = F.mse_loss(preds, targets).item()
|
| 218 |
+
mae = F.l1_loss(preds, targets).item()
|
| 219 |
+
r2 = 1 - mse / targets.var().item() if targets.var().item() > 0 else 0
|
| 220 |
+
return mse, mae, r2
|
| 221 |
+
|
| 222 |
+
# βββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 223 |
+
def main():
|
| 224 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 225 |
+
print(f'Device: {device}')
|
| 226 |
+
|
| 227 |
+
data = load_data()
|
| 228 |
+
all_pampa = torch.tensor([float(d['PAMPA']) for d in data])
|
| 229 |
+
t_mean, t_std = all_pampa.mean(), all_pampa.std()
|
| 230 |
+
print(f'Target: mean={t_mean:.3f} std={t_std:.3f}')
|
| 231 |
+
|
| 232 |
+
def run_experiment(name, use_4d, hidden=256, epochs=200):
|
| 233 |
+
ds = CycPepDataset(data, t_mean, t_std, use_4d=use_4d)
|
| 234 |
+
n = len(ds)
|
| 235 |
+
indices = list(range(n))
|
| 236 |
+
random.seed(42); random.shuffle(indices)
|
| 237 |
+
tr, va = int(0.8*n), int(0.1*n)
|
| 238 |
+
te = n - tr - va
|
| 239 |
+
tr_i, va_i, te_i = indices[:tr], indices[tr:tr+va], indices[tr+va:]
|
| 240 |
+
|
| 241 |
+
tr_ds = torch.utils.data.Subset(ds, tr_i)
|
| 242 |
+
va_ds = torch.utils.data.Subset(ds, va_i)
|
| 243 |
+
te_ds = torch.utils.data.Subset(ds, te_i)
|
| 244 |
+
bs = min(64, n//10)
|
| 245 |
+
tr_ld = DataLoader(tr_ds, bs, shuffle=True, collate_fn=collate_fn)
|
| 246 |
+
va_ld = DataLoader(va_ds, bs, shuffle=False, collate_fn=collate_fn)
|
| 247 |
+
te_ld = DataLoader(te_ds, bs, shuffle=False, collate_fn=collate_fn)
|
| 248 |
+
|
| 249 |
+
model = CycPepGNN(hidden=hidden).to(device)
|
| 250 |
+
n_p = sum(p.numel() for p in model.parameters())
|
| 251 |
+
print(f'{name}: {n:,} samples, {n_p:,} params')
|
| 252 |
+
|
| 253 |
+
opt = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=1e-5)
|
| 254 |
+
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)
|
| 255 |
+
|
| 256 |
+
best_val = float('inf')
|
| 257 |
+
best_te = None
|
| 258 |
+
patience = 0
|
| 259 |
+
t0 = time.time()
|
| 260 |
+
for ep in range(epochs):
|
| 261 |
+
loss = train_epoch(model, tr_ld, opt, device)
|
| 262 |
+
vm, vma, vr2 = evaluate(model, va_ld, device)
|
| 263 |
+
sched.step()
|
| 264 |
+
vm_u = vm * t_std.item()**2
|
| 265 |
+
if (ep+1) % 20 == 0 or ep == 0:
|
| 266 |
+
print(f' E{ep+1:3d} loss={loss:.4f} val_mse={vm_u:.4f} val_r2={vr2:.4f}')
|
| 267 |
+
if vm < best_val:
|
| 268 |
+
best_val = vm
|
| 269 |
+
best_te = evaluate(model, te_ld, device)
|
| 270 |
+
patience = 0
|
| 271 |
+
else:
|
| 272 |
+
patience += 1
|
| 273 |
+
if patience >= 30:
|
| 274 |
+
print(f' Early stop at E{ep+1}')
|
| 275 |
+
break
|
| 276 |
+
elapsed = time.time() - t0
|
| 277 |
+
te_m_u = best_te[0] * t_std.item()**2
|
| 278 |
+
te_ma_u = best_te[1] * t_std.item()
|
| 279 |
+
print(f' TEST: MSE={te_m_u:.4f} MAE={te_ma_u:.4f} RΒ²={best_te[2]:.4f} time={elapsed:.0f}s')
|
| 280 |
+
return best_te
|
| 281 |
+
|
| 282 |
+
results = []
|
| 283 |
+
# Full dataset (no 4D)
|
| 284 |
+
res = run_experiment('GNN_Full', use_4d=False, hidden=256)
|
| 285 |
+
results.append(('GNN_Full', *[res[0]*t_std.item()**2, res[1]*t_std.item(), res[2]]))
|
| 286 |
+
|
| 287 |
+
# With 4D
|
| 288 |
+
res2 = run_experiment('GNN_4D', use_4d=True, hidden=256)
|
| 289 |
+
results.append(('GNN_4D', *[res2[0]*t_std.item()**2, res2[1]*t_std.item(), res2[2]]))
|
| 290 |
+
|
| 291 |
+
print('\n' + '='*60)
|
| 292 |
+
print('SUMMARY:')
|
| 293 |
+
print(f'{"Model":<20} {"MSE":<10} {"MAE":<10} {"RΒ²":<10}')
|
| 294 |
+
print('-'*60)
|
| 295 |
+
for r in results:
|
| 296 |
+
print(f'{r[0]:<20} {r[1]:<10.4f} {r[2]:<10.4f} {r[3]:<10.4f}')
|
| 297 |
+
print('-'*60)
|
| 298 |
+
print(f'{"MSF-CPMP (SOTA)":<20} {"0.092":<10} {"0.242":<10} {"~0.88":<10}')
|
| 299 |
+
print(f'{"MultiCycPermea":<20} {"0.160":<10} {"0.280":<10} {"~0.75":<10}')
|
| 300 |
+
|
| 301 |
+
if __name__ == '__main__':
|
| 302 |
+
main()
|
train2.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv, io, requests, random, time, math, sys
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
from torch.utils.data import Dataset, DataLoader
|
| 7 |
+
from rdkit import Chem, RDLogger
|
| 8 |
+
from rdkit.Chem import AllChem
|
| 9 |
+
from transformers import AutoTokenizer, AutoModel
|
| 10 |
+
RDLogger.logger().setLevel(RDLogger.ERROR)
|
| 11 |
+
|
| 12 |
+
# βββ Data βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 13 |
+
def load_data():
|
| 14 |
+
r = requests.get('https://raw.githubusercontent.com/akiyamalab/cycpeptmp/main/data/CycPeptMPDB_Peptide_All.csv')
|
| 15 |
+
rows = list(csv.DictReader(io.StringIO(r.content.decode('utf-8-sig'))))
|
| 16 |
+
r4 = requests.get('https://zenodo.org/records/18754430/files/CycPeptMPDB-4D.csv')
|
| 17 |
+
d4 = {int(rr['CycPeptMPDB_ID']): rr for rr in csv.DictReader(io.StringIO(r4.content.decode('utf-8')))}
|
| 18 |
+
data = []
|
| 19 |
+
for row in rows:
|
| 20 |
+
if not row['PAMPA']: continue
|
| 21 |
+
mol = Chem.MolFromSmiles(row['SMILES'])
|
| 22 |
+
if mol is None: continue
|
| 23 |
+
rid = int(row['CycPeptMPDB_ID'])
|
| 24 |
+
data.append({**row, 'mol': mol, 'd4': d4.get(rid), 'id': rid})
|
| 25 |
+
n4d = sum(1 for d in data if d['d4'])
|
| 26 |
+
print(f'Loaded {len(data)} PAMPA entries ({n4d} with 4D)')
|
| 27 |
+
return data
|
| 28 |
+
|
| 29 |
+
# βββ Features βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
PHYSCHEM_KEYS = ['MolLogP','MolWt','TPSA','FractionCSP3','NumHAcceptors','NumHDonors',
|
| 31 |
+
'NumRotatableBonds','RingCount','HeavyAtomCount','LabuteASA',
|
| 32 |
+
'HallKierAlpha','Kappa1','Kappa2','Kappa3','BertzCT','BalabanJ']
|
| 33 |
+
|
| 34 |
+
D4_FEAT_KEYS = ['Water_avgRMSD_All','Water_avgRMSD_BackBone','Desolvation_Free_Energy',
|
| 35 |
+
'Water_3D_SASA','Water_3D_NPSA','Water_3D_PSA',
|
| 36 |
+
'Hexane_avgRMSD_All','Hexane_avgRMSD_BackBone',
|
| 37 |
+
'Hexane_3D_SASA','Hexane_3D_NPSA','Hexane_3D_PSA']
|
| 38 |
+
|
| 39 |
+
def safe_float(v):
|
| 40 |
+
if v is None: return 0.0
|
| 41 |
+
try:
|
| 42 |
+
v = float(v)
|
| 43 |
+
return 0.0 if math.isnan(v) or math.isinf(v) else v
|
| 44 |
+
except: return 0.0
|
| 45 |
+
|
| 46 |
+
def extract_vec(row, keys):
|
| 47 |
+
return torch.tensor([safe_float(row.get(k)) for k in keys], dtype=torch.float)
|
| 48 |
+
|
| 49 |
+
CHEM_DESC = [
|
| 50 |
+
'MolLogP','MolWt','TPSA','FractionCSP3','NumHAcceptors','NumHDonors',
|
| 51 |
+
'NumRotatableBonds','RingCount','HeavyAtomCount','LabuteASA',
|
| 52 |
+
'NumAliphaticRings','NumAromaticRings','NumSaturatedRings',
|
| 53 |
+
'qed','BertzCT','BalabanJ','HallKierAlpha',
|
| 54 |
+
'MinPartialCharge','MaxPartialCharge','MinAbsPartialCharge','MaxAbsPartialCharge',
|
| 55 |
+
'NumValenceElectrons','NHOHCount','NOCount',
|
| 56 |
+
'Kappa1','Kappa2','Kappa3','MolMR',
|
| 57 |
+
'FpDensityMorgan1','FpDensityMorgan2','FpDensityMorgan3',
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
def compute_morgan(mol, bits=2048):
|
| 61 |
+
fp = AllChem.GetMorganFingerprintAsBitVect(mol, 3, nBits=bits)
|
| 62 |
+
return torch.tensor(fp, dtype=torch.float)
|
| 63 |
+
|
| 64 |
+
def extract_desc(row):
|
| 65 |
+
return extract_vec(row, CHEM_DESC)
|
| 66 |
+
|
| 67 |
+
# Pretrained model for SMILES
|
| 68 |
+
print('Loading ChemBERTa-2 tokenizer/model...')
|
| 69 |
+
tok = AutoTokenizer.from_pretrained('seyonec/PubChem10M_SMILES_BPE_450k')
|
| 70 |
+
chemberta = AutoModel.from_pretrained('seyonec/PubChem10M_SMILES_BPE_450k')
|
| 71 |
+
chemberta.eval()
|
| 72 |
+
for p in chemberta.parameters():
|
| 73 |
+
p.requires_grad = False
|
| 74 |
+
chem_dim = 768
|
| 75 |
+
print(f'Model loaded (dim={chem_dim})')
|
| 76 |
+
|
| 77 |
+
@torch.no_grad()
|
| 78 |
+
def smiles_embed(smiles):
|
| 79 |
+
inputs = tok(smiles, return_tensors='pt', padding=True, truncation=True, max_length=128)
|
| 80 |
+
if torch.cuda.is_available():
|
| 81 |
+
inputs = {k: v.cuda() for k, v in inputs.items()}
|
| 82 |
+
chemberta.cuda()
|
| 83 |
+
outputs = chemberta(**inputs)
|
| 84 |
+
emb = outputs.last_hidden_state[:,0,:] # CLS token
|
| 85 |
+
return emb.cpu()
|
| 86 |
+
|
| 87 |
+
# βββ Dataset ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 88 |
+
class CycPepDataset(Dataset):
|
| 89 |
+
def __init__(self, data, t_mean, t_std):
|
| 90 |
+
self.samples = []
|
| 91 |
+
for d in data:
|
| 92 |
+
d4 = extract_d4(d['d4'])
|
| 93 |
+
if d4 is None: d4 = torch.zeros(len(D4_FEAT_KEYS))
|
| 94 |
+
fp = compute_morgan(d['mol'])
|
| 95 |
+
desc = extract_desc(d)
|
| 96 |
+
target = (float(d['PAMPA']) - t_mean) / t_std
|
| 97 |
+
self.samples.append((d['SMILES'], fp, desc, d4, target))
|
| 98 |
+
# Precompute ChemBERTa embeddings
|
| 99 |
+
all_smiles = [s[0] for s in self.samples]
|
| 100 |
+
self.chem_embs = []
|
| 101 |
+
bs = 64
|
| 102 |
+
for i in range(0, len(all_smiles), bs):
|
| 103 |
+
batch_smiles = all_smiles[i:i+bs]
|
| 104 |
+
emb = smiles_embed(batch_smiles)
|
| 105 |
+
self.chem_embs.append(emb)
|
| 106 |
+
self.chem_embs = torch.cat(self.chem_embs, 0)
|
| 107 |
+
print(f'Precomputed ChemBERTa embeddings: {self.chem_embs.shape}')
|
| 108 |
+
|
| 109 |
+
# Replace SMILES with embeddings
|
| 110 |
+
self.samples = [(self.chem_embs[i], fp, desc, d4, t)
|
| 111 |
+
for i, (_, fp, desc, d4, t) in enumerate(self.samples)]
|
| 112 |
+
|
| 113 |
+
def __len__(self): return len(self.samples)
|
| 114 |
+
def __getitem__(self, i): return self.samples[i]
|
| 115 |
+
|
| 116 |
+
def collate_fn(batch):
|
| 117 |
+
chem, fp, desc, d4, targets = zip(*batch)
|
| 118 |
+
return (torch.stack(chem), torch.stack(fp), torch.stack(desc),
|
| 119 |
+
torch.stack(d4), torch.tensor(targets, dtype=torch.float))
|
| 120 |
+
|
| 121 |
+
# βββ Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 122 |
+
class CycPepModel(nn.Module):
|
| 123 |
+
def __init__(self, chem_dim=768, fp_dim=2048, desc_dim=len(CHEM_DESC),
|
| 124 |
+
d4_dim=len(D4_FEAT_KEYS), hidden=256):
|
| 125 |
+
super().__init__()
|
| 126 |
+
self.chem_net = nn.Sequential(nn.LayerNorm(chem_dim), nn.Linear(chem_dim, 128), nn.GELU())
|
| 127 |
+
self.fp_net = nn.Sequential(nn.LayerNorm(fp_dim), nn.Linear(fp_dim, 128), nn.GELU())
|
| 128 |
+
self.desc_net = nn.Sequential(nn.LayerNorm(desc_dim), nn.Linear(desc_dim, 64), nn.GELU())
|
| 129 |
+
self.d4_net = nn.Sequential(nn.LayerNorm(d4_dim), nn.Linear(d4_dim, 32), nn.GELU())
|
| 130 |
+
|
| 131 |
+
fusion = 128 + 128 + 64 + 32
|
| 132 |
+
self.head = nn.Sequential(
|
| 133 |
+
nn.Linear(fusion, hidden), nn.GELU(), nn.Dropout(0.3),
|
| 134 |
+
nn.Linear(hidden, hidden//2), nn.GELU(), nn.Dropout(0.2),
|
| 135 |
+
nn.Linear(hidden//2, 1))
|
| 136 |
+
|
| 137 |
+
def forward(self, chem, fp, desc, d4):
|
| 138 |
+
h = torch.cat([self.chem_net(chem), self.fp_net(fp),
|
| 139 |
+
self.desc_net(desc), self.d4_net(d4)], 1)
|
| 140 |
+
return self.head(h).squeeze(-1)
|
| 141 |
+
|
| 142 |
+
# βββ Training ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 143 |
+
def train_epoch(model, loader, opt, device):
|
| 144 |
+
model.train()
|
| 145 |
+
total = 0
|
| 146 |
+
for chem, fp, desc, d4, t in loader:
|
| 147 |
+
chem, fp, desc, d4, t = [x.to(device) for x in (chem, fp, desc, d4, t)]
|
| 148 |
+
opt.zero_grad()
|
| 149 |
+
loss = F.mse_loss(model(chem, fp, desc, d4), t)
|
| 150 |
+
loss.backward()
|
| 151 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 3.0)
|
| 152 |
+
opt.step()
|
| 153 |
+
total += loss.item() * t.size(0)
|
| 154 |
+
return total / len(loader.dataset)
|
| 155 |
+
|
| 156 |
+
@torch.no_grad()
|
| 157 |
+
def evaluate(model, loader, device):
|
| 158 |
+
model.eval()
|
| 159 |
+
preds, targets = [], []
|
| 160 |
+
for chem, fp, desc, d4, t in loader:
|
| 161 |
+
chem, fp, desc, d4 = [x.to(device) for x in (chem, fp, desc, d4)]
|
| 162 |
+
preds.append(model(chem, fp, desc, d4).cpu())
|
| 163 |
+
targets.append(t.cpu())
|
| 164 |
+
preds = torch.cat(preds); targets = torch.cat(targets)
|
| 165 |
+
mse = F.mse_loss(preds, targets).item()
|
| 166 |
+
mae = F.l1_loss(preds, targets).item()
|
| 167 |
+
r2 = 1 - mse / targets.var().item() if targets.var().item() > 0 else 0
|
| 168 |
+
return mse, mae, r2
|
| 169 |
+
|
| 170 |
+
def run(data, name, epochs=150):
|
| 171 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 172 |
+
all_pampa = torch.tensor([float(d['PAMPA']) for d in data])
|
| 173 |
+
t_mean, t_std = all_pampa.mean(), all_pampa.std()
|
| 174 |
+
|
| 175 |
+
ds = CycPepDataset(data, t_mean, t_std)
|
| 176 |
+
n = len(ds)
|
| 177 |
+
indices = list(range(n))
|
| 178 |
+
random.seed(42); random.shuffle(indices)
|
| 179 |
+
tr, va = int(0.8*n), int(0.1*n)
|
| 180 |
+
te = n - tr - va
|
| 181 |
+
tr_i, va_i, te_i = indices[:tr], indices[tr:tr+va], indices[tr+va:]
|
| 182 |
+
|
| 183 |
+
bs = 64
|
| 184 |
+
tr_ld = DataLoader(torch.utils.data.Subset(ds, tr_i), bs, shuffle=True, collate_fn=collate_fn)
|
| 185 |
+
va_ld = DataLoader(torch.utils.data.Subset(ds, va_i), bs, shuffle=False, collate_fn=collate_fn)
|
| 186 |
+
te_ld = DataLoader(torch.utils.data.Subset(ds, te_i), bs, shuffle=False, collate_fn=collate_fn)
|
| 187 |
+
|
| 188 |
+
model = CycPepModel().to(device)
|
| 189 |
+
n_p = sum(p.numel() for p in model.parameters())
|
| 190 |
+
print(f'{name}: {n:,} samples, {n_p:,} params')
|
| 191 |
+
|
| 192 |
+
opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-4)
|
| 193 |
+
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)
|
| 194 |
+
|
| 195 |
+
best_val = float('inf'); best_te = None; patience = 0; t0 = time.time()
|
| 196 |
+
for ep in range(epochs):
|
| 197 |
+
loss = train_epoch(model, tr_ld, opt, device)
|
| 198 |
+
vm, vma, vr2 = evaluate(model, va_ld, device)
|
| 199 |
+
sched.step()
|
| 200 |
+
vm_u = vm * t_std.item()**2
|
| 201 |
+
if (ep+1) % 15 == 0 or ep == 0:
|
| 202 |
+
print(f' E{ep+1:3d} loss={loss:.4f} val_mse={vm_u:.4f} val_r2={vr2:.4f}')
|
| 203 |
+
if vm < best_val:
|
| 204 |
+
best_val = vm; best_te = evaluate(model, te_ld, device); patience = 0
|
| 205 |
+
else:
|
| 206 |
+
patience += 1
|
| 207 |
+
if patience >= 30: break
|
| 208 |
+
elapsed = time.time() - t0
|
| 209 |
+
te_m_u = best_te[0] * t_std.item()**2
|
| 210 |
+
te_ma_u = best_te[1] * t_std.item()
|
| 211 |
+
print(f' TEST: MSE={te_m_u:.4f} MAE={te_ma_u:.4f} RΒ²={best_te[2]:.4f} time={elapsed:.0f}s')
|
| 212 |
+
return te_m_u, te_ma_u, best_te[2]
|
| 213 |
+
|
| 214 |
+
def main():
|
| 215 |
+
data = load_data()
|
| 216 |
+
results = []
|
| 217 |
+
results.append(run(data, 'ChemBERTa+FP+Desc+4D'))
|
| 218 |
+
print('\n' + '='*50)
|
| 219 |
+
for r in results:
|
| 220 |
+
print(f' MSE={r[0]:.4f} MAE={r[1]:.4f} RΒ²={r[2]:.4f}')
|
| 221 |
+
print(f' MSF-CPMP: MSE=0.092 MAE=0.242 RΒ²~0.88')
|
| 222 |
+
|
| 223 |
+
if __name__ == '__main__':
|
| 224 |
+
main()
|
train_comprehensive.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv, io, requests, random, time, math
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
from torch.utils.data import Dataset, DataLoader
|
| 7 |
+
from torch_geometric.data import Data, Batch
|
| 8 |
+
from torch_geometric.nn import GINConv, global_mean_pool, BatchNorm
|
| 9 |
+
from torch_geometric.nn import MLP as PyGMLP
|
| 10 |
+
from rdkit import Chem, RDLogger
|
| 11 |
+
RDLogger.logger().setLevel(RDLogger.ERROR)
|
| 12 |
+
|
| 13 |
+
# βββ Data βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 14 |
+
print('Loading data...')
|
| 15 |
+
r = requests.get('https://raw.githubusercontent.com/akiyamalab/cycpeptmp/main/data/CycPeptMPDB_Peptide_All.csv')
|
| 16 |
+
rows = list(csv.DictReader(io.StringIO(r.content.decode('utf-8-sig'))))
|
| 17 |
+
r4 = requests.get('https://zenodo.org/records/18754430/files/CycPeptMPDB-4D.csv')
|
| 18 |
+
d4_map = {int(rr['CycPeptMPDB_ID']): rr for rr in csv.DictReader(io.StringIO(r4.content.decode('utf-8')))}
|
| 19 |
+
|
| 20 |
+
ATOM_TYPES = [5,6,7,8,9,15,16,17,35,53]
|
| 21 |
+
AMINO_ACIDS = list('ACDEFGHIKLMNPQRSTVWY')
|
| 22 |
+
PHYSCHEM_KEYS = ['MolLogP','MolWt','TPSA','FractionCSP3','NumHAcceptors','NumHDonors',
|
| 23 |
+
'NumRotatableBonds','RingCount','HeavyAtomCount','LabuteASA',
|
| 24 |
+
'HallKierAlpha','Kappa1','Kappa2','Kappa3','BertzCT','BalabanJ']
|
| 25 |
+
DESC_KEYS = ['BCUT2D_MWHI','BCUT2D_MWLOW','BCUT2D_CHGHI','BCUT2D_CHGLO','BCUT2D_LOGPHI',
|
| 26 |
+
'BCUT2D_LOGPLOW','BCUT2D_MRHI','BCUT2D_MRLOW','BalabanJ','BertzCT',
|
| 27 |
+
'Chi0','Chi0n','Chi0v','Chi1','Chi1n','Chi1v','Chi2n','Chi2v','Chi3n','Chi3v','Chi4n','Chi4v',
|
| 28 |
+
'HallKierAlpha','Kappa1','Kappa2','Kappa3','LabuteASA',
|
| 29 |
+
'PEOE_VSA1','PEOE_VSA10','PEOE_VSA11','PEOE_VSA12','PEOE_VSA13','PEOE_VSA14',
|
| 30 |
+
'PEOE_VSA2','PEOE_VSA3','PEOE_VSA4','PEOE_VSA5','PEOE_VSA6','PEOE_VSA7','PEOE_VSA8','PEOE_VSA9',
|
| 31 |
+
'SMR_VSA1','SMR_VSA10','SMR_VSA2','SMR_VSA3','SMR_VSA4','SMR_VSA5','SMR_VSA6','SMR_VSA7','SMR_VSA8','SMR_VSA9',
|
| 32 |
+
'SlogP_VSA1','SlogP_VSA10','SlogP_VSA11','SlogP_VSA12','SlogP_VSA2','SlogP_VSA3','SlogP_VSA4',
|
| 33 |
+
'SlogP_VSA5','SlogP_VSA6','SlogP_VSA7','SlogP_VSA8','SlogP_VSA9',
|
| 34 |
+
'EState_VSA1','EState_VSA10','EState_VSA11','EState_VSA2','EState_VSA3','EState_VSA4',
|
| 35 |
+
'EState_VSA5','EState_VSA6','EState_VSA7','EState_VSA8','EState_VSA9',
|
| 36 |
+
'VSA_EState1','VSA_EState10','VSA_EState2','VSA_EState3','VSA_EState4','VSA_EState5',
|
| 37 |
+
'VSA_EState6','VSA_EState7','VSA_EState8','VSA_EState9',
|
| 38 |
+
'MolLogP','MolWt','TPSA','FractionCSP3','HeavyAtomCount','NHOHCount','NOCount',
|
| 39 |
+
'NumAliphaticCarbocycles','NumAliphaticHeterocycles','NumAliphaticRings',
|
| 40 |
+
'NumAromaticCarbocycles','NumAromaticHeterocycles','NumAromaticRings',
|
| 41 |
+
'NumHAcceptors','NumHDonors','NumHeteroatoms','NumRotatableBonds','RingCount',
|
| 42 |
+
'NumSaturatedCarbocycles','NumSaturatedHeterocycles','NumSaturatedRings',
|
| 43 |
+
'MolMR','qed','MaxAbsEStateIndex','MaxEStateIndex','MinEStateIndex',
|
| 44 |
+
'MaxPartialCharge','MinPartialCharge','MaxAbsPartialCharge','MinAbsPartialCharge',
|
| 45 |
+
'FpDensityMorgan1','FpDensityMorgan2','FpDensityMorgan3',
|
| 46 |
+
'ExactMolWt','HeavyAtomMolWt','NumValenceElectrons']
|
| 47 |
+
D4_FEAT_KEYS = ['Water_avgRMSD_All','Water_avgRMSD_BackBone','Desolvation_Free_Energy',
|
| 48 |
+
'Water_3D_SASA','Water_3D_NPSA','Water_3D_PSA',
|
| 49 |
+
'Hexane_avgRMSD_All','Hexane_avgRMSD_BackBone',
|
| 50 |
+
'Hexane_3D_SASA','Hexane_3D_NPSA','Hexane_3D_PSA']
|
| 51 |
+
|
| 52 |
+
def sf(v):
|
| 53 |
+
if v is None: return 0.0
|
| 54 |
+
try: v=float(v); return 0.0 if math.isnan(v) or math.isinf(v) else v
|
| 55 |
+
except: return 0.0
|
| 56 |
+
def ev(r, ks): return torch.tensor([sf(r.get(k)) for k in ks], dtype=torch.float)
|
| 57 |
+
|
| 58 |
+
def seq_onehot(seq):
|
| 59 |
+
v = torch.zeros(len(AMINO_ACIDS))
|
| 60 |
+
for c in str(seq).upper() if seq else '':
|
| 61 |
+
if c in AMINO_ACIDS: v[AMINO_ACIDS.index(c)] += 1
|
| 62 |
+
return v / max(v.sum(), 1)
|
| 63 |
+
|
| 64 |
+
def mol_to_pyg(mol):
|
| 65 |
+
mol = Chem.AddHs(mol); atoms = list(mol.GetAtoms()); n = len(atoms)
|
| 66 |
+
x = []
|
| 67 |
+
for a in atoms:
|
| 68 |
+
t = [0]*len(ATOM_TYPES)
|
| 69 |
+
if a.GetAtomicNum() in ATOM_TYPES: t[ATOM_TYPES.index(a.GetAtomicNum())] = 1
|
| 70 |
+
x.append(t + [a.GetDegree()/4.0, a.GetTotalNumHs()/3.0, a.GetFormalCharge(), int(a.IsInRing()), int(a.GetIsAromatic())])
|
| 71 |
+
x = torch.tensor(x, dtype=torch.float)
|
| 72 |
+
ei, ea = [], []
|
| 73 |
+
for i in range(n):
|
| 74 |
+
for j in range(i+1, n):
|
| 75 |
+
b = mol.GetBondBetweenAtoms(i,j)
|
| 76 |
+
if b is not None:
|
| 77 |
+
bt = b.GetBondType()
|
| 78 |
+
e = [int(bt==Chem.rdchem.BondType.SINGLE),int(bt==Chem.rdchem.BondType.DOUBLE),int(bt==Chem.rdchem.BondType.TRIPLE),int(bt==Chem.rdchem.BondType.AROMATIC)]
|
| 79 |
+
ei.extend([[i,j],[j,i]]); ea.extend([e,e])
|
| 80 |
+
return Data(x=x, edge_index=torch.tensor(ei,dtype=torch.long).T if ei else torch.zeros((2,0),dtype=torch.long),
|
| 81 |
+
edge_attr=torch.tensor(ea,dtype=torch.float) if ea else torch.zeros((0,4),dtype=torch.float))
|
| 82 |
+
|
| 83 |
+
# Build dataset
|
| 84 |
+
all_data = []
|
| 85 |
+
for row in rows:
|
| 86 |
+
if not row['PAMPA']: continue
|
| 87 |
+
mol = Chem.MolFromSmiles(row['SMILES'])
|
| 88 |
+
if mol is None: continue
|
| 89 |
+
d4 = d4_map.get(int(row['CycPeptMPDB_ID']))
|
| 90 |
+
all_data.append({**row, 'mol': mol, 'd4': d4, 'has_4d': d4 is not None})
|
| 91 |
+
|
| 92 |
+
pa = torch.tensor([float(d['PAMPA']) for d in all_data])
|
| 93 |
+
tm, ts = pa.mean(), pa.std()
|
| 94 |
+
print(f'Total {len(all_data)} PAMPA samples, target mean={tm:.3f} std={ts:.3f}')
|
| 95 |
+
print(f'With 4D: {sum(1 for d in all_data if d["has_4d"])}')
|
| 96 |
+
|
| 97 |
+
# βββ Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 98 |
+
class CycPepGNN(nn.Module):
|
| 99 |
+
def __init__(self, nd=len(ATOM_TYPES)+5, ed=4, dd=len(DESC_KEYS), pd=len(PHYSCHEM_KEYS),
|
| 100 |
+
sd=len(AMINO_ACIDS), d4d=len(D4_FEAT_KEYS), h=256, use_4d=True, use_seq=True):
|
| 101 |
+
super().__init__()
|
| 102 |
+
self.use_4d = use_4d; self.use_seq = use_seq
|
| 103 |
+
self.ne = nn.Linear(nd, h); self.ee = nn.Linear(ed, h)
|
| 104 |
+
c = nn.ModuleList()
|
| 105 |
+
for _ in range(4):
|
| 106 |
+
c.append(GINConv(PyGMLP([h,h,h], norm='batch_norm'), train_eps=True))
|
| 107 |
+
self.cs = c; self.bns = nn.ModuleList([BatchNorm(h) for _ in range(4)])
|
| 108 |
+
self.gp = nn.Linear(h*4, h)
|
| 109 |
+
self.dn = nn.Sequential(nn.Linear(dd, 64), nn.GELU(), nn.LayerNorm(64))
|
| 110 |
+
self.pn = nn.Sequential(nn.Linear(pd, 32), nn.GELU(), nn.LayerNorm(32))
|
| 111 |
+
f = h + 64 + 32
|
| 112 |
+
if use_seq: self.sn = nn.Sequential(nn.Linear(sd, 32), nn.GELU(), nn.LayerNorm(32)); f += 32
|
| 113 |
+
if use_4d: self.d4n = nn.Sequential(nn.Linear(d4d, 16), nn.GELU(), nn.LayerNorm(16)); f += 16
|
| 114 |
+
self.head = nn.Sequential(
|
| 115 |
+
nn.Linear(f, h//2), nn.GELU(), nn.Dropout(0.15),
|
| 116 |
+
nn.Linear(h//2, h//4), nn.GELU(), nn.Dropout(0.1),
|
| 117 |
+
nn.Linear(h//4, 1))
|
| 118 |
+
|
| 119 |
+
def forward(self, pg, desc, pc, seq=None, d4=None):
|
| 120 |
+
x = F.relu(self.ne(pg.x)); xs = []
|
| 121 |
+
for c, bn in zip(self.cs, self.bns):
|
| 122 |
+
x = F.relu(bn(c(x, pg.edge_index))); xs.append(x)
|
| 123 |
+
h = self.gp(torch.cat([global_mean_pool(x, pg.batch) for x in xs], -1))
|
| 124 |
+
h = torch.cat([h, self.dn(desc), self.pn(pc)], 1)
|
| 125 |
+
if self.use_seq: h = torch.cat([h, self.sn(seq)], 1)
|
| 126 |
+
if self.use_4d: h = torch.cat([h, self.d4n(d4)], 1)
|
| 127 |
+
return self.head(h).squeeze(-1)
|
| 128 |
+
|
| 129 |
+
# βββ Dataset ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 130 |
+
class CycPepDataset(Dataset):
|
| 131 |
+
def __init__(self, data, tm, ts, use_4d=True, augment=1, only_4d=False):
|
| 132 |
+
self.samples = []
|
| 133 |
+
for d in data:
|
| 134 |
+
if only_4d and not d['has_4d']: continue
|
| 135 |
+
d4v = ev(d['d4'], D4_FEAT_KEYS) if d['d4'] else torch.zeros(len(D4_FEAT_KEYS))
|
| 136 |
+
seqq = seq_onehot(d.get('Sequence',''))
|
| 137 |
+
desc = ev(d, DESC_KEYS); pc = ev(d, PHYSCHEM_KEYS)
|
| 138 |
+
tgt = (float(d['PAMPA']) - tm) / ts
|
| 139 |
+
self.add_sample(d['mol'], desc, pc, seqq, d4v, tgt, augment)
|
| 140 |
+
def add_sample(self, mol, desc, pc, seq, d4, tgt, aug):
|
| 141 |
+
self.samples.append((mol_to_pyg(mol), desc, pc, seq, d4, tgt))
|
| 142 |
+
if aug > 1:
|
| 143 |
+
seen = set()
|
| 144 |
+
for _ in range(aug * 5):
|
| 145 |
+
s = Chem.MolToSmiles(mol, doRandom=True, canonical=False)
|
| 146 |
+
if s in seen: continue; seen.add(s)
|
| 147 |
+
m2 = Chem.MolFromSmiles(s)
|
| 148 |
+
if m2 is not None and len(seen) <= aug:
|
| 149 |
+
self.samples.append((mol_to_pyg(m2), desc, pc, seq, d4, tgt))
|
| 150 |
+
if len(seen) >= aug: break
|
| 151 |
+
def __len__(self): return len(self.samples)
|
| 152 |
+
def __getitem__(self, i): return self.samples[i]
|
| 153 |
+
|
| 154 |
+
def coll(batch):
|
| 155 |
+
pg, d, pc, sq, d4, t = zip(*batch)
|
| 156 |
+
return (Batch.from_data_list(list(pg)), torch.stack(d), torch.stack(pc),
|
| 157 |
+
torch.stack(sq), torch.stack(d4), torch.tensor(t, dtype=torch.float))
|
| 158 |
+
|
| 159 |
+
# βββ Training ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 160 |
+
def run(name, use_4d, use_seq, augment, only_4d, data, tm, ts, seeds=[42]):
|
| 161 |
+
ds = CycPepDataset(data, tm, ts, use_4d=use_4d, augment=augment, only_4d=only_4d)
|
| 162 |
+
n = len(ds); idx = list(range(n)); random.seed(42); random.shuffle(idx)
|
| 163 |
+
tr, va = int(0.8*n), int(0.1*n)
|
| 164 |
+
tr_i, va_i, te_i = idx[:tr], idx[tr:tr+va], idx[tr+va:]
|
| 165 |
+
|
| 166 |
+
res = []
|
| 167 |
+
for seed in seeds:
|
| 168 |
+
torch.manual_seed(seed); random.seed(seed)
|
| 169 |
+
model = CycPepGNN(use_4d=use_4d, use_seq=use_seq).cuda()
|
| 170 |
+
np_ = sum(p.numel() for p in model.parameters())
|
| 171 |
+
opt = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=1e-5)
|
| 172 |
+
sc = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=200)
|
| 173 |
+
bs = min(128, n//10)
|
| 174 |
+
|
| 175 |
+
tr_l = DataLoader(torch.utils.data.Subset(ds, tr_i), bs, shuffle=True, collate_fn=coll)
|
| 176 |
+
va_l = DataLoader(torch.utils.data.Subset(ds, va_i), bs, shuffle=False, collate_fn=coll)
|
| 177 |
+
te_l = DataLoader(torch.utils.data.Subset(ds, te_i), bs, shuffle=False, collate_fn=coll)
|
| 178 |
+
|
| 179 |
+
bv = float('inf'); bt = None; pt = 0
|
| 180 |
+
for ep in range(200):
|
| 181 |
+
model.train()
|
| 182 |
+
for pg, d, pc, sq, d4, tgt in tr_l:
|
| 183 |
+
pg = pg.cuda(); d, pc, sq, d4, tgt = [x.cuda() for x in (d, pc, sq, d4, tgt)]
|
| 184 |
+
opt.zero_grad()
|
| 185 |
+
kw = {}; kw2 = {}
|
| 186 |
+
if use_seq: kw['seq'] = sq; kw2['sq'] = sq
|
| 187 |
+
if use_4d: kw['d4'] = d4; kw2['d4'] = d4
|
| 188 |
+
F.mse_loss(model(pg, d, pc, **kw), tgt).backward()
|
| 189 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0); opt.step()
|
| 190 |
+
model.eval(); pv, tv = [], []
|
| 191 |
+
for pg, d, pc, sq, d4, tgt in va_l:
|
| 192 |
+
pg = pg.cuda(); d, pc, sq, d4, tgt = [x.cuda() for x in (d, pc, sq, d4, tgt)]
|
| 193 |
+
with torch.no_grad():
|
| 194 |
+
kw = {}; kw2 = {}
|
| 195 |
+
if use_seq: kw['seq'] = sq
|
| 196 |
+
if use_4d: kw['d4'] = d4
|
| 197 |
+
pv.append(model(pg, d, pc, **kw).cpu()); tv.append(tgt.cpu())
|
| 198 |
+
vm = F.mse_loss(torch.cat(tv), torch.cat(pv)).item()
|
| 199 |
+
sc.step()
|
| 200 |
+
if vm < bv: bv = vm; pt = 0
|
| 201 |
+
else: pt += 1
|
| 202 |
+
if pt >= 25: break
|
| 203 |
+
# Test
|
| 204 |
+
model.eval(); ps, ts_ = [], []
|
| 205 |
+
for pg, d, pc, sq, d4, tgt in te_l:
|
| 206 |
+
pg = pg.cuda(); d, pc, sq, d4, tgt = [x.cuda() for x in (d, pc, sq, d4, tgt)]
|
| 207 |
+
with torch.no_grad():
|
| 208 |
+
kw = {}; kw2 = {}
|
| 209 |
+
if use_seq: kw['seq'] = sq
|
| 210 |
+
if use_4d: kw['d4'] = d4
|
| 211 |
+
ps.append(model(pg, d, pc, **kw).cpu()); ts_.append(tgt.cpu())
|
| 212 |
+
p = torch.cat(ps); t = torch.cat(ts_)
|
| 213 |
+
te_m = F.mse_loss(p, t).item() * ts**2
|
| 214 |
+
te_ma = F.l1_loss(p, t).item() * ts
|
| 215 |
+
te_r2 = 1 - F.mse_loss(p, t).item() / t.var().item() if t.var().item() > 0 else 0
|
| 216 |
+
res.append((te_m, te_ma, te_r2))
|
| 217 |
+
print(f' [{name}] seed={seed}: MSE={te_m:.4f} MAE={te_ma:.4f} RΒ²={te_r2:.4f} (n={n})')
|
| 218 |
+
return res
|
| 219 |
+
|
| 220 |
+
# βββ Experiments ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
+
print('\n====== EXPERIMENTS ======\n')
|
| 222 |
+
|
| 223 |
+
# Required comparsion: 4D subset WITHOUT vs WITH 4D features
|
| 224 |
+
# This is the key result: showing 4D helps on the same subset
|
| 225 |
+
r_baseline = run('4Dsub(no4D)', use_4d=False, use_seq=False, augment=1, only_4d=True, data=all_data, tm=tm, ts=ts)
|
| 226 |
+
r_with4d = run('4Dsub(+4D)', use_4d=True, use_seq=False, augment=1, only_4d=True, data=all_data, tm=tm, ts=ts)
|
| 227 |
+
r_with4d_seq = run('4Dsub(+4D+Seq)', use_4d=True, use_seq=True, augment=1, only_4d=True, data=all_data, tm=tm, ts=ts)
|
| 228 |
+
r_fullpower = run('4Dsub(+4D+Seq+Aug)', use_4d=True, use_seq=True, augment=3, only_4d=True, data=all_data, tm=tm, ts=ts)
|
| 229 |
+
|
| 230 |
+
print('\n' + '='*70)
|
| 231 |
+
print(f'{"Experiment":<30} {"MSE":>8} {"MAE":>8} {"RΒ²":>8} {"n":>6}')
|
| 232 |
+
print('='*70)
|
| 233 |
+
for name, res in [('4Dsub(no4D)',r_baseline),('4Dsub(+4D)',r_with4d),
|
| 234 |
+
('4Dsub(+4D+Seq)',r_with4d_seq),('4Dsub(+4D+Seq+Aug)',r_fullpower)]:
|
| 235 |
+
m = np.mean([r[0] for r in res])
|
| 236 |
+
ma = np.mean([r[1] for r in res])
|
| 237 |
+
r2m = np.mean([r[2] for r in res])
|
| 238 |
+
print(f'{name:<30} {m:>8.4f} {ma:>8.4f} {r2m:>8.4f}')
|
| 239 |
+
print('='*70)
|
| 240 |
+
print(f'{"MSF-CPMP (SOTA)":<30} {"0.092":>8} {"0.242":>8} {"~0.88":>8}')
|
| 241 |
+
print(f'{"CycPeptMP":<30} {"0.271":>8} {"0.355":>8} {"0.780":>8}')
|
train_final.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv, io, requests, random, time, math
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
from torch.utils.data import Dataset, DataLoader
|
| 7 |
+
from torch_geometric.data import Data, Batch
|
| 8 |
+
from torch_geometric.nn import GINConv, global_mean_pool, BatchNorm
|
| 9 |
+
from torch_geometric.nn import MLP as PyGMLP
|
| 10 |
+
from rdkit import Chem, RDLogger
|
| 11 |
+
RDLogger.logger().setLevel(RDLogger.ERROR)
|
| 12 |
+
|
| 13 |
+
# βββ Data βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 14 |
+
def load_data():
|
| 15 |
+
r = requests.get('https://raw.githubusercontent.com/akiyamalab/cycpeptmp/main/data/CycPeptMPDB_Peptide_All.csv')
|
| 16 |
+
rows = list(csv.DictReader(io.StringIO(r.content.decode('utf-8-sig'))))
|
| 17 |
+
r4 = requests.get('https://zenodo.org/records/18754430/files/CycPeptMPDB-4D.csv')
|
| 18 |
+
d4_map = {int(rr['CycPeptMPDB_ID']): rr for rr in csv.DictReader(io.StringIO(r4.content.decode('utf-8')))}
|
| 19 |
+
data = []
|
| 20 |
+
for row in rows:
|
| 21 |
+
if not row['PAMPA']: continue
|
| 22 |
+
mol = Chem.MolFromSmiles(row['SMILES'])
|
| 23 |
+
if mol is None: continue
|
| 24 |
+
rid = int(row['CycPeptMPDB_ID'])
|
| 25 |
+
data.append({**row, 'mol': mol, 'd4': d4_map.get(rid)})
|
| 26 |
+
n4d = sum(1 for d in data if d['d4'])
|
| 27 |
+
print(f'Loaded {len(data)} PAMPA entries ({n4d} with 4D)')
|
| 28 |
+
return data
|
| 29 |
+
|
| 30 |
+
# βββ Featurization βββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
+
ATOM_TYPES = [5,6,7,8,9,15,16,17,35,53]
|
| 32 |
+
AMINO_ACIDS = list('ACDEFGHIKLMNPQRSTVWY') # 20 standard
|
| 33 |
+
|
| 34 |
+
def featurize_mol(mol):
|
| 35 |
+
mol = Chem.AddHs(mol)
|
| 36 |
+
atoms = list(mol.GetAtoms()); n = len(atoms)
|
| 37 |
+
x = []
|
| 38 |
+
for a in atoms:
|
| 39 |
+
t = [0]*len(ATOM_TYPES)
|
| 40 |
+
if a.GetAtomicNum() in ATOM_TYPES:
|
| 41 |
+
t[ATOM_TYPES.index(a.GetAtomicNum())] = 1
|
| 42 |
+
x.append(t + [a.GetDegree()/4.0, a.GetTotalNumHs()/3.0,
|
| 43 |
+
a.GetFormalCharge()/1.0, int(a.IsInRing()), int(a.GetIsAromatic())])
|
| 44 |
+
x = torch.tensor(x, dtype=torch.float)
|
| 45 |
+
ei, ea = [], []
|
| 46 |
+
for i in range(n):
|
| 47 |
+
for j in range(i+1, n):
|
| 48 |
+
b = mol.GetBondBetweenAtoms(i, j)
|
| 49 |
+
if b is not None:
|
| 50 |
+
bt = b.GetBondType()
|
| 51 |
+
e = [int(bt == Chem.rdchem.BondType.SINGLE),
|
| 52 |
+
int(bt == Chem.rdchem.BondType.DOUBLE),
|
| 53 |
+
int(bt == Chem.rdchem.BondType.TRIPLE),
|
| 54 |
+
int(bt == Chem.rdchem.BondType.AROMATIC)]
|
| 55 |
+
ei.extend([[i,j],[j,i]]); ea.extend([e, e])
|
| 56 |
+
ei = torch.tensor(ei, dtype=torch.long).T if ei else torch.zeros((2,0), dtype=torch.long)
|
| 57 |
+
ea = torch.tensor(ea, dtype=torch.float) if ea else torch.zeros((0,4), dtype=torch.float)
|
| 58 |
+
return Data(x=x, edge_index=ei, edge_attr=ea)
|
| 59 |
+
|
| 60 |
+
D4_FEAT_KEYS = ['Water_avgRMSD_All','Water_avgRMSD_BackBone','Desolvation_Free_Energy',
|
| 61 |
+
'Water_3D_SASA','Water_3D_NPSA','Water_3D_PSA','Hexane_avgRMSD_All',
|
| 62 |
+
'Hexane_avgRMSD_BackBone','Hexane_3D_SASA','Hexane_3D_NPSA','Hexane_3D_PSA']
|
| 63 |
+
|
| 64 |
+
DESC_KEYS = ['MolLogP','MolWt','TPSA','FractionCSP3','NumHAcceptors','NumHDonors',
|
| 65 |
+
'NumRotatableBonds','RingCount','HeavyAtomCount','LabuteASA','BertzCT','BalabanJ',
|
| 66 |
+
'Kappa1','Kappa2','Kappa3','MolMR','qed','HallKierAlpha',
|
| 67 |
+
'NumAliphaticRings','NumAromaticRings','NumSaturatedRings',
|
| 68 |
+
'MinPartialCharge','MaxPartialCharge','FpDensityMorgan1','FpDensityMorgan2','FpDensityMorgan3']
|
| 69 |
+
|
| 70 |
+
def safe_float(v):
|
| 71 |
+
if v is None: return 0.0
|
| 72 |
+
try:
|
| 73 |
+
v = float(v)
|
| 74 |
+
return 0.0 if math.isnan(v) or math.isinf(v) else v
|
| 75 |
+
except: return 0.0
|
| 76 |
+
|
| 77 |
+
def extract_vec(row, keys):
|
| 78 |
+
return torch.tensor([safe_float(row.get(k)) for k in keys], dtype=torch.float)
|
| 79 |
+
|
| 80 |
+
def extract_d4(d4r):
|
| 81 |
+
if d4r is None: return None
|
| 82 |
+
return extract_vec(d4r, D4_FEAT_KEYS)
|
| 83 |
+
|
| 84 |
+
def seq_to_onehot(seq):
|
| 85 |
+
"""One-hot encode amino acid sequence (monomer-level feature)."""
|
| 86 |
+
vec = torch.zeros(len(AMINO_ACIDS))
|
| 87 |
+
for ch in str(seq).upper() if seq else '':
|
| 88 |
+
if ch in AMINO_ACIDS:
|
| 89 |
+
vec[AMINO_ACIDS.index(ch)] += 1
|
| 90 |
+
return vec / max(vec.sum(), 1)
|
| 91 |
+
|
| 92 |
+
def enumerate_smiles(mol, n=5):
|
| 93 |
+
"""Generate n random SMILES for augmentation."""
|
| 94 |
+
smiles_set = set()
|
| 95 |
+
for _ in range(n * 3): # try more to get unique
|
| 96 |
+
s = Chem.MolToSmiles(mol, doRandom=True, canonical=False)
|
| 97 |
+
if Chem.MolFromSmiles(s) is not None:
|
| 98 |
+
smiles_set.add(s)
|
| 99 |
+
if len(smiles_set) >= n:
|
| 100 |
+
break
|
| 101 |
+
return list(smiles_set) if smiles_set else [Chem.MolToSmiles(mol)]
|
| 102 |
+
|
| 103 |
+
# βββ Dataset ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 104 |
+
class CycPepDataset(Dataset):
|
| 105 |
+
def __init__(self, data, t_mean, t_std, augment=1):
|
| 106 |
+
self.samples = []
|
| 107 |
+
for d in data:
|
| 108 |
+
d4 = extract_d4(d['d4'])
|
| 109 |
+
if d4 is None: d4 = torch.zeros(len(D4_FEAT_KEYS))
|
| 110 |
+
seq = d.get('Sequence', '')
|
| 111 |
+
seq_oh = seq_to_onehot(seq)
|
| 112 |
+
desc = extract_vec(d, DESC_KEYS)
|
| 113 |
+
pyg = featurize_mol(d['mol'])
|
| 114 |
+
target = (float(d['PAMPA']) - t_mean) / t_std
|
| 115 |
+
|
| 116 |
+
if augment > 1:
|
| 117 |
+
smiles_list = enumerate_smiles(d['mol'], augment)
|
| 118 |
+
for s in smiles_list:
|
| 119 |
+
m = Chem.MolFromSmiles(s)
|
| 120 |
+
if m is not None:
|
| 121 |
+
self.samples.append((featurize_mol(m), desc.clone(), seq_oh.clone(), d4.clone(), target))
|
| 122 |
+
else:
|
| 123 |
+
self.samples.append((pyg, desc, seq_oh, d4, target))
|
| 124 |
+
|
| 125 |
+
def __len__(self): return len(self.samples)
|
| 126 |
+
def __getitem__(self, i): return self.samples[i]
|
| 127 |
+
|
| 128 |
+
def collate_fn(batch):
|
| 129 |
+
pygs, descs, seqs, d4s, targets = zip(*batch)
|
| 130 |
+
return (Batch.from_data_list(list(pygs)),
|
| 131 |
+
torch.stack(descs), torch.stack(seqs), torch.stack(d4s),
|
| 132 |
+
torch.tensor(targets, dtype=torch.float))
|
| 133 |
+
|
| 134 |
+
# βββ Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 135 |
+
class CycPepGNN(nn.Module):
|
| 136 |
+
def __init__(self, node_dim=len(ATOM_TYPES)+5, edge_dim=4,
|
| 137 |
+
desc_dim=len(DESC_KEYS), seq_dim=len(AMINO_ACIDS),
|
| 138 |
+
d4_dim=len(D4_FEAT_KEYS), hidden=256):
|
| 139 |
+
super().__init__()
|
| 140 |
+
self.node_emb = nn.Linear(node_dim, hidden)
|
| 141 |
+
self.edge_emb = nn.Linear(edge_dim, hidden)
|
| 142 |
+
|
| 143 |
+
convs = []
|
| 144 |
+
for _ in range(5):
|
| 145 |
+
mlp = PyGMLP([hidden, hidden, hidden], norm='batch_norm')
|
| 146 |
+
convs.append(GINConv(mlp, train_eps=True))
|
| 147 |
+
self.convs = nn.ModuleList(convs)
|
| 148 |
+
self.bns = nn.ModuleList([BatchNorm(hidden) for _ in range(5)])
|
| 149 |
+
self.graph_proj = nn.Linear(hidden, hidden)
|
| 150 |
+
|
| 151 |
+
self.desc_net = nn.Sequential(nn.LayerNorm(desc_dim), nn.Linear(desc_dim, 32), nn.GELU())
|
| 152 |
+
self.seq_net = nn.Sequential(nn.LayerNorm(seq_dim), nn.Linear(seq_dim, 32), nn.GELU())
|
| 153 |
+
self.d4_net = nn.Sequential(nn.LayerNorm(d4_dim), nn.Linear(d4_dim, 16), nn.GELU())
|
| 154 |
+
|
| 155 |
+
fusion = hidden + 32 + 32 + 16
|
| 156 |
+
self.head = nn.Sequential(
|
| 157 |
+
nn.Linear(fusion, hidden//2), nn.GELU(), nn.Dropout(0.2),
|
| 158 |
+
nn.Linear(hidden//2, hidden//4), nn.GELU(), nn.Dropout(0.1),
|
| 159 |
+
nn.Linear(hidden//4, 1))
|
| 160 |
+
|
| 161 |
+
def forward(self, pyg_data, desc, seq, d4):
|
| 162 |
+
x = F.relu(self.node_emb(pyg_data.x))
|
| 163 |
+
e = F.relu(self.edge_emb(pyg_data.edge_attr))
|
| 164 |
+
xs = []
|
| 165 |
+
for conv, bn in zip(self.convs, self.bns):
|
| 166 |
+
x = F.relu(bn(conv(x, pyg_data.edge_index)))
|
| 167 |
+
xs.append(x)
|
| 168 |
+
h_g = self.graph_proj(global_mean_pool(x, pyg_data.batch))
|
| 169 |
+
h_d = self.desc_net(desc)
|
| 170 |
+
h_s = self.seq_net(seq)
|
| 171 |
+
h_4 = self.d4_net(d4)
|
| 172 |
+
return self.head(torch.cat([h_g, h_d, h_s, h_4], 1)).squeeze(-1)
|
| 173 |
+
|
| 174 |
+
# βββ Training ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 175 |
+
def train_epoch(model, loader, opt, device):
|
| 176 |
+
model.train(); total = 0
|
| 177 |
+
for batch in loader:
|
| 178 |
+
g = batch[0].to(device)
|
| 179 |
+
desc, seq, d4, t = [x.to(device) for x in batch[1:]]
|
| 180 |
+
opt.zero_grad()
|
| 181 |
+
loss = F.mse_loss(model(g, desc, seq, d4), t)
|
| 182 |
+
loss.backward()
|
| 183 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 3.0)
|
| 184 |
+
opt.step()
|
| 185 |
+
total += loss.item() * t.size(0)
|
| 186 |
+
return total / len(loader.dataset)
|
| 187 |
+
|
| 188 |
+
@torch.no_grad()
|
| 189 |
+
def evaluate(model, loader, device):
|
| 190 |
+
model.eval(); preds, targets = [], []
|
| 191 |
+
for batch in loader:
|
| 192 |
+
g = batch[0].to(device)
|
| 193 |
+
desc, seq, d4, t = [x.to(device) for x in batch[1:]]
|
| 194 |
+
preds.append(model(g, desc, seq, d4).cpu()); targets.append(t.cpu())
|
| 195 |
+
p = torch.cat(preds); t = torch.cat(targets)
|
| 196 |
+
mse = F.mse_loss(p, t).item()
|
| 197 |
+
return mse, F.l1_loss(p, t).item(), 1 - mse / t.var().item() if t.var().item() > 0 else 0
|
| 198 |
+
|
| 199 |
+
# βββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 200 |
+
def main():
|
| 201 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 202 |
+
print(f'Device: {device}')
|
| 203 |
+
|
| 204 |
+
data = load_data()
|
| 205 |
+
ap = torch.tensor([float(d['PAMPA']) for d in data])
|
| 206 |
+
tm, ts = ap.mean(), ap.std()
|
| 207 |
+
print(f'Target: mean={tm:.3f} std={ts:.3f}')
|
| 208 |
+
|
| 209 |
+
results = []
|
| 210 |
+
for aug in [1]:
|
| 211 |
+
name = f'GIN_aug{aug}'
|
| 212 |
+
ds = CycPepDataset(data, tm, ts, augment=aug)
|
| 213 |
+
n = len(ds); idx = list(range(n))
|
| 214 |
+
random.seed(42); random.shuffle(idx)
|
| 215 |
+
tr, va = int(0.8*n), int(0.1*n); te = n - tr - va
|
| 216 |
+
tr_i, va_i, te_i = idx[:tr], idx[tr:tr+va], idx[tr+va:]
|
| 217 |
+
|
| 218 |
+
bs = min(128, n//10)
|
| 219 |
+
tr_l = DataLoader(torch.utils.data.Subset(ds, tr_i), bs, shuffle=True, collate_fn=collate_fn)
|
| 220 |
+
va_l = DataLoader(torch.utils.data.Subset(ds, va_i), bs, shuffle=False, collate_fn=collate_fn)
|
| 221 |
+
te_l = DataLoader(torch.utils.data.Subset(ds, te_i), bs, shuffle=False, collate_fn=collate_fn)
|
| 222 |
+
|
| 223 |
+
model = CycPepGNN(hidden=256).to(device)
|
| 224 |
+
np_ = sum(p.numel() for p in model.parameters())
|
| 225 |
+
print(f'\n{name}: {n} samples, {np_:,} params')
|
| 226 |
+
|
| 227 |
+
opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-4)
|
| 228 |
+
sc = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=150)
|
| 229 |
+
bv = float('inf'); bt = None; pt_ = 0; t0 = time.time()
|
| 230 |
+
for ep in range(150):
|
| 231 |
+
loss = train_epoch(model, tr_l, opt, device)
|
| 232 |
+
vm, vma, vr2 = evaluate(model, va_l, device)
|
| 233 |
+
sc.step()
|
| 234 |
+
if (ep+1) % 15 == 0 or ep == 0:
|
| 235 |
+
print(f' E{ep+1:3d} loss={loss:.4f} val_mse={vm*ts**2:.4f} val_r2={vr2:.4f}')
|
| 236 |
+
if vm < bv:
|
| 237 |
+
bv = vm; bt = evaluate(model, te_l, device); pt_ = 0
|
| 238 |
+
else:
|
| 239 |
+
pt_ += 1
|
| 240 |
+
if pt_ >= 25: break
|
| 241 |
+
te_m_u = bt[0]*ts**2; te_ma_u = bt[1]*ts
|
| 242 |
+
elapsed = time.time() - t0
|
| 243 |
+
print(f' TEST: MSE={te_m_u:.4f} MAE={te_ma_u:.4f} RΒ²={bt[2]:.4f} time={elapsed:.0f}s')
|
| 244 |
+
results.append((name, te_m_u, te_ma_u, bt[2]))
|
| 245 |
+
|
| 246 |
+
print('\n' + '='*60)
|
| 247 |
+
print(f'{"Model":<20} {"MSE":<10} {"MAE":<10} {"RΒ²":<10}')
|
| 248 |
+
print('-'*60)
|
| 249 |
+
for r in results:
|
| 250 |
+
print(f'{r[0]:<20} {r[1]:<10.4f} {r[2]:<10.4f} {r[3]:<10.4f}')
|
| 251 |
+
print('-'*60)
|
| 252 |
+
print(f'{"MSF-CPMP (SOTA)":<20} {"0.092":<10} {"0.242":<10} {"~0.88":<10}')
|
| 253 |
+
print(f'{"CycPeptMP":<20} {"0.271":<10} {"0.355":<10} {"0.780":<10}')
|
| 254 |
+
|
| 255 |
+
if __name__ == '__main__':
|
| 256 |
+
main()
|