File size: 7,687 Bytes
0646ed9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | {
"cells": [
{
"cell_type": "markdown",
"id": "b2f61e03",
"metadata": {},
"source": [
"# GRASP-A3 Colab quickstart\n",
"\n",
"Public model: [caithmac/GRASP-A3](https://huggingface.co/caithmac/GRASP-A3).\n",
"Use a fresh Python 3.10–3.13 runtime. A GPU is optional for this small example.\n",
"Run the three code cells in order. No Hub token is required.\n",
"\n",
"The loader and model hashes are verified against the audited release, and one Hub\n",
"revision is used throughout. Installing an updated loader in a runtime that already\n",
"imported an older `grasp` requires restarting that runtime before loading A3.\n"
]
},
{
"cell_type": "markdown",
"id": "9b593ed9",
"metadata": {},
"source": [
"## 1. Install the A3-compatible loader"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "062b6490",
"metadata": {},
"outputs": [],
"source": [
"# Fresh Colab runtime recommended; restart if an older grasp was imported.\n",
"import hashlib, json, shutil, subprocess, sys, urllib.request\n",
"from pathlib import Path\n",
"\n",
"assert (3, 10) <= sys.version_info[:2] < (3, 14), \"Use Python 3.10–3.13.\"\n",
"MODEL_ID = \"caithmac/GRASP-A3\"\n",
"with urllib.request.urlopen(\"https://huggingface.co/api/models/\" + MODEL_ID, timeout=60) as response:\n",
" model_info = json.load(response)\n",
"assert model_info[\"id\"] == MODEL_ID and not model_info[\"private\"]\n",
"MODEL_REVISION = model_info[\"sha\"]\n",
"assert len(MODEL_REVISION) == 40 and all(c in \"0123456789abcdef\" for c in MODEL_REVISION)\n",
"WHEEL_FILENAME = \"grasp_molecular-0.1.2-py3-none-any.whl\"\n",
"WHEEL_SHA256 = \"914eaf0b182ad3d70ab57f0b38d6cd7d8e772f11dd0d95bd2e40ee5c1476b6d6\"\n",
"WEIGHTS_SHA256 = \"b3a16758d213b1fd792eca74649d814cb36e1f588a931ba50252911ed38fd3ac\"\n",
"wheel_path = Path.cwd() / WHEEL_FILENAME\n",
"wheel_url = \"https://huggingface.co/\" + MODEL_ID + \"/resolve/\" + MODEL_REVISION + \"/\" + WHEEL_FILENAME\n",
"with urllib.request.urlopen(wheel_url, timeout=60) as response, wheel_path.open(\"wb\") as target:\n",
" shutil.copyfileobj(response, target)\n",
"assert hashlib.sha256(wheel_path.read_bytes()).hexdigest() == WHEEL_SHA256, \"Loader hash mismatch.\"\n",
"subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"--quiet\", str(wheel_path)])\n",
"print(\"Hub revision:\", MODEL_REVISION)\n"
]
},
{
"cell_type": "markdown",
"id": "a9b1d01a",
"metadata": {},
"source": [
"## 2. Verify and load the public encoder"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "53ea6513",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import pandas as pd\n",
"import torch\n",
"from huggingface_hub import hf_hub_download\n",
"from grasp import GRASPEncoder\n",
"\n",
"# Verify the downloaded public weights before loading the same pinned revision.\n",
"weight_path = Path(hf_hub_download(MODEL_ID, \"model.safetensors\", revision=MODEL_REVISION))\n",
"digest = hashlib.sha256()\n",
"with weight_path.open(\"rb\") as stream:\n",
" for block in iter(lambda: stream.read(8 * 1024**2), b\"\"):\n",
" digest.update(block)\n",
"assert digest.hexdigest() == WEIGHTS_SHA256, \"Weight hash mismatch.\"\n",
"encoder = GRASPEncoder.from_pretrained(MODEL_ID, revision=MODEL_REVISION)\n",
"assert encoder.config[\"architecture\"][\"factorized_atom_features\"]\n",
"assert sum(p.numel() for p in encoder.model.parameters()) == 93_089_496\n",
"\n",
"smiles = [\"CCO\", \"c1ccccc1\", \"N[C@@H](C)C(=O)O\", \"F[C@H](Cl)Br\", \"F[C@@H](Cl)Br\"]\n",
"embeddings = encoder.encode(smiles, batch_size=4)\n",
"assert embeddings.shape == (5, 768) and np.isfinite(embeddings).all()\n",
"print(\"Device:\", encoder.device, \"| Embedding shape:\", embeddings.shape)\n",
"display(pd.DataFrame(embeddings[:, :5], index=smiles, columns=[f\"dim_{i}\" for i in range(5)]))\n",
"np.save(\"a3_example_embeddings.npy\", embeddings)\n"
]
},
{
"cell_type": "markdown",
"id": "7a0b894c",
"metadata": {},
"source": [
"## 3. Encode your own molecules and save the output"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "eec7a254",
"metadata": {},
"outputs": [],
"source": [
"# Replace these with your own valid SMILES. Outputs are embeddings, not potency predictions.\n",
"my_smiles = [\"CC(=O)O\", \"CCN\", \"c1ccncc1\"]\n",
"my_embeddings = encoder.encode(my_smiles, batch_size=4)\n",
"assert my_embeddings.shape == (len(my_smiles), 768) and np.isfinite(my_embeddings).all()\n",
"np.save(\"a3_my_embeddings.npy\", my_embeddings)\n",
"pd.DataFrame({\"smiles\": my_smiles}).to_csv(\"a3_my_smiles.csv\", index=False)\n",
"print(\"Saved a3_my_embeddings.npy and a3_my_smiles.csv\")\n"
]
},
{
"cell_type": "markdown",
"id": "d930d899",
"metadata": {},
"source": [
"## Optional: train a task head or fine-tune on your CSVs\n",
"\n",
"Upload separate `train.csv` and `valid.csv` files with `smiles,target` columns.\n",
"Copy this template into a new code cell only after those files are ready:\n",
"\n",
"```python\n",
"from grasp.train import fit\n",
"from grasp import GRASPPredictor\n",
"\n",
"fit(model_source=str(weight_path.parent), train_csv=\"train.csv\", valid_csv=\"valid.csv\",\n",
" output_dir=\"my_a3_predictor\", task=\"regression\", method=\"frozen\",\n",
" epochs=20, patience=5, batch_size=16)\n",
"predictor = GRASPPredictor.from_pretrained(\"my_a3_predictor\")\n",
"print(predictor.predict([\"CCO\", \"CCN\"]))\n",
"```\n",
"\n",
"`task` is `regression` or `binary` (binary targets must be 0/1).\n",
"`method` is `frozen`, `full` or `lora`. The template is illustrative; its settings\n",
"are not a benchmark or a universal recommended tuning recipe. The predictor learns\n",
"layer mixing, atom pooling and a nonlinear task head. The encoder package does not\n",
"include endpoint-specific prediction heads. Use shared fixed splits for comparisons.\n"
]
},
{
"cell_type": "markdown",
"id": "8b2bb03b",
"metadata": {},
"source": [
"## Interpretation and scope\n",
"\n",
"`encode` returns final-layer CLS vectors. Reported OpenADMET results use full\n",
"fine-tuning with a mixed-layer predictor, not a frozen CLS probe. A3 changes both\n",
"the atom representation and generator/corruption task. It is not a representation-only\n",
"causal experiment. Source overlap, censored labels and assay comparability require\n",
"task-specific handling; the checkpoint's Step 2 supervision is binary.\n",
"\n",
"See the model card for license (CC BY-NC 4.0), provenance, limits and validation.\n",
"The publication smoke test checks hosted files/inference on local CPU; this notebook\n",
"has not been run on a Google-hosted Colab GPU during publication.\n"
]
}
],
"metadata": {
"colab": {
"name": "GRASP_A3_Colab_Quickstart.ipynb",
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
|