diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..0a27fdb --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,90 @@ +name: Deploy Documentation + +# Publishes versioned docs to the gh-pages branch with mike. +# - a GitHub release -> deploy that version and move the `latest` alias +# - a push to main -> deploy the floating `dev` version (unreleased docs) +# - manual dispatch -> deploy an arbitrary version/alias (safe for testing) +# +# GitHub Pages for this repo serves "from a branch" (gh-pages); mike commits and +# pushes the built site there, and Pages rebuilds automatically. This is NOT the +# actions/deploy-pages OIDC flow, so the only permission needed is contents:write. +on: + release: + types: [published] + push: + branches: [main] + paths: + - "docs/**" + - "mkdocs.yml" + - "themap/**" + - "pyproject.toml" + - ".github/workflows/docs.yml" + workflow_dispatch: + inputs: + version: + description: "Version identifier to deploy (e.g. 0.5.0 or test)" + required: true + default: "test" + alias: + description: "Alias to point at this version (leave blank for none)" + required: false + default: "" + +# Serialize deploys: they all mutate the single gh-pages branch, and concurrent +# pushes would race and reject. Do not cancel in-flight deploys (a half-finished +# mike push could leave gh-pages inconsistent) — queue them instead. +concurrency: + group: docs-deploy + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + # mike pushes the built site to the gh-pages branch. + contents: write + steps: + - uses: actions/checkout@v4 + with: + # mike needs full history and the gh-pages branch; a shallow clone + # breaks versioned deploys. + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + cache-dependency-glob: "pyproject.toml" + + - name: Install dependencies + run: uv pip install --system -e ".[docs]" + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Deploy released version + if: github.event_name == 'release' + run: | + VERSION="${GITHUB_REF_NAME#v}" + mike deploy --push --update-aliases "$VERSION" latest + mike set-default --push latest + + - name: Deploy dev docs from main + if: github.event_name == 'push' + run: mike deploy --push dev + + - name: Deploy manual dispatch + if: github.event_name == 'workflow_dispatch' + run: | + if [ -n "${{ inputs.alias }}" ]; then + mike deploy --push --update-aliases "${{ inputs.version }}" "${{ inputs.alias }}" + else + mike deploy --push "${{ inputs.version }}" + fi diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index dfdfba5..0000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# .readthedocs.yaml -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -version: 2 - -build: - os: ubuntu-22.04 - tools: - python: "3.10" - -mkdocs: - configuration: mkdocs.yml - -python: - install: - - method: pip - path: . - extra_requirements: - - docs diff --git a/docs/assets/css/custom-splito.css b/docs/assets/css/themap.css similarity index 84% rename from docs/assets/css/custom-splito.css rename to docs/assets/css/themap.css index b8e43ed..a27b6b9 100644 --- a/docs/assets/css/custom-splito.css +++ b/docs/assets/css/themap.css @@ -14,11 +14,20 @@ } [data-md-color-scheme="slate"] { + /* Lighter purple in dark mode so header text and links keep AA contrast. */ + --md-primary-fg-color: #b39ddb; + --md-primary-fg-color--light: #d1c4e9; + --md-primary-fg-color--dark: #9575cd; + --md-accent-fg-color: #b388ff; --md-code-bg-color: #1e1e2e; --md-default-bg-color: #0d1117; } /* ===== Typography ===== */ +.md-typeset { + line-height: 1.7; +} + .md-typeset h1 { font-weight: 700; letter-spacing: -0.02em; @@ -27,9 +36,9 @@ .md-typeset h2 { font-weight: 600; - margin-top: 2rem; - padding-bottom: 0.5rem; - border-bottom: 2px solid var(--md-primary-fg-color--light); + margin-top: 2.5rem; + padding-bottom: 0.3rem; + border-bottom: 1px solid var(--md-default-fg-color--lightest); } .md-typeset h3 { @@ -67,11 +76,11 @@ /* ===== Navigation ===== */ .md-nav__title { font-weight: 600; - font-size: 0.75rem; + font-size: 0.8rem; } .md-nav__link { - font-size: 0.8rem; + font-size: 0.85rem; } .md-nav__item { @@ -85,7 +94,7 @@ /* Right-side table of contents */ .md-nav--secondary .md-nav__link { - font-size: 0.75rem; + font-size: 0.8rem; } /* Tab navigation styling */ @@ -105,7 +114,7 @@ overflow: hidden; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); border: 1px solid rgba(0, 0, 0, 0.08); - font-size: 0.85rem; + font-size: 0.9rem; } .md-typeset table:not([class]) th { @@ -116,6 +125,12 @@ text-align: left; } +/* In dark mode the primary is a light purple; use a darker header tone so the + white header text keeps contrast. */ +[data-md-color-scheme="slate"] .md-typeset table:not([class]) th { + background: var(--md-primary-fg-color--dark); +} + .md-typeset table:not([class]) td { padding: 0.65rem 1rem; border-bottom: 1px solid rgba(0, 0, 0, 0.05); @@ -165,17 +180,17 @@ /* ===== Footer ===== */ .md-footer { - background: linear-gradient(135deg, var(--themap-gradient-start), var(--themap-gradient-end)); + background-color: var(--md-primary-fg-color--dark); } .md-footer-meta { - background: rgba(0, 0, 0, 0.15); + background: rgba(0, 0, 0, 0.2); } /* ===== Header ===== */ .md-header { - background: linear-gradient(90deg, var(--themap-gradient-start), var(--themap-gradient-end)); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + background-color: var(--md-primary-fg-color); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12); } /* ===== Search ===== */ @@ -196,8 +211,8 @@ } .md-typeset a.md-button--primary { - background: linear-gradient(135deg, var(--themap-gradient-start), var(--themap-gradient-end)); - border: none; + background: var(--md-primary-fg-color); + border-color: var(--md-primary-fg-color); } /* ===== API Documentation ===== */ @@ -222,7 +237,7 @@ /* ===== Content Width ===== */ .md-content__inner { - max-width: 900px; + max-width: 54rem; } /* ===== Version Selector ===== */ diff --git a/docs/assets/images/banner.png b/docs/assets/images/banner.png new file mode 100644 index 0000000..2b1e76c Binary files /dev/null and b/docs/assets/images/banner.png differ diff --git a/docs/index.md b/docs/index.md index 228906a..3754f3b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,14 @@ # THEMAP: Task Hardness Estimation for Molecular Activity Prediction +[![DOI](https://img.shields.io/badge/DOI-10.1021%2Facs.jcim.4c00160-blue)](https://doi.org/10.1021/acs.jcim.4c00160) +[![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![PyPI version](https://badge.fury.io/py/themap.svg)](https://pypi.org/project/themap/) + +

+ THEMAP banner +

+ **THEMAP** is a Python library for computing distances between chemical datasets and estimating task hardness for bioactivity prediction. It helps researchers identify similar tasks for transfer learning and quantify prediction difficulty.
@@ -41,18 +50,23 @@ ## Installation ```bash -# Clone and install (uv-based) -git clone https://github.com/HFooladi/THEMAP.git -cd THEMAP -source install.sh +# From PyPI (recommended) +pip install themap + +# With all optional dependencies (ML, protein, OTDD) +pip install "themap[all]" +``` -# Or install with pip -pip install -e . +To install from source for development: -# With all optional dependencies -pip install -e ".[all]" +```bash +git clone https://github.com/HFooladi/THEMAP.git +cd THEMAP +source install.sh # uv-based; creates .venv and installs dev extras ``` +See [Getting Started](user-guide/getting-started.md) for the optional dependency groups. + ## Quick Examples ### Command Line diff --git a/docs/tutorials/Basics.ipynb b/docs/tutorials/Basics.ipynb deleted file mode 100644 index 5dc8329..0000000 --- a/docs/tutorials/Basics.ipynb +++ /dev/null @@ -1,511 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Importing Required Libraries\n", - "\n", - "First, we are importing the libraries and modules that are required for running this notebook." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# import general packages\n", - "import os\n", - "import sys\n", - "\n", - "import pandas as pd\n", - "import torch\n", - "from dpu_utils.utils.richpath import RichPath\n", - "from tqdm.notebook import tqdm\n", - "\n", - "# Setting up local details:\n", - "# This should be the location of the checkout of the THEMAP repository:\n", - "repo_path = os.path.dirname(os.path.abspath(\"\"))\n", - "CHECKOUT_PATH = repo_path\n", - "DATASET_PATH = os.path.join(repo_path, \"datasets\")\n", - "\n", - "os.chdir(CHECKOUT_PATH)\n", - "sys.path.insert(0, CHECKOUT_PATH)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from third_party.otdd.otdd.pytorch.datasets import MolDataset, load_molecule_data\n", - "from third_party.otdd.otdd.pytorch.distance import DatasetDistance" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# import visualization packages\n", - "%matplotlib inline\n", - "\n", - "import ipywidgets as widgets\n", - "import matplotlib\n", - "import matplotlib.pyplot as plt\n", - "\n", - "from themap.data import MoleculeDataset, ProteinDataset\n", - "\n", - "light_color = plt.get_cmap(\"plasma\").colors[170]\n", - "dark_color = \"black\"\n", - "\n", - "matplotlib.rcParams.update(\n", - " {\n", - " \"pgf.texsystem\": \"pdflatex\",\n", - " \"font.family\": \"serif\",\n", - " \"font.serif\": \"Computer Modern Roman\",\n", - " \"font.size\": 20,\n", - " \"text.usetex\": True,\n", - " \"pgf.rcfonts\": False,\n", - " }\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Create source and target datasets (Data)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "source_dataset_path = RichPath.create(os.path.join(DATASET_PATH, \"train\", \"CHEMBL1023359.jsonl.gz\"))\n", - "target_dataset_path = RichPath.create(os.path.join(DATASET_PATH, \"test\", \"CHEMBL2219358.jsonl.gz\"))\n", - "\n", - "source_dataset = MoleculeDataset.load_from_file(source_dataset_path)\n", - "target_dataset = MoleculeDataset.load_from_file(target_dataset_path)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Molecule_Feaurizer = widgets.Dropdown(\n", - " options=[\"gin_supervised_infomax\", \"gin_supervised_masking\", \"gin_supervised_edgepred\"],\n", - " value=\"gin_supervised_infomax\",\n", - " description=\"Molecule Featurizer:\",\n", - " disabled=False,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Molecule_Feaurizer" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "## compute and load the embeddings\n", - "molecule_feaurizer = Molecule_Feaurizer.value\n", - "source_features = source_dataset.get_features(molecule_feaurizer)\n", - "target_features = target_dataset.get_features(molecule_feaurizer)\n", - "assert source_features.shape[1] == target_features.shape[1]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "source_dataset_otdd = MolDataset(source_dataset)\n", - "target_dataset_otdd = MolDataset(target_dataset)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "source_dataset_loader = load_molecule_data(source_dataset)\n", - "target_dataset_loader = load_molecule_data(target_dataset)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Instantiate distance\n", - "dist = DatasetDistance(\n", - " source_dataset_loader,\n", - " target_dataset_loader,\n", - " inner_ot_method=\"exact\",\n", - " debiased_loss=True,\n", - " p=2,\n", - " entreg=1e-1,\n", - " device=\"cuda\" if torch.cuda.is_available() else \"cpu\",\n", - ")\n", - "\n", - "d = dist.distance(maxsamples=1000)\n", - "print(f\"OTDD(src,tgt)={d}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Calculate chemcial distance between target datasets with all the source datasets" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import glob\n", - "\n", - "source_datasets_path = glob.glob(os.path.join(DATASET_PATH, \"train\", \"CHEMBL*\"))\n", - "target_datasets_path = glob.glob(os.path.join(DATASET_PATH, \"test\", \"CHEMBL*\"))\n", - "chem_distances = {}\n", - "for target_path in tqdm(target_datasets_path):\n", - " chem_distance = {}\n", - " target_dataset_path = RichPath.create(target_path)\n", - " target_dataset = MoleculeDataset.load_from_file(target_dataset_path)\n", - " target_features = target_dataset.get_features(molecule_feaurizer)\n", - " target_dataset_otdd = MolDataset(target_dataset)\n", - " target_dataset_loader = load_molecule_data(target_dataset)\n", - " for source_path in source_datasets_path:\n", - " source_dataset_path = RichPath.create(source_path)\n", - " source_dataset = MoleculeDataset.load_from_file(source_dataset_path)\n", - " source_features = source_dataset.get_features(molecule_feaurizer)\n", - " source_dataset_otdd = MolDataset(source_dataset)\n", - " source_dataset_loader = load_molecule_data(source_dataset)\n", - "\n", - " dist = DatasetDistance(\n", - " source_dataset_loader,\n", - " target_dataset_loader,\n", - " inner_ot_method=\"exact\",\n", - " debiased_loss=True,\n", - " p=2,\n", - " entreg=1e-1,\n", - " device=\"cuda\" if torch.cuda.is_available() else \"cpu\",\n", - " )\n", - "\n", - " d = dist.distance(maxsamples=1000)\n", - " print(f\"OTDD({source_dataset.task_id},{target_dataset.task_id})= {d}\")\n", - " chem_distance[source_dataset.task_id] = d.cpu().item()\n", - " chem_distances[target_dataset.task_id] = chem_distance" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "## Choose your target from chem_distances.keys()\n", - "your_tasks = \"CHEMBL2219236\"\n", - "chem_dist = chem_distances[your_tasks]\n", - "fig = plt.figure(figsize=(12, 5))\n", - "plt.bar(chem_dist.keys(), chem_dist.values())\n", - "plt.xlabel(\"Source datasets\")\n", - "plt.ylabel(\"OTDD\")\n", - "plt.title(f\"OTDD between source datasets and target {your_tasks}\")\n", - "plt.xticks(rotation=90)\n", - "plt.grid(axis=\"y\", linestyle=\"--\", alpha=1.0)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "chem_distances.keys()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Calculate protein distance between target datasets with all the source datasets" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Protein_Feaurizer = widgets.Dropdown(\n", - " options=[\n", - " \"esm2_t6_8M_UR50D\",\n", - " \"esm2_t12_35M_UR50D\",\n", - " \"esm2_t30_150M_UR50D\",\n", - " \"esm2_t33_650M_UR50D\",\n", - " \"esm2_t36_3B_UR50D\",\n", - " ],\n", - " value=\"esm2_t33_650M_UR50D\",\n", - " description=\"Protein Featurizer:\",\n", - " disabled=False,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Protein_Feaurizer" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "source_protein = ProteinDataset.load_from_file(\"datasets/train/train_proteins.fasta\")\n", - "target_protein = ProteinDataset.load_from_file(\"datasets/test/test_proteins.fasta\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "protein_featurizer = Protein_Feaurizer.value\n", - "source_protein_features = source_protein.get_features(protein_featurizer)\n", - "target_protein_features = target_protein.get_features(protein_featurizer)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from scipy.spatial.distance import cdist\n", - "\n", - "dist = cdist(source_protein.features, target_protein.features)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "prot_distances = {}\n", - "for i, target_prot in enumerate(target_protein.task_id):\n", - " prot_distance = {}\n", - " for j, source_prot in enumerate(source_protein.task_id):\n", - " prot_distance[source_prot] = dist[j, i]\n", - " prot_distances[target_prot] = prot_distance" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "## Choose your target from chem_distances.keys()\n", - "your_tasks = \"CHEMBL2219236\"\n", - "prot_dist = prot_distances[your_tasks]\n", - "fig = plt.figure(figsize=(12, 5))\n", - "plt.bar(prot_dist.keys(), prot_dist.values())\n", - "plt.xlabel(\"Source datasets\")\n", - "plt.ylabel(\"Protein Distance\")\n", - "plt.title(f\"Protein Distance between source datasets and target {your_tasks}\")\n", - "plt.xticks(rotation=90)\n", - "plt.grid(axis=\"y\", linestyle=\"--\", alpha=1.0)\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Combine Two Distances" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we can answer to the following questions:\n", - "- Given a target task, what is the closest source task in terms of chemical and protein distances?\n", - "- Given the source tasks, which target task is hardest to transfer to in terms of chemical and protein space?\n", - "- Given a target task and source tasks, how to pick the k nearset source tasks fo transfer learning?\n", - "\n", - "So, let's answer to this questions in the following sections." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "chem_df = pd.DataFrame.from_dict(chem_distances)\n", - "prot_df = pd.DataFrame.from_dict(prot_distances)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "## Given a target task, what is the closest source task in terms of chemical and protein distances?\n", - "your_task = \"CHEMBL2219236\"\n", - "\n", - "chem_distance = chem_df[your_task]\n", - "prot_distance = prot_df[your_task]\n", - "\n", - "normalized_chem_distance = (chem_distance - chem_distance.min()) / (chem_distance.max() - chem_distance.min())\n", - "normalized_prot_distance = (prot_distance - prot_distance.min()) / (prot_distance.max() - prot_distance.min())\n", - "normalized_prot_distance = normalized_prot_distance.reindex(normalized_chem_distance.index)\n", - "normalized_comb_distance = (normalized_chem_distance + normalized_prot_distance) / 2\n", - "\n", - "\n", - "print(f\"Closest source task in terms of chemical distance: {chem_distance.idxmin()}\")\n", - "print(f\"Closest source task in terms of protein distance: {prot_distance.idxmin()}\")\n", - "print(\n", - " f\"Closest source task in terms of combination of chemical and protein distance: {normalized_comb_distance.idxmin()}\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "## Given the source tasks, which target task is hardest to transfer to in terms of chemical and protein space?\n", - "## Answering this question require to define hardness, which we consider here as the average of k-nearest source tasks.\n", - "k = 3\n", - "\n", - "target_tasks = chem_df.columns\n", - "source_tasks = chem_df.index\n", - "\n", - "hardness_all = {}\n", - "for target_task in target_tasks:\n", - " hardness = {}\n", - " chem_distance = chem_df[target_task]\n", - " prot_distance = prot_df[target_task]\n", - "\n", - " chem_distance = chem_distance.sort_values()\n", - " prot_distance = prot_distance.sort_values()\n", - "\n", - " hardness[\"EXT_CHEM\"] = chem_distance[:k].sum() / k\n", - " hardness[\"EXT_PROT\"] = prot_distance[:k].sum() / k\n", - "\n", - " hardness_all[target_task] = hardness\n", - "\n", - "hardness_df = pd.DataFrame.from_dict(hardness_all).T\n", - "hardness_df[\"all\"] = (hardness_df[\"EXT_CHEM\"] + hardness_df[\"EXT_PROT\"]) / 2\n", - "\n", - "print(f\"Easiest target task in terms of chemical distance: {hardness_df['EXT_CHEM'].idxmin()}\")\n", - "print(f\"Easiest target task in terms of protein distance: {hardness_df['EXT_PROT'].idxmin()}\")\n", - "print(\n", - " f\"Easiest target task in terms of combination of chemical and protein distance: {hardness_df['all'].idxmin()}\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.figure(figsize=(12, 5))\n", - "plt.bar(hardness_df.index, hardness_df[\"all\"])\n", - "plt.xlabel(\"Target datasets\")\n", - "plt.ylabel(\"Hardness\")\n", - "plt.title(\"Hardness of target datasets\")\n", - "plt.xticks(rotation=90)\n", - "plt.grid(axis=\"y\", linestyle=\"--\", alpha=1.0)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "## Given a target task and source tasks, how to pick the k nearset source tasks fo transfer learning?\n", - "## Given the source tasks, which target task is hardest to transfer to in terms of chemical and protein space?\n", - "## Answering this question require to define hardness, which we consider here as the average of k-nearest source tasks.\n", - "k = 3\n", - "\n", - "target_tasks = chem_df.columns\n", - "source_tasks = chem_df.index\n", - "\n", - "closest_tasks = {}\n", - "for target_task in target_tasks:\n", - " closest = {}\n", - " chem_distance = chem_df[target_task]\n", - " prot_distance = prot_df[target_task]\n", - "\n", - " chem_distance = chem_distance.sort_values()\n", - " prot_distance = prot_distance.sort_values()\n", - "\n", - " closest[\"EXT_CHEM\"] = chem_distance[:k].index.to_list()\n", - " closest[\"EXT_PROT\"] = prot_distance[:k].index.to_list()\n", - "\n", - " closest_tasks[target_task] = closest\n", - "\n", - "closest_df = pd.DataFrame.from_dict(closest_tasks).T" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "pytorch", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/tutorials/colab-notebooks.md b/docs/tutorials/colab-notebooks.md new file mode 100644 index 0000000..82ac185 --- /dev/null +++ b/docs/tutorials/colab-notebooks.md @@ -0,0 +1,20 @@ +# Run on Colab + +Want to see what THEMAP does before installing anything? Run these notebooks +directly in your browser on Google Colab — no local setup needed. + +| Notebook | What it covers | Runtime | Open | +| --- | --- | --- | --- | +| **5-minute quick tour** | Install THEMAP, download 5 sample ChEMBL datasets, compute a 2×3 distance matrix with `quick_distance`, and visualise it as a heatmap. | CPU | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/HFooladi/THEMAP/blob/main/notebooks/colab/01_quick_tour.ipynb) | +| **API deep dive** | Walk through the building blocks: `DatasetLoader`, `MoleculeFeaturizer`, `DatasetDistance`, YAML pipelines, featurizer/metric comparison, and a PCA task landscape over 13 datasets. | CPU | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/HFooladi/THEMAP/blob/main/notebooks/colab/02_api_deep_dive.ipynb) | +| **OTDD deep dive** | Run OTDD on the same ChEMBL assays with three featurizers (ECFP, `desc2D`, ChemBERTa). Compare OTDD against Euclidean/Cosine, and see why OTDD's Gaussian inner approximation rewards continuous representations and struggles with binary fingerprints. | GPU (T4) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/HFooladi/THEMAP/blob/main/notebooks/colab/03_otdd_deep_dive.ipynb) | + +All three notebooks live in +[`notebooks/colab/`](https://github.com/HFooladi/THEMAP/tree/main/notebooks/colab). +The first two run end-to-end on a free Colab CPU runtime; the OTDD notebook needs +a GPU (a free T4 works) because OTDD's Wasserstein computation is GPU-bound at any +practical scale. + +!!! tip + On Colab, select **Runtime → Change runtime type → T4 GPU** before running the + OTDD notebook. diff --git a/docs/tutorials/working-with-tasks.md b/docs/tutorials/working-with-tasks.md index c1b8e5f..e6b93e7 100644 --- a/docs/tutorials/working-with-tasks.md +++ b/docs/tutorials/working-with-tasks.md @@ -1,15 +1,23 @@ # Working with Tasks -This tutorial covers THEMAP's unified task system that integrates molecular data, protein data, and metadata for comprehensive analysis. +This tutorial covers THEMAP's unified task system that integrates molecular data, +protein data, and other metadata for comprehensive analysis. + +!!! note + The code on this page is written against the current THEMAP API. The examples + are illustrative — running them requires a real dataset directory laid out as + shown below. ## Understanding the Task System -The Task system in THEMAP provides a unified interface for working with multi-modal bioactivity prediction data: +The Task system in THEMAP provides a unified interface for working with +multi-modal bioactivity prediction data: -- **Tasks**: Individual prediction problems (e.g., CHEMBL1023359) -- **Data modalities**: Molecules, proteins, metadata -- **Splits**: Train/test/validation divisions -- **Caching**: Efficient feature storage and retrieval +- **Tasks**: individual prediction problems (e.g. `CHEMBL1023359`) +- **Data modalities**: molecules (`molecule_dataset`) and metadata such as protein + embeddings (`metadata_datasets`) +- **Folds**: train / validation / test divisions, addressed with the `DataFold` enum +- **Caching**: efficient feature storage and retrieval ## Loading Tasks @@ -25,7 +33,7 @@ tasks = Tasks.from_directory( load_molecules=True, load_proteins=True, load_metadata=True, - cache_dir="cache/" + cache_dir="cache/", ) print(f"Loaded {len(tasks)} tasks") @@ -33,7 +41,7 @@ print(f"Loaded {len(tasks)} tasks") ### Task Configuration File -The `sample_tasks_list.json` file defines which tasks belong to which split: +The `sample_tasks_list.json` file defines which tasks belong to which fold: ```json { @@ -49,8 +57,8 @@ The `sample_tasks_list.json` file defines which tasks belong to which split: datasets/ ├── train/ │ ├── CHEMBL1023359.jsonl.gz # Molecular data -│ ├── CHEMBL1023359.fasta # Protein sequences -│ └── CHEMBL1023359_metadata.json +│ ├── CHEMBL1023359.fasta # Protein sequence +│ └── ... ├── test/ │ └── ... ├── valid/ @@ -60,134 +68,157 @@ datasets/ ## Exploring Tasks -### Basic Task Information +Fold-aware methods take a `DataFold` value (`TRAIN`, `VALIDATION`, or `TEST`), not +a string. There is no single "all folds" accessor, so concatenate the three folds +when you need every task ID. ```python -# Get task statistics +from themap.data.enums import DataFold + +# Per-fold counts print(f"Total tasks: {len(tasks)}") -print(f"Train tasks: {tasks.get_num_fold_tasks('TRAIN')}") -print(f"Test tasks: {tasks.get_num_fold_tasks('TEST')}") -print(f"Valid tasks: {tasks.get_num_fold_tasks('VALID')}") +print(f"Train tasks: {tasks.get_num_fold_tasks(DataFold.TRAIN)}") +print(f"Test tasks: {tasks.get_num_fold_tasks(DataFold.TEST)}") +print(f"Valid tasks: {tasks.get_num_fold_tasks(DataFold.VALIDATION)}") -# Get task IDs -train_task_ids = tasks.get_task_ids(fold='TRAIN') +# Task IDs for a single fold +train_task_ids = tasks.get_task_ids(DataFold.TRAIN) print(f"Training task IDs: {train_task_ids}") -# Access individual tasks -task = tasks.get_task("CHEMBL1023359") -print(f"Task {task.task_id} has {len(task.molecules)} molecules") + +def all_task_ids(tasks): + """Return task IDs across every fold.""" + return ( + tasks.get_task_ids(DataFold.TRAIN) + + tasks.get_task_ids(DataFold.VALIDATION) + + tasks.get_task_ids(DataFold.TEST) + ) + + +# Access an individual task +task = tasks.get_task_by_id("CHEMBL1023359") +print(f"Task {task.task_id} has {len(task.molecule_dataset)} molecules") ``` ### Data Modality Access +A `Task` exposes its molecules through `molecule_dataset` and any other modality +(such as protein embeddings) through the `metadata_datasets` dictionary, where +protein data lives under the `"protein"` key. + ```python -# Access different data types -for task_id in tasks.get_task_ids()[:3]: # First 3 tasks - task = tasks.get_task(task_id) +for task_id in all_task_ids(tasks)[:3]: # first 3 tasks + task = tasks.get_task_by_id(task_id) print(f"\nTask: {task_id}") # Molecular data - if hasattr(task, 'molecules') and task.molecules: - print(f" Molecules: {len(task.molecules)}") - print(f" Sample SMILES: {task.molecules[0].smiles}") - - # Protein data - if hasattr(task, 'proteins') and task.proteins: - print(f" Proteins: {len(task.proteins)}") - print(f" Sample protein length: {len(task.proteins[0].sequence)}") - - # Metadata - if hasattr(task, 'metadata') and task.metadata: - print(f" Metadata keys: {list(task.metadata.keys())}") + if task.molecule_dataset is not None: + print(f" Molecules: {len(task.molecule_dataset)}") + print(f" Sample SMILES: {task.molecule_dataset.smiles[0]}") + + # Metadata modalities (e.g. protein embeddings) + if task.metadata_datasets: + print(f" Metadata modalities: {list(task.metadata_datasets.keys())}") + if "protein" in task.metadata_datasets: + print(" Has protein metadata") ``` ## Task-Based Distance Computation ### Unified Distance Calculation +`TaskDistanceCalculator` orchestrates molecule, protein, and combined distances. +`compute_all_distances` returns a dictionary keyed by `"molecules"`, `"protein"`, +and `"combined"`; each value is a nested `{target_id: {source_id: distance}}` map. + ```python -from themap.distance import TaskDistance +from themap.distance import TaskDistanceCalculator -# Create task distance calculator -task_distance = TaskDistance( +# Create the calculator (dataset_method drives molecule distances, +# metadata_method drives protein/metadata distances) +task_distance = TaskDistanceCalculator( tasks=tasks, - molecule_method="cosine", - protein_method="euclidean" + dataset_method="cosine", + metadata_method="euclidean", ) -# Compute all distance types +# Compute every distance type at once all_distances = task_distance.compute_all_distances( - combination_strategy="weighted_average", - molecule_weight=0.7, - protein_weight=0.3 + molecule_featurizer="ecfp", + combination="weighted_average", + weights={"molecules": 0.7, "protein": 0.3}, ) print("Distance types computed:") -print(f" Molecule distances: {len(all_distances['molecule'])} tasks") -print(f" Protein distances: {len(all_distances['protein'])} tasks") -print(f" Combined distances: {len(all_distances['combined'])} tasks") +print(f" Molecule distances: {len(all_distances['molecules'])} target tasks") +print(f" Protein distances: {len(all_distances['protein'])} target tasks") +print(f" Combined distances: {len(all_distances['combined'])} target tasks") ``` ### Specific Distance Types ```python -# Compute only molecular distances -molecule_distances = task_distance.get_molecule_distances() +# Molecule distances only +molecule_distances = task_distance.compute_molecule_distance( + molecule_featurizer="ecfp", +) -# Compute only protein distances -protein_distances = task_distance.get_protein_distances() +# Protein distances only +protein_distances = task_distance.compute_protein_distance( + protein_featurizer="esm2_t33_650M_UR50D", +) -# Compute combined distances with custom weights -combined_distances = task_distance.get_combined_distances( - molecule_weight=0.6, - protein_weight=0.4 +# Combined distances with custom weights +combined_distances = task_distance.compute_combined_distance( + molecule_featurizer="ecfp", + weights={"molecules": 0.6, "protein": 0.4}, + combination="weighted_average", ) ``` ## Working with Folds -### Train-Test Analysis +### Train–Test Analysis + +Each distance matrix is indexed as `distances[target_id][source_id]`. With the +default `source_fold=DataFold.TRAIN`, train tasks are the sources (inner keys) and +the evaluated tasks are the targets (outer keys). ```python -def analyze_train_test_distances(tasks, distance_type="molecule"): - """Analyze distances between train and test tasks.""" +import pandas as pd +from themap.data.enums import DataFold - # Get task IDs by fold - train_ids = set(tasks.get_task_ids(fold='TRAIN')) - test_ids = set(tasks.get_task_ids(fold='TEST')) - # Compute distances - task_distance = TaskDistance(tasks=tasks) - distances = task_distance.compute_all_distances()[distance_type] +def analyze_train_test_distances(tasks, distance_type="molecules"): + """Collect distances between train (source) and test (target) tasks.""" + train_ids = set(tasks.get_task_ids(DataFold.TRAIN)) + test_ids = set(tasks.get_task_ids(DataFold.TEST)) - # Extract train-test distances - train_test_distances = [] + task_distance = TaskDistanceCalculator(tasks=tasks) + distances = task_distance.compute_all_distances()[distance_type] + pairs = [] for test_id in test_ids: - if test_id in distances: - for train_id in train_ids: - if train_id in distances[test_id]: - train_test_distances.append({ - 'test_task': test_id, - 'train_task': train_id, - 'distance': distances[test_id][train_id] - }) - - return train_test_distances - -# Analyze train-test relationships -tt_distances = analyze_train_test_distances(tasks, "molecule") + if test_id not in distances: + continue + for train_id in train_ids: + if train_id in distances[test_id]: + pairs.append({ + "test_task": test_id, + "train_task": train_id, + "distance": distances[test_id][train_id], + }) + return pairs + + +tt_distances = analyze_train_test_distances(tasks, "molecules") print(f"Found {len(tt_distances)} train-test pairs") -# Find closest training tasks for each test task -import pandas as pd - +# Closest training tasks for each test task df = pd.DataFrame(tt_distances) -for test_task in df['test_task'].unique(): - task_distances = df[df['test_task'] == test_task] - closest = task_distances.nsmallest(3, 'distance') - +for test_task in df["test_task"].unique(): + closest = df[df["test_task"] == test_task].nsmallest(3, "distance") print(f"\nTest task {test_task} - closest training tasks:") for _, row in closest.iterrows(): print(f" {row['train_task']}: {row['distance']:.3f}") @@ -196,31 +227,30 @@ for test_task in df['test_task'].unique(): ### Cross-Validation Support ```python -def create_cv_folds(tasks, n_folds=5): - """Create cross-validation folds from tasks.""" +import random +from themap.data.enums import DataFold - all_task_ids = tasks.get_task_ids() - random.shuffle(all_task_ids) # Randomize - fold_size = len(all_task_ids) // n_folds +def create_cv_folds(tasks, n_folds=5): + """Create cross-validation folds from all task IDs.""" + task_ids = ( + tasks.get_task_ids(DataFold.TRAIN) + + tasks.get_task_ids(DataFold.VALIDATION) + + tasks.get_task_ids(DataFold.TEST) + ) + random.shuffle(task_ids) + + fold_size = len(task_ids) // n_folds folds = [] - for i in range(n_folds): - start_idx = i * fold_size - end_idx = start_idx + fold_size if i < n_folds - 1 else len(all_task_ids) - - test_tasks = all_task_ids[start_idx:end_idx] - train_tasks = [tid for tid in all_task_ids if tid not in test_tasks] - - folds.append({ - 'fold': i, - 'train': train_tasks, - 'test': test_tasks - }) - + start = i * fold_size + end = start + fold_size if i < n_folds - 1 else len(task_ids) + test_tasks = task_ids[start:end] + train_tasks = [tid for tid in task_ids if tid not in test_tasks] + folds.append({"fold": i, "train": train_tasks, "test": test_tasks}) return folds -# Create CV folds + cv_folds = create_cv_folds(tasks, n_folds=5) print(f"Created {len(cv_folds)} CV folds") ``` @@ -229,31 +259,31 @@ print(f"Created {len(cv_folds)} CV folds") ### Feature Caching +Pass a `cache_dir` and THEMAP caches computed features there, reusing them on +subsequent loads. + ```python -# Tasks automatically cache computed features tasks_with_cache = Tasks.from_directory( directory="datasets/", task_list_file="datasets/sample_tasks_list.json", load_molecules=True, load_proteins=True, - cache_dir="cache/", # Features will be cached here - force_reload=False # Use cached features if available + cache_dir="cache/", # features are cached here ) -# Check cache status print(f"Cache directory: {tasks_with_cache.cache_dir}") ``` ### Selective Loading ```python -# Load only specific data types for better performance +# Load only what you need for faster processing molecule_only_tasks = Tasks.from_directory( directory="datasets/", task_list_file="datasets/sample_tasks_list.json", load_molecules=True, - load_proteins=False, # Skip protein loading - load_metadata=False # Skip metadata loading + load_proteins=False, # skip protein loading + load_metadata=False, # skip other metadata ) print("Loaded molecular data only for faster processing") @@ -266,49 +296,37 @@ print("Loaded molecular data only for faster processing") ```python def filter_tasks_by_size(tasks, min_molecules=10, max_molecules=1000): """Filter tasks by number of molecules.""" + filtered = [] + for task_id in all_task_ids(tasks): + task = tasks.get_task_by_id(task_id) + if task.molecule_dataset is None: + continue + n_molecules = len(task.molecule_dataset) + if min_molecules <= n_molecules <= max_molecules: + filtered.append(task_id) + return filtered - filtered_task_ids = [] - - for task_id in tasks.get_task_ids(): - task = tasks.get_task(task_id) - if hasattr(task, 'molecules') and task.molecules: - n_molecules = len(task.molecules) - if min_molecules <= n_molecules <= max_molecules: - filtered_task_ids.append(task_id) - return filtered_task_ids - -# Filter tasks good_tasks = filter_tasks_by_size(tasks, min_molecules=20, max_molecules=500) -print(f"Found {len(good_tasks)} tasks with appropriate size") +print(f"Found {len(good_tasks)} tasks with an appropriate size") ``` -### Task Metadata Analysis +### Inspecting Modalities ```python -def analyze_task_metadata(tasks): - """Analyze metadata across all tasks.""" - - metadata_summary = {} - - for task_id in tasks.get_task_ids(): - task = tasks.get_task(task_id) - - if hasattr(task, 'metadata') and task.metadata: - for key, value in task.metadata.items(): - if key not in metadata_summary: - metadata_summary[key] = [] - metadata_summary[key].append(value) - - # Print summary - for key, values in metadata_summary.items(): - unique_values = len(set(str(v) for v in values)) - print(f"Metadata '{key}': {len(values)} tasks, {unique_values} unique values") - - return metadata_summary - -# Analyze metadata -metadata = analyze_task_metadata(tasks) +def summarize_modalities(tasks): + """Count how many tasks carry each metadata modality.""" + counts = {} + for task_id in all_task_ids(tasks): + task = tasks.get_task_by_id(task_id) + for modality in (task.metadata_datasets or {}): + counts[modality] = counts.get(modality, 0) + 1 + for modality, n in counts.items(): + print(f"Modality '{modality}': present in {n} tasks") + return counts + + +summarize_modalities(tasks) ``` ## Integration with External Tools @@ -316,46 +334,25 @@ metadata = analyze_task_metadata(tasks) ### Export for Analysis ```python -def export_task_summary(tasks, output_file="task_summary.csv"): - """Export task information to CSV for external analysis.""" - - import pandas as pd - - summary_data = [] - - for task_id in tasks.get_task_ids(): - task = tasks.get_task(task_id) - - row = {'task_id': task_id} - - # Add molecule info - if hasattr(task, 'molecules') and task.molecules: - row['n_molecules'] = len(task.molecules) - else: - row['n_molecules'] = 0 - - # Add protein info - if hasattr(task, 'proteins') and task.proteins: - row['n_proteins'] = len(task.proteins) - row['avg_protein_length'] = sum(len(p.sequence) for p in task.proteins) / len(task.proteins) - else: - row['n_proteins'] = 0 - row['avg_protein_length'] = 0 - - # Add metadata - if hasattr(task, 'metadata') and task.metadata: - for key, value in task.metadata.items(): - row[f'metadata_{key}'] = value +import pandas as pd - summary_data.append(row) - df = pd.DataFrame(summary_data) +def export_task_summary(tasks, output_file="task_summary.csv"): + """Export per-task information to CSV for external analysis.""" + rows = [] + for task_id in all_task_ids(tasks): + task = tasks.get_task_by_id(task_id) + rows.append({ + "task_id": task_id, + "n_molecules": len(task.molecule_dataset) if task.molecule_dataset else 0, + "modalities": ",".join((task.metadata_datasets or {}).keys()), + }) + df = pd.DataFrame(rows) df.to_csv(output_file, index=False) print(f"Task summary exported to {output_file}") - return df -# Export summary + task_df = export_task_summary(tasks) ``` @@ -363,75 +360,34 @@ task_df = export_task_summary(tasks) ### Performance Tips -1. **Use caching**: Always specify a cache directory -2. **Load selectively**: Only load needed data types -3. **Filter early**: Remove unsuitable tasks before distance computation -4. **Batch processing**: Process tasks in groups for large datasets +1. **Use caching**: always specify a `cache_dir`. +2. **Load selectively**: only load the modalities you need. +3. **Filter early**: remove unsuitable tasks before distance computation. +4. **Batch processing**: process tasks in groups for large datasets. ### Memory Management -```python -# For large task sets, process in batches -def process_tasks_in_batches(tasks, batch_size=10): - """Process tasks in smaller batches to manage memory.""" - - task_ids = tasks.get_task_ids() - - for i in range(0, len(task_ids), batch_size): - batch_ids = task_ids[i:i + batch_size] - - # Create subset tasks - batch_tasks = Tasks() - for task_id in batch_ids: - batch_tasks.add_task(tasks.get_task(task_id)) - - # Process batch - yield batch_tasks - -# Example usage -for batch_tasks in process_tasks_in_batches(tasks, batch_size=5): - print(f"Processing batch with {len(batch_tasks)} tasks") - # Perform distance computation on batch -``` - -## Troubleshooting - -### Common Issues - -1. **Missing files**: Ensure all task files exist in expected directories -2. **Cache conflicts**: Use `force_reload=True` to refresh cached features -3. **Memory errors**: Reduce batch size or load fewer data modalities -4. **Inconsistent data**: Validate that all tasks have required data types - -### Validation +For large task sets, build smaller `Tasks` collections with the constructor +(there is no incremental `add_task`; pass fold lists instead). ```python -def validate_tasks(tasks): - """Validate task data consistency.""" - - issues = [] +from themap.data.enums import DataFold - for task_id in tasks.get_task_ids(): - task = tasks.get_task(task_id) - # Check for required data - if not hasattr(task, 'molecules') or not task.molecules: - issues.append(f"Task {task_id}: No molecular data") - - if not hasattr(task, 'proteins') or not task.proteins: - issues.append(f"Task {task_id}: No protein data") - - if issues: - print("Validation issues found:") - for issue in issues: - print(f" - {issue}") - else: - print("All tasks validated successfully") +def process_train_tasks_in_batches(tasks, batch_size=10): + """Yield smaller Tasks collections over the train fold.""" + train_ids = tasks.get_task_ids(DataFold.TRAIN) + for i in range(0, len(train_ids), batch_size): + batch_ids = train_ids[i:i + batch_size] + batch_tasks = Tasks( + train_tasks=[tasks.get_task_by_id(tid) for tid in batch_ids], + ) + yield batch_tasks - return issues -# Validate loaded tasks -validation_issues = validate_tasks(tasks) +for batch_tasks in process_train_tasks_in_batches(tasks, batch_size=5): + print(f"Processing batch with {len(batch_tasks)} tasks") + # Perform distance computation on the batch ``` ## Next Steps diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index 404f2fc..6c1aa62 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -8,32 +8,37 @@ This guide covers installation and three ways to compute distances between molec - Python 3.10 or higher -### Quick Install +### Install from PyPI (recommended) ```bash -git clone https://github.com/HFooladi/THEMAP.git -cd THEMAP -source install.sh # creates .venv with uv +pip install themap # core +pip install "themap[all]" # everything (ML, protein, OTDD) ``` -To reactivate later: +Install only the extras you need: ```bash -source .venv/bin/activate +pip install "themap[ml]" # molecular analysis +pip install "themap[protein]" # protein analysis (ESM2) +pip install "themap[otdd]" # OTDD distance computation ``` -### Optional Dependencies +### Install from source (development) + +```bash +git clone https://github.com/HFooladi/THEMAP.git +cd THEMAP +source install.sh # creates .venv with uv, installs dev + test extras +``` -THEMAP has optional dependency groups for different functionality: +To reactivate the environment later: ```bash -pip install -e ".[ml]" # molecular analysis -pip install -e ".[protein]" # protein analysis (ESM2) -pip install -e ".[otdd]" # OTDD distance computation -pip install -e ".[all]" # everything -pip install -e ".[dev,test]" # development +source .venv/bin/activate ``` +The editable equivalent of the extras above is `pip install -e ".[all]"` (or `".[ml]"`, `".[dev,test]"`, etc.). + ### Verify Installation ```python diff --git a/docs/user-guide/reproducing-fsmol.md b/docs/user-guide/reproducing-fsmol.md new file mode 100644 index 0000000..2ddf26f --- /dev/null +++ b/docs/user-guide/reproducing-fsmol.md @@ -0,0 +1,78 @@ +# Reproducing FS-Mol Experiments + +The companion data for our paper *"Quantifying the hardness of bioactivity +prediction tasks for transfer learning"* (J. Chem. Inf. Model. 64(10), 4031–4046, +2024) is published on [Zenodo (record 10605093)](https://zenodo.org/records/10605093). +It contains pre-computed OTDD distance matrices across multiple molecular +featurizers, ESM-2 protein embeddings, internal chemical hardness measures, and +ProtoNet evaluation summaries on the FS-Mol benchmark — everything needed to +reproduce the figures and tables without re-running the expensive embedding +pipelines. + +## 1. Install dependencies + +```bash +source install.sh # creates .venv and installs themap[all,dev,test] +``` + +The reproduction notebooks rely on the optional `ml` extras (torch, ESM, etc.); +the all-in-one install above covers them. + +## 2. Download the dataset (~16 GB) + +You need ~35 GB of free disk space (16 GB zip + ~16 GB extracted). The script +downloads with resume support, verifies the MD5 checksum, extracts into +`datasets/fsmol_hardness/`, and removes the zip when done. + +```bash +make download-fsmol +# or, equivalently: +python scripts/download_fsmol_data.py +``` + +Useful flags: `--keep-zip` (don't delete the archive after extraction), `--force` +(re-download), `--no-verify` (skip MD5 — only if you've already verified +out-of-band), `--dest DIR` (custom location). + +After it completes (~31 GB extracted) you should see: + +``` +datasets/fsmol_hardness/ +├── ext_chem/ # OTDD distance matrices per molecular featurizer +├── ext_prot/ # ESM-2 protein-distance matrices (t6_8M ... t36_3B) +├── int_chem/{train,test}/ # Internal chemical hardness (RF baselines) +├── embeddings/ # Per-task molecular embeddings used to compute the OTDDs +├── FSMol_Eval_ProtoNet/summary/ # ProtoNet performance per support-set size (16/32/64/128) +└── FSMol_Eval_randomForest/summary/ # Random-forest baseline performance summaries +``` + +The reproduction notebooks read from `ext_chem/`, `ext_prot/`, `int_chem/`, and +`FSMol_Eval_ProtoNet/`; the other two directories are provided so you can rebuild +the OTDD matrices from raw embeddings if desired. + +??? note "Manual download (no Python)" + + ```bash + mkdir -p datasets/fsmol_hardness + cd datasets + wget -c https://zenodo.org/records/10605093/files/fsmol_hardness.zip + echo "10644660a53d8d106b6883cb53eb1f3b fsmol_hardness.zip" | md5sum -c - + unzip fsmol_hardness.zip -d fsmol_hardness/ + ``` + +## 3. Run the reproduction notebooks + +```bash +cd notebooks +jupyter lab # or: jupyter notebook +``` + +| Notebook | What it reproduces | +| --- | --- | +| [`external_chemical_hardness.ipynb`](https://github.com/HFooladi/THEMAP/blob/main/notebooks/external_chemical_hardness.ipynb) | External chemical-space hardness: correlation between k-nearest source-task OTDD distance and ProtoNet performance, across molecular featurizers (GIN, UniMol, ChemBERTa/Roberta-Zinc, desc2D). | +| [`external_protein_hardness.ipynb`](https://github.com/HFooladi/THEMAP/blob/main/notebooks/external_protein_hardness.ipynb) | External protein-space hardness: correlation between target/source protein-embedding distance and performance, across ESM-2 model sizes (t6_8M → t36_3B). | +| [`task_hardness.ipynb`](https://github.com/HFooladi/THEMAP/blob/main/notebooks/task_hardness.ipynb) | Combined task-hardness score (external chemical + external protein + internal chemical) and its correlation with ProtoNet performance at support-set sizes 16/32/64/128. | + +Notebook paths are resolved relative to the `notebooks/` directory, so launch +Jupyter from there. Outputs are auto-stripped on commit by the pre-commit hook +(`nbstripout`). diff --git a/mkdocs.yml b/mkdocs.yml index bbfb791..9dd2e02 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,9 +19,11 @@ nav: - User Guide: - Distance Computation: user-guide/distance-computation.md - Command Line Interface: user-guide/cli.md + - Reproducing FS-Mol: user-guide/reproducing-fsmol.md - Tutorials: - Overview: tutorials/index.md + - Run on Colab: tutorials/colab-notebooks.md - Basic Distance Computation: tutorials/basic-distance-computation.md - Working with Tasks: tutorials/working-with-tasks.md - Performance Optimization: tutorials/performance-optimization.md @@ -57,6 +59,7 @@ theme: features: - navigation.tabs - navigation.tabs.sticky + - navigation.sections - navigation.path - navigation.top - navigation.footer @@ -64,6 +67,7 @@ theme: - toc.follow - content.code.copy - content.code.annotate + - content.tabs.link - search.suggest - search.highlight @@ -71,7 +75,7 @@ theme: logo: assets/images/logo-white.svg extra_css: - - assets/css/custom-splito.css + - assets/css/themap.css extra_javascript: - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js @@ -122,12 +126,19 @@ plugins: - sys.path.append("themap") options: show_root_heading: yes + show_root_full_path: false heading_level: 3 show_source: false group_by_category: true members_order: source separate_signature: true show_signature_annotations: true + signature_crossrefs: true + merge_init_into_class: true + show_symbol_type_heading: true + show_symbol_type_toc: true + docstring_section_style: table + filters: ["!^_"] line_length: 80 docstring_style: google @@ -149,6 +160,6 @@ extra: link: https://github.com/HFooladi/THEMAP - icon: fontawesome/brands/python link: https://pypi.org/project/themap/ - analytics: - provider: google - property: G-XXXXXXXXXX + # analytics: + # provider: google + # property: G-XXXXXXXXXX # replace with a real Measurement ID to enable