{ "cells": [ { "cell_type": "markdown", "id": "6b76e37d", "metadata": {}, "source": [ "# Tutorial 07 — Ligands and Parameter Files\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/uw-ipd/tmol/blob/master/docs/tutorial/07_ligand_and_params.ipynb)\n", "\n", "This tutorial loads authoritative ligand chemistry, builds a ligand-aware `PoseStack`, scores one protein-ligand complex, and refines its pocket. It also shows how TMol parameter files differ from Rosetta `.params` files.\n", "\n", "## Learning objectives\n", "\n", "- Load or generate ligand chemistry and extend a `ParameterDatabase`.\n", "- Build and score a protein-ligand complex.\n", "- Repack and minimize a local pocket.\n", "\n", "## Before you begin\n", "\n", "- **Prerequisites:** The core Tutorials 01–06, especially scoring, packing, and minimization.\n", "- **Curriculum:** This is the first specialized path after 06. Continue to [08 — Nucleic acids](08_nucleic_acids.ipynb), which includes an RNA–ligand example.\n", "- **Related:** [Ligand workflow](../user_guide/ligands.md) · [Ligand API](../api/ligand.rst)\n", "\n", "The reported cross-mask value comes from one connected complex; it is not a binding free energy. The Rosetta writer is an interoperability aid, not a validated parameterization workflow." ] }, { "cell_type": "markdown", "id": "558b3ed7", "metadata": {}, "source": [ "## Setup\n", "\n", "The tutorial uses checked-in structures and parameter files, so it requires no network access. It keeps two chemistry paths separate: a pinned ADA/LG1 protein–ligand complex for scoring and refinement, and an independent AMPC MOL2 preparation example for inspecting generated parameters." ] }, { "cell_type": "code", "execution_count": 1, "id": "dcf8a4dd", "metadata": {}, "outputs": [], "source": [ "try:\n", " import google.colab # noqa: F401\n", "except ImportError:\n", " IN_COLAB = False\n", "else:\n", " IN_COLAB = True\n", "\n", "if IN_COLAB:\n", " from urllib.request import urlopen\n", "\n", " exec(\n", " urlopen(\n", " \"https://raw.githubusercontent.com/uw-ipd/tmol/\"\n", " \"master/docs/tutorial/colab_setup.py\"\n", " ).read(),\n", " globals(),\n", " )\n", " setup_colab(\n", " [\n", " \"tmol/tests/data/protein_ligand_test/ada.xtal-lig.mmff94.tmol\",\n", " \"tmol/tests/data/protein_ligand_test/ada.tmol.nomin.cif\",\n", " \"tmol/tests/data/ligand_test/ligand_ground_truth/mol2/ampc_1.mol2\",\n", " ]\n", " )" ] }, { "cell_type": "code", "execution_count": 2, "id": "d3aedaae", "metadata": {}, "outputs": [], "source": [ "from contextlib import redirect_stderr, redirect_stdout\n", "from io import StringIO\n", "from pathlib import Path\n", "import tempfile\n", "import warnings\n", "\n", "import attrs\n", "import biotite.structure as struc\n", "import biotite.structure.io\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", "import torch\n", "import yaml\n", "from IPython.display import display\n", "\n", "import tmol\n", "from tmol.database import ParameterDatabase\n", "from tmol.io import build_context_from_biotite, pose_stack_from_biotite\n", "from tmol.ligand import (\n", " inject_params_file,\n", " load_params_file,\n", " nonstandard_residue_info_from_mol2,\n", " prepare_single_ligand,\n", " read_params_file,\n", " write_params_file,\n", ")\n", "from tmol.ops import (\n", " build_coord_mask_for_mask_and_interacting_atoms,\n", " calculate_block_pair_ddg,\n", " compute_block_adjacency,\n", " compute_block_centroids_and_furthest_dist,\n", ")\n", "from tmol.score import beta2016_score_function\n", "\n", "SEED = 20260807\n", "np.random.seed(SEED)\n", "torch.manual_seed(SEED)\n", "warnings.filterwarnings(\n", " \"ignore\", message=r\"Sparse invariant checks are implicitly disabled.*\"\n", ")\n", "device = (\n", " torch.device(\"cuda\", torch.cuda.current_device())\n", " if torch.cuda.is_available()\n", " else torch.device(\"cpu\")\n", ")\n", "LIGAND_RES_NAME = \"LG1\"\n", "\n", "\n", "def show_table(frame):\n", " try:\n", " from itables import show\n", " except ImportError:\n", " return display(frame)\n", " return show(frame)\n", "\n", "\n", "def ligand_block_mask(pose_stack):\n", " mask = torch.zeros_like(pose_stack.block_type_ind, dtype=torch.bool)\n", " for pose_i in range(pose_stack.n_poses):\n", " for block_i in range(pose_stack.max_n_blocks):\n", " type_i = int(pose_stack.block_type_ind[pose_i, block_i])\n", " if type_i < 0:\n", " continue\n", " block_type = pose_stack.packed_block_types.active_block_types[type_i]\n", " mask[pose_i, block_i] = block_type.name3 == LIGAND_RES_NAME\n", " if not bool(mask.any()):\n", " raise RuntimeError(f\"No {LIGAND_RES_NAME} ligand block was built\")\n", " return mask" ] }, { "cell_type": "markdown", "id": "d359d0d6", "metadata": {}, "source": [ "## Two parameter representations\n", "\n", "Prefer mmCIF/CIF for structure input, especially for protein–ligand systems: CIF chemical-component and bond tables can preserve connectivity and bond order that PDB coordinate records do not reliably encode. Retain MOL2 or prepared `.tmol` chemistry when it is the authoritative ligand source; a coordinate-only conversion cannot recover missing bond orders.\n", "\n", "Rosetta `.params` is a line-oriented, normally one-residue format. Ligand records commonly include `ATOM`, `BOND`/`BOND_TYPE`, `CHI`, `PROTON_CHI`, `NBR_ATOM`, and `ICOOR_INTERNAL`. TMol's current Rosetta output is an **experimental syntactic export** of related records, not a Rosetta parameterization or validation workflow. In particular, the next cells expose its current TMol atom types, hard-coded Rosetta MM type `X`, and placeholder `NBR_RADIUS 999.0`. Those fields must be replaced or validated in an actual Rosetta preparation workflow before Rosetta use.\n", "\n", "TMol's portable `.tmol` file is versioned YAML with three top-level payloads:\n", "\n", "- `chemical`: residue atoms, bonds, internal coordinates, torsions, and properties;\n", "- `elec`: per-atom partial charges; and\n", "- `cartbonded`: residue-specific bonded parameters.\n", "\n", "The same schemas are split across TMol's canonical database files at `tmol/database/default/chemical/chemical.yaml`, `tmol/database/default/scoring/elec.yaml`, and `tmol/database/default/scoring/cartbonded.yaml`. A portable `.tmol` file bundles ligand additions to those three domains; `chemical.yaml` alone is not a complete scoring parameter set." ] }, { "cell_type": "code", "execution_count": 3, "id": "5525e2ca", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Wrote: /tmp/tmol-ligand-tutorial-1nzaecoz/LG1.params\n", "Wrote: /tmp/tmol-ligand-tutorial-1nzaecoz/LG1.tmol\n" ] } ], "source": [ "repo_root = Path.cwd()\n", "if not (\n", " repo_root / \"tmol/tests/data/protein_ligand_test/ada.tmol.nomin.cif\"\n", ").exists():\n", " repo_root = Path(tmol.__file__).resolve().parents[1]\n", "data_dir = repo_root / \"tmol\" / \"tests\" / \"data\" / \"protein_ligand_test\"\n", "reference_tmol = data_dir / \"ada.xtal-lig.mmff94.tmol\"\n", "complex_cif = data_dir / \"ada.tmol.nomin.cif\"\n", "ligand_mol2 = (\n", " repo_root\n", " / \"tmol\"\n", " / \"tests\"\n", " / \"data\"\n", " / \"ligand_test\"\n", " / \"ligand_ground_truth\"\n", " / \"mol2\"\n", " / \"ampc_1.mol2\"\n", ")\n", "\n", "# This is the pinned ADA/LG1 preparation used by the complex below. The later\n", "# AMPC MOL2 preparation is an independent pipeline demonstration.\n", "preparation = load_params_file(reference_tmol)[0]\n", "work_dir = Path(tempfile.mkdtemp(prefix=\"tmol-ligand-tutorial-\"))\n", "rosetta_path = work_dir / \"LG1.params\"\n", "tmol_path = work_dir / \"LG1.tmol\"\n", "write_params_file(preparation, rosetta_path, format=\"rosetta\")\n", "write_params_file(preparation, tmol_path, format=\"tmol\")\n", "\n", "print(\"Wrote:\", rosetta_path)\n", "print(\"Wrote:\", tmol_path)" ] }, { "cell_type": "code", "execution_count": 4, "id": "3767019c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Experimental Rosetta-syntax .params excerpt\n", "NAME LG1\n", "IO_STRING LG1 Z\n", "TYPE LIGAND\n", "AA UNK\n", "ATOM C1 CR X -0.3020\n", "ATOM N1 Nad3 X 0.0480\n", "ATOM C2 CR X 0.0360\n", "ATOM N2 Nim X -0.5650\n", "ATOM C3 CR X 0.1410\n", "ATOM C4 CDp X 0.7160\n", "ATOM O1 Oad X -0.5700\n", "ATOM N3 Nad X -0.8000\n", "ATOM C5 CS1 X 0.2560\n", "ATOM C6 CSp X 0.2800\n", "ATOM O2 Ohx X -0.6800\n", "ATOM C7 CS2 X 0.0000\n", "ATOM C8 CS2 X 0.1430\n", "ATOM C9 CR X -0.1430\n", "\n", "TMol .tmol excerpt\n", "version: '1.0'\n", "chemical:\n", " residues:\n", " - name: LG1\n", " base_name: LG1\n", " name3: LG1\n", " io_equiv_class: LG1\n", " atoms:\n", " - {name: C1, atom_type: CR}\n", " - {name: N1, atom_type: Nad3}\n", " - {name: C2, atom_type: CR}\n", " - {name: N2, atom_type: Nim}\n", " - {name: C3, atom_type: CR}\n", " - {name: C4, atom_type: CDp}\n", " - {name: O1, atom_type: Oad}\n", " - {name: N3, atom_type: Nad}\n", " - {name: C5, atom_type: CS1}\n", " - {name: C6, atom_type: CSp}\n" ] }, { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "
atomTMol_atom_typepinned_partial_chargeexport_atom_typeexport_MM_typeexport_partial_chargeprovenance
C1CR-0.302CRX-0.302pinned ada.xtal-lig.mmff94.tmol
N1Nad30.048Nad3X0.048pinned ada.xtal-lig.mmff94.tmol
C2CR0.036CRX0.036pinned ada.xtal-lig.mmff94.tmol
N2Nim-0.565NimX-0.565pinned ada.xtal-lig.mmff94.tmol
C3CR0.141CRX0.141pinned ada.xtal-lig.mmff94.tmol
C4CDp0.716CDpX0.716pinned ada.xtal-lig.mmff94.tmol
O1Oad-0.570OadX-0.570pinned ada.xtal-lig.mmff94.tmol
N3Nad-0.800NadX-0.800pinned ada.xtal-lig.mmff94.tmol
C5CS10.256CS1X0.256pinned ada.xtal-lig.mmff94.tmol
C6CSp0.280CSpX0.280pinned ada.xtal-lig.mmff94.tmol
(26 more rows not shown)
\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Syntactic-export diagnostics: all MM types are X; NBR_RADIUS is the placeholder 999.\n" ] }, { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", "
\n", " \n", " \n", " \n", " \n", "\n", "\n", "\n", "
representationtop-level/recordsvalidated_use
experimental Rosetta-syntax .paramsATOM:36, BOND:0, BOND_TYPE:37, CHI:7, PROTON_CHI:0, NBR_ATOM:1, NBR_RADIUS:1, ICOOR_INTERNAL:36inspection only; not Rosetta-validated
TMol .tmolversion, chemical, elec, cartbondedpinned input for this TMol tutorial
Canonical split YAMLchemical.yaml + elec.yaml + cartbonded.yamlTMol database organization
\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "rosetta_lines = rosetta_path.read_text().splitlines()\n", "tmol_document = yaml.safe_load(tmol_path.read_text())\n", "\n", "print(\"Experimental Rosetta-syntax .params excerpt\")\n", "print(\"\\n\".join(rosetta_lines[:18]))\n", "print(\"\\nTMol .tmol excerpt\")\n", "print(\"\\n\".join(tmol_path.read_text().splitlines()[:18]))\n", "\n", "records = [\n", " \"ATOM\",\n", " \"BOND\",\n", " \"BOND_TYPE\",\n", " \"CHI\",\n", " \"PROTON_CHI\",\n", " \"NBR_ATOM\",\n", " \"NBR_RADIUS\",\n", " \"ICOOR_INTERNAL\",\n", "]\n", "record_counts = {\n", " record: sum(line.startswith(record + \" \") for line in rosetta_lines)\n", " for record in records\n", "}\n", "written_atoms = {\n", " fields[1]: {\n", " \"export_atom_type\": fields[2],\n", " \"export_MM_type\": fields[3],\n", " \"export_partial_charge\": float(fields[4]),\n", " }\n", " for line in rosetta_lines\n", " if line.startswith(\"ATOM \")\n", " for fields in [line.split()]\n", "}\n", "nbr_radius = float(\n", " next(line.split()[1] for line in rosetta_lines if line.startswith(\"NBR_RADIUS \"))\n", ")\n", "ada_atom_frame = pd.DataFrame(\n", " [\n", " {\n", " \"atom\": atom.name,\n", " \"TMol_atom_type\": atom.atom_type,\n", " \"pinned_partial_charge\": preparation.partial_charges[atom.name],\n", " **written_atoms[atom.name],\n", " \"provenance\": \"pinned ada.xtal-lig.mmff94.tmol\",\n", " }\n", " for atom in preparation.residue_type.atoms\n", " ]\n", ")\n", "assert record_counts[\"BOND_TYPE\"] == len(preparation.residue_type.bonds)\n", "assert set(ada_atom_frame[\"export_MM_type\"]) == {\"X\"}\n", "assert np.array_equal(\n", " ada_atom_frame[\"TMol_atom_type\"], ada_atom_frame[\"export_atom_type\"]\n", ")\n", "assert nbr_radius == 999.0\n", "assert np.allclose(\n", " ada_atom_frame[\"pinned_partial_charge\"],\n", " ada_atom_frame[\"export_partial_charge\"],\n", " atol=5e-5,\n", ")\n", "show_table(ada_atom_frame)\n", "print(\n", " \"Syntactic-export diagnostics: all MM types are X; \"\n", " f\"NBR_RADIUS is the placeholder {nbr_radius:g}.\"\n", ")\n", "format_comparison = pd.DataFrame(\n", " [\n", " {\n", " \"representation\": \"experimental Rosetta-syntax .params\",\n", " \"top-level/records\": \", \".join(\n", " f\"{key}:{value}\" for key, value in record_counts.items()\n", " ),\n", " \"validated_use\": \"inspection only; not Rosetta-validated\",\n", " },\n", " {\n", " \"representation\": \"TMol .tmol\",\n", " \"top-level/records\": \", \".join(tmol_document.keys()),\n", " \"validated_use\": \"pinned input for this TMol tutorial\",\n", " },\n", " {\n", " \"representation\": \"Canonical split YAML\",\n", " \"top-level/records\": (\n", " \"chemical.yaml + elec.yaml + cartbonded.yaml\"\n", " ),\n", " \"validated_use\": \"TMol database organization\",\n", " },\n", " ]\n", ")\n", "show_table(format_comparison)" ] }, { "cell_type": "markdown", "id": "ab327207", "metadata": {}, "source": [ "## The Rosetta reader is intentionally lossy\n", "\n", "The experimental writer emits bond-order-aware `BOND_TYPE` records and copies each available partial charge into the fourth field of its `ATOM` record. It otherwise carries TMol atom-type strings into the Rosetta atom-type column, hard-codes MM type `X`, and writes `NBR_RADIUS 999.0`; it does not assign or validate Rosetta atom/MM types, neighbor geometry, or scoring behavior. Consequently, the output must not be described as a usable Rosetta parameter set.\n", "\n", "The deliberately partial `read_params_file()` supports names, atom names/types, `BOND`/`BOND_TYPE`, `CHI`, `PROTON_CHI`, `NBR_ATOM`, and `ICOOR_INTERNAL`, but it drops the serialized `ATOM` charges and ignores unsupported Rosetta records. A `RawResidueType` reconstructed from `.params` therefore has neither TMol's separate electrostatic-charge map nor its residue-specific cartbonded parameter tables. Neither direction is a validated or lossless interconversion.\n", "\n", "**Expected observations.** Topology counts should survive this file's write/read inspection, `BOND_TYPE` should be present, and nonzero charges should be visible in the written `ATOM` lines; charge and cartbonded rows are nevertheless absent from the reader result. Matching counts establish neither parameter-value equivalence nor usability in Rosetta." ] }, { "cell_type": "code", "execution_count": 5, "id": "79ef40ce", "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", "
\n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "\n", "\n", "\n", "\n", "
fieldsource_countreader_countcount_preserved
atoms3636True
bonds3737True
torsions / chi declarations77True
internal coordinates3636True
partial-charge map360False
cartbonded parameter rows990False
\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Count equality checks structure, not full parameter-value equivalence.\n" ] } ], "source": [ "partial_residue_type = read_params_file(rosetta_path)\n", "source_residue_type = preparation.residue_type\n", "count_pairs = [\n", " (\"atoms\", len(source_residue_type.atoms), len(partial_residue_type.atoms)),\n", " (\"bonds\", len(source_residue_type.bonds), len(partial_residue_type.bonds)),\n", " (\"torsions / chi declarations\", len(source_residue_type.torsions), len(partial_residue_type.torsions)),\n", " (\"internal coordinates\", len(source_residue_type.icoors), len(partial_residue_type.icoors)),\n", "]\n", "lossiness_rows = [\n", " {\n", " \"field\": field,\n", " \"source_count\": source_count,\n", " \"reader_count\": reader_count,\n", " \"count_preserved\": source_count == reader_count,\n", " }\n", " for field, source_count, reader_count in count_pairs\n", "]\n", "lossiness_rows.extend(\n", " [\n", " {\n", " \"field\": \"partial-charge map\",\n", " \"source_count\": len(preparation.partial_charges),\n", " \"reader_count\": 0,\n", " \"count_preserved\": False,\n", " },\n", " {\n", " \"field\": \"cartbonded parameter rows\",\n", " \"source_count\": sum(\n", " len(getattr(preparation.cartbonded_params, field.name))\n", " for field in attrs.fields(type(preparation.cartbonded_params))\n", " ),\n", " \"reader_count\": 0,\n", " \"count_preserved\": False,\n", " },\n", " ]\n", ")\n", "lossiness = pd.DataFrame(lossiness_rows)\n", "show_table(lossiness)\n", "print(\"Count equality checks structure, not full parameter-value equivalence.\")" ] }, { "cell_type": "markdown", "id": "5233bd36", "metadata": {}, "source": [ "## Prepare separate AMPC chemistry from MOL2\n", "\n", "This is an independent pipeline demonstration, not regeneration or validation of the pinned ADA/LG1 artifact above. The names are deliberately distinct: the source file is AMPC chemistry, `res_name=\"AMP\"` makes the generated residue type `AMP`, while the unrelated pinned ADA complex uses residue name `LG1`. The next cell reads checked-in AMPC MOL2—with explicit hydrogens, Tripos bond types, an atom-level formal-charge annotation, and `MMFF94_CHARGES` partial charges—and constructs a fresh `LigandPreparation` named `AMP`. It is written for inspection but is not injected into or scored with the ADA complex. No network request or stochastic conformer generation is involved.\n", "\n", "Keep two quantities distinct: AMPC's molecular formal charge is −1 from the source molecular graph, while its per-atom MMFF94 partial charges should sum to approximately −1 at the MOL2 text precision. TMol's generic `properties.protonation.protonation_state` string is residue-alternative metadata used by packing; a value such as `\"neutral\"` is **not** a molecular formal-charge calculation and must not override the source chemistry.\n", "\n", "TMol does not infer trustworthy bond orders or charges from protein–ligand Cartesian coordinates. Preparation starts from authoritative ligand chemistry, then produces the residue type, charge map, and cartbonded parameters needed for scoring." ] }, { "cell_type": "code", "execution_count": 6, "id": "ddf77e5a", "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "
source_atomgenerated_atomelementsource_Tripos_subtypesource_formal_chargesource_MMFF94_partial_chargegenerated_TMol_atom_typegenerated_partial_charge
O1O1O20-0.8167OG2-0.8167
S1S1S301.3448SG31.3448
O2O2O20-0.8167OG2-0.8167
O3O3Oco2-1-0.8167OG2-0.8167
C1C1C300.1052CS20.1052
C2C2C300.0000CS20.0000
C3C3C300.2300CS20.2300
S2S2S30-0.2860Ssl-0.2860
C4C4Car00.4341CR0.4341
N1N1Nar0-0.5653Nim-0.5653
(17 more rows not shown)
\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "
pipeline_roleresidue_nameatomsbondstorsionsmolecular_formal_charge_from_source_graphformal_charge_provenancesource_partial_charge_sumgenerated_partial_charge_sumpartial_charge_modelpartial_charge_provenancegeneric_protonation_state_metadatametadata_interpretation
independent AMPC MOL2 preparation demoAMP27285-1MOL2 UNITY_ATOM_ATTR charge -1 / parsed source graph-1.0001-1.0001MMFF94_CHARGESchecked-in ampc_1.mol2 atom recordsneutralnot a molecular formal charge
\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Generated AMPC portable parameters (not used for ADA): /tmp/tmol-ligand-tutorial-1nzaecoz/AMPC.generated.tmol\n" ] } ], "source": [ "mol2_text = ligand_mol2.read_text()\n", "assert \"MMFF94_CHARGES\" in mol2_text.splitlines()[:10]\n", "assert \"charge -1\" in mol2_text\n", "\n", "mol2_info = nonstandard_residue_info_from_mol2(ligand_mol2, res_name=\"AMP\")\n", "if mol2_info.partial_charges is None:\n", " raise RuntimeError(\"AMPC MOL2 did not expose authoritative partial charges\")\n", "\n", "# Preserve the authoritative MOL2 records before preparation. Hydrogen names may\n", "# be regenerated, while charge transfer remains index-based.\n", "source_atom_names = tuple(mol2_info.atom_names)\n", "source_elements = tuple(mol2_info.elements)\n", "source_subtypes = tuple(mol2_info.atom_array.tmol_source_subtype)\n", "source_partial_charges = dict(mol2_info.partial_charges)\n", "\n", "generated_preparation = prepare_single_ligand(mol2_info)\n", "generated_tmol_path = work_dir / \"AMPC.generated.tmol\"\n", "write_params_file(generated_preparation, generated_tmol_path, format=\"tmol\")\n", "\n", "def mol2_unity_atom_formal_charges(text, atom_names):\n", " \"\"\"Read explicit per-atom formal charges from a TRIPOS MOL2 block.\"\"\"\n", " lines = [line.strip() for line in text.splitlines()]\n", " try:\n", " line_index = lines.index(\"@UNITY_ATOM_ATTR\") + 1\n", " except ValueError as error:\n", " raise RuntimeError(\"MOL2 has no UNITY_ATOM_ATTR formal charges\") from error\n", " charges = {name: 0 for name in atom_names}\n", " while line_index < len(lines) and not lines[line_index].startswith(\"@\"):\n", " if not lines[line_index]:\n", " line_index += 1\n", " continue\n", " atom_id, attribute_count = map(\n", " int, lines[line_index].split()[:2]\n", " )\n", " line_index += 1\n", " for _ in range(attribute_count):\n", " attribute, value = lines[line_index].split()[:2]\n", " line_index += 1\n", " if attribute == \"charge\":\n", " charges[atom_names[atom_id - 1]] = int(value)\n", " return charges\n", "\n", "\n", "source_formal_charges = mol2_unity_atom_formal_charges(\n", " mol2_text, source_atom_names\n", ")\n", "source_formal_charge = sum(source_formal_charges.values())\n", "source_partial_charge_sum = float(sum(source_partial_charges.values()))\n", "generated_partial_charge_sum = float(\n", " sum(generated_preparation.partial_charges.values())\n", ")\n", "generic_protonation_metadata = (\n", " generated_preparation.residue_type.properties.protonation.protonation_state\n", ")\n", "assert source_formal_charge == -1\n", "assert np.isclose(source_partial_charge_sum, -1.0, atol=5e-4)\n", "assert np.isclose(\n", " generated_partial_charge_sum, source_partial_charge_sum, atol=1e-8\n", ")\n", "\n", "generated_atoms = tuple(generated_preparation.residue_type.atoms)\n", "assert len(generated_atoms) == len(source_atom_names)\n", "source_charge_vector = [source_partial_charges[name] for name in source_atom_names]\n", "generated_charge_vector = [\n", " generated_preparation.partial_charges[atom.name] for atom in generated_atoms\n", "]\n", "assert np.allclose(generated_charge_vector, source_charge_vector, atol=1e-8)\n", "ampc_atom_frame = pd.DataFrame(\n", " [\n", " {\n", " \"source_atom\": source_name,\n", " \"generated_atom\": generated_atom.name,\n", " \"element\": element,\n", " \"source_Tripos_subtype\": subtype,\n", " \"source_formal_charge\": source_formal_charges[source_name],\n", " \"source_MMFF94_partial_charge\": source_partial_charges[source_name],\n", " \"generated_TMol_atom_type\": generated_atom.atom_type,\n", " \"generated_partial_charge\": generated_preparation.partial_charges[\n", " generated_atom.name\n", " ],\n", " }\n", " for source_name, element, subtype, generated_atom in zip(\n", " source_atom_names,\n", " source_elements,\n", " source_subtypes,\n", " generated_atoms,\n", " )\n", " ]\n", ")\n", "show_table(ampc_atom_frame)\n", "\n", "preparation_frame = pd.DataFrame(\n", " [\n", " {\n", " \"pipeline_role\": \"independent AMPC MOL2 preparation demo\",\n", " \"residue_name\": generated_preparation.residue_type.name3,\n", " \"atoms\": len(generated_preparation.residue_type.atoms),\n", " \"bonds\": len(generated_preparation.residue_type.bonds),\n", " \"torsions\": len(generated_preparation.residue_type.torsions),\n", " \"molecular_formal_charge_from_source_graph\": source_formal_charge,\n", " \"formal_charge_provenance\": (\n", " \"MOL2 UNITY_ATOM_ATTR charge -1 / parsed source graph\"\n", " ),\n", " \"source_partial_charge_sum\": source_partial_charge_sum,\n", " \"generated_partial_charge_sum\": generated_partial_charge_sum,\n", " \"partial_charge_model\": \"MMFF94_CHARGES\",\n", " \"partial_charge_provenance\": \"checked-in ampc_1.mol2 atom records\",\n", " \"generic_protonation_state_metadata\": generic_protonation_metadata,\n", " \"metadata_interpretation\": \"not a molecular formal charge\",\n", " }\n", " ]\n", ")\n", "show_table(preparation_frame)\n", "print(\"Generated AMPC portable parameters (not used for ADA):\", generated_tmol_path)" ] }, { "cell_type": "markdown", "id": "a3bebd9b", "metadata": {}, "source": [ "## Inject parameters and build one reusable context\n", "\n", "`ParameterDatabase` values are immutable: injection returns a new database and leaves the default database unchanged. Next, derive one structure-independent `BiotitePoseBuildContext` from that extended database and construct the pose with `context=context`. This ensures that pose building and scoring share the same ligand definitions, charges, and cartbonded parameters when the same extended database is passed through the context.\n", "\n", "**Expected observations.** The extended database has one additional ligand residue, the original database is unchanged, and the build context holds the exact extended database object. If pose construction reports an unknown `LG1`, the parameter file and build context were not threaded through the same workflow." ] }, { "cell_type": "code", "execution_count": 7, "id": "7eff981f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "default residues: 142\n", "extended residues: 143\n", "context reuses extended database: True\n" ] } ], "source": [ "base_database = ParameterDatabase.get_default()\n", "extended_database = inject_params_file(base_database, tmol_path)\n", "\n", "complex_array = biotite.structure.io.load_structure(\n", " str(complex_cif), model=1, include_bonds=True\n", ")\n", "if isinstance(complex_array, struc.AtomArrayStack):\n", " complex_array = complex_array[0]\n", "\n", "pose_diagnostics = StringIO()\n", "try:\n", " with redirect_stdout(pose_diagnostics), redirect_stderr(pose_diagnostics):\n", " context = build_context_from_biotite(\n", " complex_array,\n", " device,\n", " param_db=extended_database,\n", " prepare_ligands=False,\n", " )\n", " pose_stack = pose_stack_from_biotite(\n", " complex_array,\n", " device,\n", " context=context,\n", " no_optH=True,\n", " )\n", "except Exception:\n", " print(pose_diagnostics.getvalue())\n", " raise\n", "score_function = beta2016_score_function(\n", " device, param_db=context.parameter_database\n", ")\n", "\n", "print(\"default residues:\", len(base_database.chemical.residues))\n", "print(\"extended residues:\", len(extended_database.chemical.residues))\n", "print(\"context reuses extended database:\", context.parameter_database is extended_database)" ] }, { "cell_type": "markdown", "id": "fb594213", "metadata": {}, "source": [ "## AtomArray ligand and pocket queries\n", "\n", "Biotite annotations are NumPy arrays, so ligand and pocket selections require no query-language extension. This descriptive pocket identifies protein residues with at least one heavy atom within 4.5 Å of any ligand heavy atom, then selects every atom in those complete residues. Selection stays in the rich input representation before conversion to TMol block masks.\n", "\n", "Keep this cutoff distinct from the later protocol scopes: 4.5 Å selects complete residues for the compact pocket pose; packing uses TMol's expanded-sphere block adjacency with a 5 Å additive constant; minimization moves ligand atoms and protein side-chain atoms whose atom-level distance from the ligand is at most 5 Å.\n", "\n", "**Expected observations.** The ligand card should isolate one `LG1` residue; the 4.5 Å pocket card should include nearby protein atoms but exclude the ligand. An empty ligand selection usually means the CIF residue name and prepared `.tmol` `name3` disagree." ] }, { "cell_type": "code", "execution_count": 8, "id": "6d3c10e7", "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", "
\n", " \n", " \n", " \n", " \n", "\n", "\n", "
selectionatomsresidues
ligand361
4.5 Å pocket27017
\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
\n", "
\n", "
\n", " pick a selection · drag to rotate · scroll to zoom · click a highlighted atom to label it\n", "
\n", "
\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "[('', 17, 'HIS'),\n", " ('', 19, 'ASP'),\n", " ('', 58, 'LEU'),\n", " ('', 61, 'PHE'),\n", " ('', 62, 'LEU'),\n", " ('', 65, 'PHE'),\n", " ('', 66, 'ASP'),\n", " ('', 101, 'ARG'),\n", " ('', 102, 'TYR'),\n", " ('', 103, 'SER')]" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ligand_query = complex_array.res_name == LIGAND_RES_NAME\n", "ligand_atoms = complex_array[ligand_query]\n", "if ligand_atoms.array_length() == 0:\n", " raise RuntimeError(\n", " f\"No {LIGAND_RES_NAME} atoms found; check CIF and parameter residue names\"\n", " )\n", "\n", "ligand_heavy_query = ligand_query & (complex_array.element != \"H\")\n", "ligand_heavy_coords = complex_array.coord[ligand_heavy_query]\n", "all_to_ligand = complex_array.coord[:, None, :] - ligand_heavy_coords[None, :, :]\n", "nearest_ligand_distance = np.linalg.norm(all_to_ligand, axis=-1).min(axis=1)\n", "pocket_contact_query = (\n", " (~ligand_query)\n", " & (complex_array.element != \"H\")\n", " & (nearest_ligand_distance <= 4.5)\n", ")\n", "pocket_residues = sorted(\n", " {\n", " (str(chain), int(resid), str(name))\n", " for chain, resid, name in zip(\n", " complex_array.chain_id[pocket_contact_query],\n", " complex_array.res_id[pocket_contact_query],\n", " complex_array.res_name[pocket_contact_query],\n", " )\n", " }\n", ")\n", "pocket_query = np.zeros(complex_array.array_length(), dtype=bool)\n", "for chain, resid, name in pocket_residues:\n", " pocket_query |= (\n", " (complex_array.chain_id == chain)\n", " & (complex_array.res_id == resid)\n", " & (complex_array.res_name == name)\n", " )\n", "pocket_atoms = complex_array[pocket_query]\n", "selection_table = pd.DataFrame(\n", " [\n", " {\"selection\": \"ligand\", \"atoms\": ligand_atoms.array_length(), \"residues\": 1},\n", " {\"selection\": \"4.5 Å pocket\", \"atoms\": pocket_atoms.array_length(), \"residues\": len(pocket_residues)},\n", " ]\n", ")\n", "show_table(selection_table)\n", "display(\n", " tmol.selection_gallery(\n", " complex_array,\n", " {\"ligand\": ligand_query, \"4.5 Å pocket\": pocket_query},\n", " )\n", ")\n", "pocket_residues[:10]" ] }, { "cell_type": "markdown", "id": "23d8be00", "metadata": {}, "source": [ "## Score the ligand–protein interaction\n", "\n", "Select the `LG1` block from the weighted block-pair matrix, then sum both ligand→protein and protein→ligand orientations. The result is the score-function interaction assigned between the ligand and protein in one bound complex, with no separated state, reorganization, or solvent correction; it is explicitly **not a ddG or binding free energy**.\n", "\n", "**Expected observations.** The interaction value must be finite and the viewer should center on `LG1`. The viewer converts coordinates to PDB text for display, so ligand bonds are a geometry-oriented visualization—the CIF/`.tmol` data remain authoritative for chemistry." ] }, { "cell_type": "code", "execution_count": 9, "id": "0f8b3e76", "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", "
\n", " \n", " \n", " \n", "\n", "
metricvalue
weighted ligand–protein block-pair interaction12.211264
\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/3dmoljs_load.v0": "
\n

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n
\n", "text/html": [ "
\n", "

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n", "
\n", "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "ligand_blocks = ligand_block_mask(pose_stack)\n", "real_blocks = pose_stack.block_type_ind >= 0\n", "protein_blocks = real_blocks & ~ligand_blocks\n", "\n", "block_pair_scorer = score_function.render_block_pair_scoring_module(pose_stack)\n", "block_pair_scores = block_pair_scorer(pose_stack.coords)\n", "interaction_score = (\n", " block_pair_scores[0]\n", " * ligand_blocks[0, :, None]\n", " * protein_blocks[0, None, :]\n", ").sum() + (\n", " block_pair_scores[0]\n", " * protein_blocks[0, :, None]\n", " * ligand_blocks[0, None, :]\n", ").sum()\n", "\n", "show_table(\n", " pd.DataFrame(\n", " [\n", " {\n", " \"metric\": \"weighted ligand–protein block-pair interaction\",\n", " \"value\": float(interaction_score.detach().cpu()),\n", " }\n", " ]\n", " )\n", ")\n", "\n", "try:\n", " viewer = tmol.view(pose_stack, zoom_to={\"resn\": LIGAND_RES_NAME})\n", " viewer.setStyle(\n", " {\"resn\": LIGAND_RES_NAME},\n", " {\"stick\": {\"colorscheme\": \"cyanCarbon\", \"radius\": 0.22}},\n", " )\n", " viewer.show()\n", "except ImportError:\n", " print(\"Install py3Dmol for the interactive ligand-pocket view.\")" ] }, { "cell_type": "markdown", "id": "07f90929", "metadata": {}, "source": [ "## Refine the ligand pocket\n", "\n", "The parameter comparison and one-complex score above establish the chemistry and accounting. This final section asks a separate local-optimization question. Packing samples protein side-chain rotamers only: the helper registers Dunbrack, fixed-amino-acid-chi, and input-conformation samplers, but no ligand-chi sampler. Although `LG1` declares chi torsions, it stays in its input conformation during packing and moves only during the following Cartesian minimization.\n", "\n", "Three distance rules have different jobs:\n", "\n", "1. **4.5 Å atom-to-ligand cutoff:** selects complete protein residues for the compact pocket pose.\n", "2. **5 Å expanded-sphere adjacency constant:** packing includes blocks whose centroid distance is less than the sum of both block radii plus 5 Å. This is not an atom-to-atom 5 Å cutoff.\n", "3. **5 Å atom-level minimization shell:** minimization moves every ligand atom plus protein side-chain atoms within 5 Å of any ligand atom.\n", "\n", "Despite its historical name, `calculate_block_pair_ddg()` returns the weighted cross-mask block-pair interaction from one complex. The purple viewer and scope table expose the 5 Å minimization-shell preview on the input pocket, and the table separately reports the packing scope. The helper repacks first and then recomputes its atom-level 5 Å shell on the packed coordinates, so exact minimization membership can change if packing moves an atom across the cutoff.\n", "\n", "> **Where “ddG” comes from.** Rosetta commonly uses score differences as computational $\\Delta G$ or $\\Delta\\Delta G$ proxies: [InterfaceAnalyzer](https://docs.rosettacommons.org/docs/latest/application_documentation/analysis/interface-analyzer) compares a bound complex with separated partners, while [Cartesian ddG](https://github.com/RosettaCommons/documentation/blob/master/cartesian-ddG.md) compares independently optimized mutant and wild-type models. These are score-function differences in Rosetta energy units, not direct thermodynamic free-energy calculations. TMol's historical `calculate_block_pair_ddg()` name is looser still: it sums cross-mask block-pair terms in one complex and performs no state subtraction.\n", "\n", "The result table therefore labels the computed quantity as a ligand–protein interaction score, and the term table shows which weighted components changed." ] }, { "cell_type": "code", "execution_count": 10, "id": "24d64779", "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "
complete_protein_residues_selected_at_4.5Ablocks_in_5A_expanded_sphere_pack_scopemovable_ligand_atomsprotein_sidechain_atoms_in_input_pose_5A_min_shellinput_pose_min_shell_atoms_total
171836133169
\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/3dmoljs_load.v0": "
\n

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n
\n", "text/html": [ "
\n", "

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n", "
\n", "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "
termbeforeafterdelta
1fa_ljrep35.1767882.131725-33.045063
0fa_ljatr-28.486050-21.5977786.888271
17lk_ball6.5594133.215152-3.344261
2fa_lk8.6105086.263557-2.346951
16lk_ball_iso-6.101487-4.2529601.848527
3fa_elec-0.514678-1.848817-1.334139
4hbond-1.0810310.0000001.081031
19lk_bridge_uncpl-0.051930-0.0063250.045605
18lk_bridge-0.011995-0.0015980.010397
7cart_torsions0.0000000.0000000.000000
\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "image/png": "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4sWLFwAAIQTWrVuHzp07Q1dXFy9fvoQQQm1plZ+fX4nHnu3p06cIDw8HABgbG6N3796oW7cu7t+/DwcHBzRs2BBr165Vq793715069at1GIiIiIiInrdWz1jkW358uUQQqBDhw4ICgpCzZo1C92nYsWKeO+99+Dj44NLly7hwoULOHbsGADA1tYWvXr1Qt++fdG/f3+EhYXh7Nmz0NfXL5X409LS0L17d9SsWRM1atTAzZs3cfXqVfj7+wMAVq1ahS5duuDp06eoXr06tm/fjpYtW2LYsGGlEg8RERERUU5vTWKR84Fyw4cPV3vuxIoVK1CrVi2EhIQUmlhk72tvb49jx47B1dUVGzduVFtatG3bNvz111+4e/cu6tSpg19//RU//fSTWts5YyrKQ+9er+vq6opLly4hMDAQt2/fRu/evfHrr7/C0tISANCiRQvcvn0bu3btQmxsLPz9/fkcCyIiIiIqUwohhNB2EKR9CQkJsLCwQHx8PJ/4TURE9I7Lvi7wmrwO+krjUu9v9/wBpd4HFU9RrhHfmhkLTcXExGDFihX5vj5p0iRYWVm99TEQEREREZWkt/7mbSIiIiIiKn3v3IyFlZUVvv7663c+BiIiIiKiksQZCyIiIiIiko2JBRERERERycbEgoiIiIiIZGNiQUREREREsr1zN28TERERkWb+/LoPn29FGuOMBRERERERycbEgoiIiIiIZGNiQUREREREsjGxICIiIiIi2ZhYEBERERGRbEwsiIiIiIhINn7dLBERERHlqf/X26GvNC7WvrvnDyjhaKi844wFERERERHJxsSCiIiIiIhkY2JBRERERESyMbEgIiIiIiLZmFgQEREREZFsTCyIiIiIiEg2JhZERERERCQbEwsiIiIiIpKNiUUx9OrVC3PmzJFdpzSNHTsWkyZN0lr/RERERPRu4ZO3iyEpKQmpqamy65Sm5ORkZGVlaa1/IiIiInq3cMaCiIiIiIhkY2JRTImJiZg4cSJq1qyJatWq4auvvoJKpVKrk5CQgLFjx6J69epwc3PD/Pnz1V5XqVT49ttvUatWLdjb26Nz5874999/1er06tULU6dOLbCvzMxMTJs2DZUrV0adOnUwffp0pKWlld7giYiIiIhyYGJRTH5+fnBzc8PBgwexcuVKLFmyBJs3b85Vx9raGgcOHMCSJUuwYMEC+Pn5Sa/PnTsXP/74I5YuXYrTp0+jbt266NChA548eSLVSUpKwpIlSwrsa/bs2QgICMBvv/2Gv//+G1FRUdi2bVvpHwQiIiIiov+nEEIIbQfxpunUqROMjIywe/duqWzgwIEwNTXF2rVrpTpRUVG4ePGiVOe7777D6tWrER4ejvT0dFhaWmLFihX44IMPAABCCNStWxfdu3fHokWLNOorLS0N1tbW8Pf3x9ChQwEA6enpqFy5Mnr06CHFk1NaWprarEZCQgKcnZ0RHx8Pc3PzkjlQRERE9EZKSEiAhYUFvCavg77SuFht7J4/oISjIm3IPhc0uUbkjEUx1apVS23bxsYG0dHRamUtWrTItf3gwQMkJSUhLCwMKSkpaNmypfS6QqFAq1atcPXqVY37CgsLQ3JyslpfBgYGaNKkSYHxz58/HxYWFtKPs7NzISMmIiIiIsofE4ti0tHJfehyTv7o6uqqbevpvfoSrszMTGRmZqqVvV4n+zVN+sr+5qf8+srPrFmzEB8fL/08evSowPpERERERAVhYlGKXl8GBQChoaGws7NDhQoV4ObmBj09PZw/f16tzvnz51GjRg2N+3B1dYW+vr5aXyqVCpcuXSpwP6VSCXNzc7UfIiIiIqLiYmJRik6cOIHVq1cjMzMTly5dwoIFC/DJJ58AAIyMjDBu3Dh89dVXuH37NjIyMrBixQqcP38eEydO1LgPExMTjBo1Cl9++SXu37+PtLQ0fP3113jw4EFpDYuIiIiIKBcmFqVoyJAh2LZtGywtLdG0aVN0794dM2bMkF5fsGABWrdujYYNG8LY2Bg//vgjtm3bVqQZCwBYtGgRatSoAXd3d1SsWBE3btxAt27dSno4RERERET54rdCFUNycjJ0dXVhaGgolaWmpkKlUsHExCRXnaysLAgh8r3vQaVSIS0tDUZGRsXqK1t6ejr09fWhUCiQkpIChUKRZ5t5Kcod/0RERPR247dCUbaiXCMWfIcv5SnnBT2AXBfwr9fJeWN1Tjo6OvkmAJr0lc3AwED6f2Pj4v0RICIiIiIqDi6FIiIiIiIi2ZhYEBERERGRbEwsiIiIiIhINiYWREREREQkGxMLIiIiIiKSjYkFERERERHJxsSCiIiIiIhk43MsiIiIiChPf37dhw/OJY1xxoKIiIiIiGRjYkFERERERLIxsSAiIiIiItmYWBARERERkWxMLIiIiIiISDYmFkREREREJBsTCyIiIiIiko2JBRERERERycbEgoiIiIiIZGNiQUREREREsjGxICIiIiIi2ZhYEBERERGRbEwsiIiIiIhINiYWREREREQkW5ESiyNHjuDMmTPS9oEDB3D+/PlidSxn35xx5OXYsWM4efJksdp/G2hyjIiIiIiISopeUSqvWLECTk5O8PT0BAAsXLgQHh4eaNKkSZE7lrNvzjjy8vPPP8PQ0BDvvfdekdt/G2hyjIiIiIiISkqREovyokOHDrCystJ2GERERERE9P9KNLEQQmDr1q2wtbVF+/btNdrnxYsXOHfuHLKystCxY0cYGxtLrx04cADW1tZwcnLCiRMnYGJiAi8vL9StWxcmJiZq7cTFxeHIkSMwMzND06ZN8+wrNjYWR44cgbm5OZo2bYorV65AV1dXbVZDpVLh5MmTePz4MapWrYqmTZtCoVBoNJa///4bbm5uqFOnjlQWEhICfX19qY/sMVWpUgVnz55FVlYWWrduDXNzc7W2VCoVTp8+jUePHqFevXqoXbt2ruPi7Oyc77EjIiIiIipLJZZYZGRkYOTIkbh48SIOHDig0T5BQUH4/fff0aRJE9y4cQMZGRkICQmBs7MzgFfLpRITExEZGYnGjRvDw8MDXl5euZb5XL58GR07dkSlSpXg7OyMK1euwNzcHM2aNZP6unDhAjp37ozKlSvDyckJV69ehZmZGZo1ayZd9D958gQ9evRAeno66tSpg4sXL8Le3h579+7NdeGfl6+++gpDhw5VSyz8/Pxgamoq9bFw4UIkJSUhOjoa9evXx+3btxEXF4czZ87A0dERAPD48WP4+PggKioKTZs2xbx589C1a1csXrxY7bg8ffo032NHRERERFSWSiSxSElJQd++fRETE4Njx45pvEzp0qVLuHz5MqpXr4709HR07NgRM2fOxO+//y7VuX37Nq5duyZddOdl4sSJ6NChAwICAqBQKHD06FG0bdtWLbGYOHEiOnfujM2bN0OhUODEiRNo1aqVWp3Ro0fD09MTfn5+UCgUyMzMRKdOnTBv3jwsWrSoGEcmbw8ePMDly5dha2uLrKwsNGzYEH5+fpg3bx4AYPjw4TA3N8fx48elmZn9+/ertaHJsStIWloa0tLSpO2EhIQSGh0RERERvYtkJxaxsbHo3LkzTExMcOjQIZiammq8r7e3N6pXrw4AMDAwwCeffIKRI0dCCCEtP+rZs2eBSUVkZCSOHj2KkydPSvu0adMGHh4eUp0XL17g5MmTanVatmypllRERETgwIEDmDNnDn7//XcIISCEgIODA4KDgzUekyZ8fX1ha2sLANDV1UXz5s1x+/ZtAMDTp08RFBSEgwcPqi338vLyUmtDk2NXkPnz52POnDklNSQiIiIiesfJTiw2bdqErKws3Lx5s0hJBQBUrlxZbdvFxQUvX75EZGQk7OzsAAAVK1YssI2HDx/m21a2R48e5Vnn9e3sdq5evYqwsDCpXKlUom3bthqNR1MVKlRQ21YqldLsQXas7u7uBbahybEryKxZszBlyhRpOyEhgcuoiIiIiKjYZCcW48ePR3h4OLp27YqgoCA4ODhovG9MTEyubV1dXVhaWkplhX36nv3Jf0xMDCpVqqTWVvYFvI2NTb51LCwsAED676RJk9CqVSuNx/A6XV1dqFQqtbLU1NQiJVzZY4+MjESVKlXyrafJsSuIUqmEUqnUOC4iIiIiooLIfvK2np4eAgICUKdOHbRv3x4REREa77tv3z6kpKRI23/++SdatGgBfX19jdtwcnKCq6srtm/fLpU9efJE7eF4zs7OcHFxwV9//SWVPX/+XK1O9erV4ebmhp9++ilXHw8ePNAolkqVKuHWrVvSdlJSUpEfUletWjVUrVoV69atUyt/+vSp2nZJHDsiIiIiopJSIjdvZycXAwYMQLt27RAcHFzoEibg1WxE27ZtMXToUFy8eBF//PEHjhw5UqS+dXR0sHDhQgwaNAixsbFwcXHBmjVr1L7FSUdHB99//z2GDRuG2NhYVK5cGb/88gvMzMykGRGFQoH169fD29sbXl5e8Pb2RmxsLP755x/069cPn3/+eaGxjB49GgMGDICtrS0cHBykezWKQqFQYO3atejRowdevHiBdu3a4datW3j48CH+/vtvtXpyjx0RERERUUkpUmKR88F0Xl5e0nIdfX19bNmyBV9++SV+++03zJgxo8C2vLy8MHbsWJiYmCA4OBiWlpY4c+YMGjVqlGf7BcXRt29f2NnZYevWrYiKisLq1atx48YNtU/vBw4ciIoVK2L79u2IioqCv78/vv32W7UEpFWrVrh58yY2bdokfRPVTz/9pHYjeEF69eqFvXv3Yu/evYiMjMRPP/2E8+fP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uwzs2LED8fHxePDgAXR0mMsRERER0duHiUUpu3nzJk6dOgUTExOcPHkSRkZGaNKkCe7du4dnz54BAKytreHm5gYDA4MS7fvChQvQ19dHvXr1SrRdIiIiIqKcmFiUst9//x3//PMPoqOjMXPmTDg5OSEgIAA7duzAzp07AQDPnz9HXFwc1q9fD29v7xLp97///S/WrFmD/fv3l0h7REREREQFYWJRyubNmwcTExNs27YNx48fl8qnTp2KqVOnStvr1q3DyJEjER4eDhMTk2L3p1Kp8PHHH2P//v04fvw43N3d86yXlpaGtLQ0aTshIaHYfRIRERERccG/FqWnp+P27ds4efIk3N3dERsbi2vXrslqb9CgQTh+/HiBSQUAzJ8/HxYWFtKPs7NzsfslIiIiIuKMhZZs374dEyZMgIGBARwdHaGvrw8hBCIiIordpr+/P1QqFe7cuQNHR8cC686aNQtTpkyRthMSEphcEBEREVGxMbHQgoyMDAwfPhxLly7FRx99BODVbIORkRFUKlWx2x05ciRCQ0MxZMgQ/PPPPwUuqVIqlVAqlcXui4iIiIjodVwKpQUvXrxASkoK2rdvL5Xt379fVlIBAObm5ti/fz8yMjLQvXt3JCcnyw2ViIiIiEgjTCy0wNHRETVq1MCnn36K/fv3w8/PDx999FGJPOMiO7lIS0tjckFEREREZYaJRRlwdnZGw4YNpW2FQoH9+/fD2dkZCxcuxIkTJ7B9+3Z07NgR1tbWGrVZu3ZtuLm5Sdv16tVDlSpVAPwvubCxscH8+fNLcihERERERHlSCCGEtoMg7UtISICFhQXi4+Nhbm6u7XCIiIjeST6ztmg7BABARloK9i8dzesCKtI1Im/eLodevnyJc+fO5fu6h4cHDA0NyzAiIiIiIqKCMbEoh7Kf0p2fLVu2oFKlSmUYERERERFRwZhYlEOVKlVSe0o3EREREVF5x5u3iYiIiIhINiYWREREREQkGxMLIiIiIiKSjYkFERERERHJxudYEAA+x4KIiIj+h9cFlK0o5wJnLIiIiIiISDYmFkREREREJBsTCyIiIiIiko2JBRERERERycbEgoiIiIiIZGNiQUREREREsjGxICIiIiIi2fS0HQARERHR28Rn1hZthyBbRlqKtkOgNxBnLIiIiIiISDYmFkREREREJBsTCyIiIiIiko2JBRER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Y6gwOc+bpKQknDp16p09b17HGQvCN998g+joaHTq1AkWFhY4ffo0Fi5ciJkzZ0oP7Jo/fz6ePXsGLy8vGBkZYdu2bdi7dy927Nih5ejLzqeffgozMzO0adMGSqUSR48exbJly7BixQqpzogRI7BixQr07NkTU6ZMwYULF/D7779Ly6feBV988QVWrVqFZcuWISkpSfr00NbWVlreMmLECLi7u+O9995Deno6Vq1ahcjISHz55ZfaDL1MjRo1Crt378aaNWvw+PFjPH78GADg5OQkJRmzZs1CkyZNMHLkSPTt2xdbt27F48ePC13q8TaJiYnB9evXpeUYN27cgI2NDSpVqgRXV1cAQIsWLTBkyBA0aNAAERER+P7771G1alV88MEH2gy9TGU/sCv7E9PLly8jMzMTVapUkR5w9t1336F79+6oUqUKmjRpgqVLl6Jy5coYNWqUNkPXqunTp0OhUKBDhw7Q0dHBxo0bce7cORw8eFDboWnFhAkTsHr1arz//vsYN24cgoODcejQIYSEhGg7tHKlV69eeO+999CsWTMkJiZi2bJlUCgUuZaYv4sUIvujMHpnCSEQEBCA3bt3IyYmBi4uLhg8eLDaGkIhBLZs2YK///4b8fHxqF69OiZMmIBq1appMfKylZmZifXr1+PAgQNISkqCq6srRo8enesbNKKiorBgwQKEhobC2toa48aNk24MfBeMGjUKd+7cyVXeqVMnaWlHWloafv75ZwQFBUEIgfr162PSpEn5Lrt7G/Xo0QNxcXG5ygcNGoSPP/5Y2r558yYWL16MsLAwVK1aFVOnTpVuGHwXHDt2LM+17v3795eWVEVFReHHH3/E2bNnYWZmhhYtWmD8+PGFfkXt22TXrl1YtGhRrvLx48erLdHLvhcoMjISDRs2xMyZMzW+T+NtlJWVhd9++w379u1DamoqatWqhYkTJ6otS3zXPHnyBAsWLMC1a9fg4OCAiRMnwtPTU9thlStJSUnw8/PDsWPHoK+vj8aNG2PixIkwNzfXdmhax8SCiIiIiIhkezcWxxMRERERUaliYkFERERERLIxsSAiIiIiItmYWBARERERkWxMLIiIiIiISDYmFkREREREJBsTCyIiIiIiko2JBRGRFp04cQLnzp0r0j4hISG4ePFi6QSUhzNnzuDMmTP5vl6cMbwp3uaxlRaVSoWAgABE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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "
stagewhole_pocket_total_scoreligand_protein_interactionRMS_displacement_all_real_atoms_ARMS_displacement_input_pose_5A_mask_atoms_A
input pocket2521.95874014.0995400.0000000.000000
repacked + minimized2327.234863-16.0970441.8394392.231036
\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
\n", " \n", "
\n", " \n", "
\n", "
interaction 14.100
\n", "
\n", "

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n", "
\n", "\n", "
\n", "
\n", "
interaction -16.097; all-real-atom RMS motion 1.839 Å; input-pose 5 Å-mask atom RMS motion 2.231 Å
\n", "
\n", "

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n", "
\n", "\n", "
\n", "
\n", "
\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "refinement_residues = set(pocket_residues)\n", "protein_refinement_query = np.zeros(complex_array.array_length(), dtype=bool)\n", "for chain, resid, name in refinement_residues:\n", " protein_refinement_query |= (\n", " (complex_array.chain_id == chain)\n", " & (complex_array.res_id == resid)\n", " & (complex_array.res_name == name)\n", " )\n", "refinement_array = complex_array[ligand_query | protein_refinement_query]\n", "refinement_diagnostics = StringIO()\n", "try:\n", " with redirect_stdout(refinement_diagnostics), redirect_stderr(refinement_diagnostics):\n", " refinement_context = build_context_from_biotite(\n", " refinement_array,\n", " device,\n", " param_db=extended_database,\n", " prepare_ligands=False,\n", " )\n", " refinement_pose = pose_stack_from_biotite(\n", " refinement_array,\n", " device,\n", " context=refinement_context,\n", " no_optH=True,\n", " )\n", "except Exception:\n", " print(refinement_diagnostics.getvalue())\n", " raise\n", "refinement_sfxn = beta2016_score_function(\n", " device, param_db=refinement_context.parameter_database\n", ")\n", "refinement_ligand_mask = ligand_block_mask(refinement_pose)\n", "movable_coord_mask = build_coord_mask_for_mask_and_interacting_atoms(\n", " refinement_pose, refinement_ligand_mask\n", ")\n", "refinement_centroids, refinement_radii = compute_block_centroids_and_furthest_dist(\n", " refinement_pose\n", ")\n", "packing_adjacency = compute_block_adjacency(\n", " refinement_centroids, refinement_radii, constant=5.0\n", ")\n", "packing_blocks = refinement_ligand_mask | (\n", " refinement_ligand_mask.unsqueeze(2) & packing_adjacency\n", ").any(dim=1)\n", "movable_real_atom_count = int(\n", " (movable_coord_mask & refinement_pose.real_atoms).sum().item()\n", ")\n", "ligand_atom_count = int(\n", " refinement_pose.n_ats_per_block[refinement_ligand_mask].sum().item()\n", ")\n", "show_table(\n", " pd.DataFrame(\n", " [\n", " {\n", " \"complete_protein_residues_selected_at_4.5A\": len(refinement_residues),\n", " \"blocks_in_5A_expanded_sphere_pack_scope\": int(packing_blocks.sum()),\n", " \"movable_ligand_atoms\": ligand_atom_count,\n", " \"protein_sidechain_atoms_in_input_pose_5A_min_shell\": (\n", " movable_real_atom_count - ligand_atom_count\n", " ),\n", " \"input_pose_min_shell_atoms_total\": movable_real_atom_count,\n", " }\n", " ]\n", " )\n", ")\n", "try:\n", " movable_viewer = tmol.view(\n", " refinement_pose,\n", " highlighted=movable_coord_mask[refinement_pose.real_atoms],\n", " highlight_color=\"#7b2cbf\",\n", " )\n", " movable_viewer.show()\n", "except ImportError:\n", " print(\"Install py3Dmol to inspect the movable refinement atoms.\")\n", "\n", "torch.manual_seed(SEED)\n", "interaction_before = calculate_block_pair_ddg(\n", " refinement_pose,\n", " refinement_ligand_mask,\n", " sfxn=refinement_sfxn,\n", " minimize=False,\n", " pack=False,\n", " database=extended_database,\n", ")\n", "interaction_after, refined_pose = calculate_block_pair_ddg(\n", " refinement_pose,\n", " refinement_ligand_mask,\n", " sfxn=refinement_sfxn,\n", " minimize=True,\n", " pack=True,\n", " database=extended_database,\n", " return_pose_stack=True,\n", ")\n", "\n", "\n", "def ligand_protein_scores_by_term(pose):\n", " scorer = refinement_sfxn.render_block_pair_scoring_module(pose)\n", " matrices = scorer(pose.coords, sum_terms=False, apply_weights=True)[:, 0]\n", " ligand = refinement_ligand_mask[0]\n", " protein = (pose.block_type_ind64[0] >= 0) & ~ligand\n", " return matrices[:, ligand][:, :, protein].sum((1, 2)) + matrices[:, protein][\n", " :, :, ligand\n", " ].sum((1, 2))\n", "\n", "\n", "before_by_term = ligand_protein_scores_by_term(refinement_pose)\n", "after_by_term = ligand_protein_scores_by_term(refined_pose)\n", "torch.testing.assert_close(before_by_term.sum().reshape(1), interaction_before)\n", "torch.testing.assert_close(after_by_term.sum().reshape(1), interaction_after)\n", "term_frame = pd.DataFrame(\n", " {\n", " \"term\": [score_type.name for score_type in refinement_sfxn.all_score_types()],\n", " \"before\": before_by_term.detach().cpu().numpy(),\n", " \"after\": after_by_term.detach().cpu().numpy(),\n", " \"delta\": (after_by_term - before_by_term).detach().cpu().numpy(),\n", " }\n", ")\n", "term_frame = term_frame.loc[term_frame[\"delta\"].abs().sort_values(ascending=False).index]\n", "show_table(term_frame.head(10))\n", "plot_terms = term_frame.head(8).sort_values(\"delta\")\n", "fig, ax = plt.subplots(figsize=(8, 4))\n", "ax.barh(plot_terms[\"term\"], plot_terms[\"delta\"], color=\"#4c78a8\")\n", "ax.axvline(0, color=\"black\", linewidth=0.8)\n", "ax.set(\n", " xlabel=\"weighted interaction-score change\",\n", " title=\"Largest ligand–protein term changes after refinement\",\n", ")\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "refinement_total_scorer = refinement_sfxn.render_whole_pose_scoring_module(\n", " refinement_pose\n", ")\n", "refined_total_scorer = refinement_sfxn.render_whole_pose_scoring_module(refined_pose)\n", "total_before = float(refinement_total_scorer(refinement_pose.coords).detach().cpu()[0])\n", "total_after = float(refined_total_scorer(refined_pose.coords).detach().cpu()[0])\n", "real_atoms = refinement_pose.real_atoms & refined_pose.real_atoms\n", "coordinate_delta = refined_pose.coords[real_atoms] - refinement_pose.coords[real_atoms]\n", "all_real_atom_rms = float(\n", " torch.sqrt(torch.mean(torch.sum(coordinate_delta.square(), dim=-1))).detach().cpu()\n", ")\n", "movable_atoms = movable_coord_mask & real_atoms\n", "movable_coordinate_delta = (\n", " refined_pose.coords[movable_atoms] - refinement_pose.coords[movable_atoms]\n", ")\n", "movable_atom_rms = float(\n", " torch.sqrt(torch.mean(torch.sum(movable_coordinate_delta.square(), dim=-1)))\n", " .detach()\n", " .cpu()\n", ")\n", "\n", "# The whole-pocket total contains protein intramolecular and one-body terms as\n", "# well as ligand–protein interactions; it is not an interaction or binding score.\n", "refinement_frame = pd.DataFrame(\n", " [\n", " {\n", " \"stage\": \"input pocket\",\n", " \"whole_pocket_total_score\": total_before,\n", " \"ligand_protein_interaction\": float(interaction_before.detach().cpu()[0]),\n", " \"RMS_displacement_all_real_atoms_A\": 0.0,\n", " \"RMS_displacement_input_pose_5A_mask_atoms_A\": 0.0,\n", " },\n", " {\n", " \"stage\": \"repacked + minimized\",\n", " \"whole_pocket_total_score\": total_after,\n", " \"ligand_protein_interaction\": float(interaction_after.detach().cpu()[0]),\n", " \"RMS_displacement_all_real_atoms_A\": all_real_atom_rms,\n", " \"RMS_displacement_input_pose_5A_mask_atoms_A\": movable_atom_rms,\n", " },\n", " ]\n", ")\n", "show_table(refinement_frame)\n", "display(\n", " tmol.switchable_view(\n", " {\"input pocket\": refinement_pose, \"refined pocket\": refined_pose},\n", " notes={\n", " \"input pocket\": f\"interaction {float(interaction_before.detach().cpu()[0]):.3f}\",\n", " \"refined pocket\": (\n", " f\"interaction {float(interaction_after.detach().cpu()[0]):.3f}; \"\n", " f\"all-real-atom RMS motion {all_real_atom_rms:.3f} Å; \"\n", " f\"input-pose 5 Å-mask atom RMS motion {movable_atom_rms:.3f} Å\"\n", " ),\n", " },\n", " )\n", ")" ] }, { "cell_type": "markdown", "id": "8b5d760b", "metadata": {}, "source": [ "## Rosetta comparison\n", "\n", "Rosetta `.params` files combine residue topology, atom types, charges, internal coordinates, and sampling declarations. TMol `.tmol` files mirror `ParameterDatabase`, carrying distinct chemistry, electrostatics, and cartbonded payloads in one versioned YAML document.\n", "\n", "TMol's current Rosetta-syntax writer and partial reader are inspection aids, not a validated conversion workflow: the writer retains TMol atom types and placeholder Rosetta fields, while the reader cannot reconstruct all charge and cartbonded data. Preserve authoritative source chemistry and use Rosetta-native preparation for Rosetta workflows.\n", "\n", "RosettaLigand and GALigandDock add global docking and protocol layers not demonstrated here. This tutorial covers parameter registration, local pocket repacking and minimization, and one-complex interaction diagnostics. See the [Rosetta-to-TMol crosswalk](rosetta_crosswalk.md) for the broader boundary.\n", "\n", "## Next: test pose sensitivity or add mixed biopolymers\n", "\n", "Continue to [Case Study 10 — Ligand Pose Sensitivity and Local Rescue](10_ligand_pose_sensitivity.ipynb) to score controlled rigid-body decoys in one batch and locally minimize diagnostic states. Continue to [08 — Working with DNA and RNA](08_nucleic_acids.ipynb) for protein–DNA and RNA–ligand systems." ] }, { "cell_type": "markdown", "id": "3af1e347", "metadata": {}, "source": [ "## Exercises\n", "\n", "1. Reload `AMPC.generated.tmol` and verify that AMPC atom names, partial charges, and cartbonded payload survive that `.tmol` round trip. Confirm that its source formal charge and partial-charge sum remain separate concepts. Do not compare it numerically with the chemically unrelated pinned ADA/LG1 artifact as a parity test.\n", "2. Inspect which `CHI` and `PROTON_CHI` declarations survive the experimental Rosetta-syntax write/read comparison. Confirm separately that written `ATOM` charges are not retained and explain why TMol atom types, MM type `X`, and radius 999 still preclude a Rosetta-usability claim.\n", "3. Change the 4.5 Å AtomArray pocket cutoff while leaving the two 5 Å protocol scopes unchanged, then quantify the effect on runtime and refined interaction score.\n", "4. Separate ligand desolvation and protein desolvation contributions in the per-term interaction table.\n", "5. Repeat pocket refinement with several packer seeds and report the interaction-score and structural spread." ] }, { "cell_type": "markdown", "id": "32a676a8", "metadata": {}, "source": [ "## References\n", "\n", "- [Rosetta-to-TMol crosswalk](rosetta_crosswalk.md)\n", "- [Rosetta residue `.params` reference](https://docs.rosettacommons.org/docs/latest/rosetta_basics/file_types/Residue-Params-file)\n", "- [Rosetta ligand-preparation tutorial](https://docs.rosettacommons.org/demos/latest/tutorials/prepare_ligand/prepare_ligand_tutorial)\n", "- [RosettaLigand workshop slides](https://meilerlab.org/wp-content/uploads/2025/11/ligand_docking_presentation.pdf) and [2025 ligand-docking exercise](https://meilerlab.org/wp-content/uploads/2025/03/ligand_docking_tutorial.pdf)\n", "- [Rosetta `molfile_to_params.py`](https://github.com/RosettaCommons/rosetta/blob/main/source/scripts/python/public/molfile_to_params.py)\n", "- [PyRosettaCluster ligand-parameter workflow](https://nbviewer.org/github/RosettaCommons/PyRosetta.notebooks/blob/master/notebooks/16.10-PyRosettaCluster-Ligand-params.ipynb)\n", "- [TMol ligand workflow](../user_guide/ligands.md)" ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "T4" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }