{ "cells": [ { "cell_type": "markdown", "id": "9b71d06a", "metadata": {}, "source": [ "# TMol How-To Guide — Ligands and NCAAs\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/RosettaCommons/tmol/blob/main/docs/tutorial/04_ligands_and_ncaas.ipynb)" ] }, { "cell_type": "markdown", "id": "5a8eb77f", "metadata": {}, "source": [ "## 4. Ligands and NCAAs" ] }, { "cell_type": "markdown", "id": "46535788", "metadata": {}, "source": [ "Small-molecule ligands and non-canonical amino acids both go through the same preparation pipeline (`prepare_ligands=True`) seen briefly in section 1. This notebook goes further: reusing prepared chemistry, saving/loading it, splitting a ligand into independently-scored fragments, and the handful of NCAAs that don't need the pipeline at all." ] }, { "cell_type": "markdown", "id": "c8b6f5f8", "metadata": {}, "source": [ "### Setup" ] }, { "cell_type": "markdown", "id": "c8469669", "metadata": {}, "source": [ "Wheel install, imports, seeding, and device selection live in [notebook_setup.py](https://github.com/RosettaCommons/tmol/blob/main/docs/tutorial/notebook_setup.py)." ] }, { "cell_type": "code", "execution_count": 1, "id": "5a1647c7", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:48:56.194765Z", "iopub.status.busy": "2026-10-20T00:48:56.194407Z", "iopub.status.idle": "2026-10-20T00:49:07.074108Z", "shell.execute_reply": "2026-10-20T00:49:07.060516Z" }, "tags": [ "collapse-code" ] }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Environment variable CCD_MIRROR_PATH not set. Will not be able to use function requiring this variable. To set it you may:\n", " (1) add the line 'export VAR_NAME=path/to/variable' to your .bashrc or .zshrc file\n", " (2) set it in your current shell with 'export VAR_NAME=path/to/variable'\n", " (3) write it to a .env file in the root of the atomworks.io repository\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "Environment variable PDB_MIRROR_PATH not set. Will not be able to use function requiring this variable. To set it you may:\n", " (1) add the line 'export VAR_NAME=path/to/variable' to your .bashrc or .zshrc file\n", " (2) set it in your current shell with 'export VAR_NAME=path/to/variable'\n", " (3) write it to a .env file in the root of the atomworks.io repository\n" ] } ], "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 urlretrieve\n", "\n", " urlretrieve(\n", " \"https://raw.githubusercontent.com/RosettaCommons/tmol/\"\n", " \"main/docs/tutorial/notebook_setup.py\",\n", " \"notebook_setup.py\",\n", " )\n", "\n", "from notebook_setup import * # noqa: F401,F403\n", "\n", "device, param_db = setup()\n" ] }, { "cell_type": "markdown", "id": "639d463f", "metadata": {}, "source": [ "### Ligands" ] }, { "cell_type": "markdown", "id": "3c3ffa1c", "metadata": {}, "source": [ "#### Automatic pipeline" ] }, { "cell_type": "markdown", "id": "1d526f6a", "metadata": {}, "source": [ "We return to [3PTB](https://www.rcsb.org/structure/3PTB) (trypsin + the small-molecule inhibitor benzamidine, BEN) from section 1 and go further: relax the whole complex, then read off a ligand-protein interaction energy and the ligand's own internal strain." ] }, { "cell_type": "code", "execution_count": 2, "id": "8aabef6f", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:49:07.078961Z", "iopub.status.busy": "2026-10-20T00:49:07.078448Z", "iopub.status.idle": "2026-10-20T00:52:48.988557Z", "shell.execute_reply": "2026-10-20T00:52:48.987254Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "==============================\n", "*** Open Babel Warning in WriteMolecule\n", " No 2D or 3D coordinates exist. Stereochemical information will be stored using an Open Babel extension. To generate 2D or 3D coordinates instead use --gen2D or --gen3D.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "n_poses=1, max_n_blocks=225\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/tmol/tmol/tmol/io/_pose_stack_from_biotite.py:1401: UserWarning: Renumbering chain A of the output AtomArray: biotite's CIF writer does not support numbering that decreases within a chain\n", " warnings.warn(\n", "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n" ] }, { "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" }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/tmol/tmol/tmol/io/_pose_stack_from_biotite.py:1401: UserWarning: Renumbering chain A of the output AtomArray: biotite's CIF writer does not support numbering that decreases within a chain\n", " warnings.warn(\n", "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n", "/home/runner/work/tmol/tmol/tmol/io/_pose_stack_from_biotite.py:1401: UserWarning: Renumbering chain A of the output AtomArray: biotite's CIF writer does not support numbering that decreases within a chain\n", " warnings.warn(\n", "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n", "/home/runner/work/tmol/tmol/tmol/io/_pose_stack_from_biotite.py:1401: UserWarning: Renumbering chain A of the output AtomArray: biotite's CIF writer does not support numbering that decreases within a chain\n", " warnings.warn(\n", "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n", "/home/runner/work/tmol/tmol/tmol/io/_pose_stack_from_biotite.py:1401: UserWarning: Renumbering chain A of the output AtomArray: biotite's CIF writer does not support numbering that decreases within a chain\n", " warnings.warn(\n", "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n" ] }, { "data": { "text/html": [ "\n", "
\n", " \n", "
\n", " \n", "
\n", "
pre-relax (3PTB, trypsin + BEN)
\n", "
\n", "

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

\n", "
\n", "\n", "
\n", "
\n", "
post fast_relax: n_poses=1, max_n_blocks=225
\n", "
\n", "

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

\n", "
\n", "\n", "
\n", "
\n", "
original vs. relaxed
\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": [ "from tmol.io import atom_array_from_file, pose_stack_from_atom_array, build_context_from_atom_array\n", "\n", "ligand_cif_text = fetch_cif(\"3PTB\")\n", "ligand_structure = atom_array_from_file(io.StringIO(ligand_cif_text))\n", "pose_stack, build_context = pose_stack_from_atom_array(\n", " ligand_structure, device, prepare_ligands=True, param_db=param_db, return_context=True\n", ")\n", "score_function = beta2016_score_function(device, param_db=build_context.parameter_database)\n", "fold_forest = tmol.FoldForest.reasonable_fold_forest(pose_stack)\n", "\n", "torch.manual_seed(SEED)\n", "relaxed = tmol.fast_relax(\n", " pose_stack.clone(), score_function, PackerPalette(), tmol.CartesianMoveMap(), fold_forest, num_repeats=1\n", ")\n", "print(f\"n_poses={relaxed.n_poses}, max_n_blocks={relaxed.max_n_blocks}\")\n", "\n", "ligand_atom_mask = res_mask_to_coord_mask(pose_stack, tmol.get_ligand_mask(pose_stack))\n", "viewer = tmol.view(pose_stack, highlighted=ligand_atom_mask[pose_stack.real_atoms])\n", "viewer.show()\n", "\n", "display(tmol.overlay_view(\n", " {\"original\": pose_stack, \"relaxed\": relaxed},\n", " notes={\n", " \"original\": \"pre-relax (3PTB, trypsin + BEN)\",\n", " \"relaxed\": f\"post fast_relax: n_poses={relaxed.n_poses}, max_n_blocks={relaxed.max_n_blocks}\",\n", " },\n", "))\n" ] }, { "cell_type": "markdown", "id": "a0d3bf8d", "metadata": {}, "source": [ "`tmol.ops.calculate_block_pair_ddg` sums the cross-mask block-pair interaction between a residue mask and everything else -- here, between the ligand block and the rest of the complex. (Despite its name it reports that raw interaction, not a true bound/unbound ddG.)" ] }, { "cell_type": "code", "execution_count": 3, "id": "bedc77c9", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:52:49.005466Z", "iopub.status.busy": "2026-10-20T00:52:49.004777Z", "iopub.status.idle": "2026-10-20T00:52:49.092642Z", "shell.execute_reply": "2026-10-20T00:52:49.089423Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ligand-protein interaction energy: -27.49\n" ] } ], "source": [ "ligand_mask = torch.zeros((1, relaxed.max_n_blocks), dtype=torch.bool)\n", "for block in range(relaxed.max_n_blocks):\n", " bt_ind = int(relaxed.block_type_ind64[0, block])\n", " if bt_ind >= 0 and relaxed.packed_block_types.active_block_types[bt_ind].name3 == \"BEN\":\n", " ligand_mask[0, block] = True\n", "\n", "interaction = calculate_block_pair_ddg(relaxed, ligand_mask, sfxn=score_function, minimize=False, pack=False)\n", "print(f\"ligand-protein interaction energy: {float(interaction[0]):.2f}\")\n" ] }, { "cell_type": "markdown", "id": "b8891356", "metadata": {}, "source": [ "`gen_torsions` is the generic-atom-type torsional term that shows up in a per-term score breakdown (see section 2) -- it fills in wherever a residue lacks dedicated per-residue cartbonded parameters (canonical amino acids score an exact zero here), which in practice means ligands and other novel chemistry. It's a reasonable proxy for \"ligand strain.\"" ] }, { "cell_type": "code", "execution_count": 4, "id": "ca5af52a", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:52:49.098674Z", "iopub.status.busy": "2026-10-20T00:52:49.098399Z", "iopub.status.idle": "2026-10-20T00:52:49.144359Z", "shell.execute_reply": "2026-10-20T00:52:49.142517Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "gen_torsions (ligand strain): 0.61\n" ] } ], "source": [ "relaxed_scorer = score_function.render_whole_pose_scoring_module(relaxed)\n", "weighted_terms = relaxed_scorer(relaxed.coords, sum_terms=False, apply_weights=True)\n", "gen_torsions_index = score_function.all_score_types().index(ScoreType.gen_torsions)\n", "print(f\"gen_torsions (ligand strain): {float(weighted_terms[gen_torsions_index, 0].detach()):.2f}\")\n" ] }, { "cell_type": "markdown", "id": "4958035b", "metadata": {}, "source": [ "#### Reusing a build context" ] }, { "cell_type": "markdown", "id": "48fb6429", "metadata": {}, "source": [ "Preparing a ligand's chemistry (typing, charges, conformer) is the expensive part of loading. [1BTY](https://www.rcsb.org/structure/1BTY) is a *different* trypsin-benzamidine structure -- passing the earlier `build_context` into `context=` skips re-preparing BEN entirely. `build_context_from_atom_array` extends a copy of the parameter database rather than mutating it, so the original database is untouched." ] }, { "cell_type": "code", "execution_count": 5, "id": "33f0a714", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:52:49.149529Z", "iopub.status.busy": "2026-10-20T00:52:49.147381Z", "iopub.status.idle": "2026-10-20T00:52:50.710191Z", "shell.execute_reply": "2026-10-20T00:52:50.708511Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "reused context to build a second pose: 225 blocks, no re-preparation needed\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/tmol/tmol/tmol/io/_pose_stack_from_biotite.py:1401: UserWarning: Renumbering chain A of the output AtomArray: biotite's CIF writer does not support numbering that decreases within a chain\n", " warnings.warn(\n", "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n" ] }, { "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": [ "other_cif_text = fetch_cif(\"1BTY\")\n", "other_structure = atom_array_from_file(io.StringIO(other_cif_text))\n", "other_pose_stack = pose_stack_from_atom_array(other_structure, device, context=build_context, no_optH=True)\n", "print(f\"reused context to build a second pose: {other_pose_stack.max_n_blocks} blocks, no re-preparation needed\")\n", "\n", "other_ligand_mask = res_mask_to_coord_mask(other_pose_stack, tmol.get_ligand_mask(other_pose_stack))\n", "viewer = tmol.view(other_pose_stack, highlighted=other_ligand_mask[other_pose_stack.real_atoms])\n", "viewer.show()\n" ] }, { "cell_type": "markdown", "id": "af21f61c", "metadata": {}, "source": [ "#### Saving and loading `.tmol` ligand parameters" ] }, { "cell_type": "markdown", "id": "edcc7402", "metadata": {}, "source": [ "`prepare_ligands` (the lower-level function `prepare_ligands=True` calls into) can also write everything it derives for a ligand to a `.tmol` YAML file via `params_output`, given just that ligand's own atoms -- not the whole complex, since the pipeline expects a self-contained molecule." ] }, { "cell_type": "code", "execution_count": 6, "id": "9238ed26", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:52:50.728044Z", "iopub.status.busy": "2026-10-20T00:52:50.727730Z", "iopub.status.idle": "2026-10-20T00:52:51.647413Z", "shell.execute_reply": "2026-10-20T00:52:51.645307Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "wrote /tmp/ben.tmol (9485 bytes)\n" ] } ], "source": [ "import tempfile\n", "from pathlib import Path\n", "from tmol.ligand._preparation import prepare_ligands\n", "\n", "ligand_only_atoms = ligand_structure[ligand_structure.res_name == \"BEN\"]\n", "tmol_path = Path(tempfile.gettempdir()) / \"ben.tmol\"\n", "_, _ = prepare_ligands(ligand_only_atoms, param_db=param_db, params_output=str(tmol_path), seed=SEED)\n", "print(f\"wrote {tmol_path} ({tmol_path.stat().st_size} bytes)\")\n" ] }, { "cell_type": "markdown", "id": "a9fbe163", "metadata": {}, "source": [ "`inject_params_file` reads a `.tmol` file directly into a `ParameterDatabase` -- here the global default -- without touching RDKit or OpenBabel again. Loading a full complex can then skip `prepare_ligands=True` entirely and just point `ligand_params_files` at the file." ] }, { "cell_type": "code", "execution_count": 7, "id": "5c6e616e", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:52:51.652808Z", "iopub.status.busy": "2026-10-20T00:52:51.652507Z", "iopub.status.idle": "2026-10-20T00:52:53.024735Z", "shell.execute_reply": "2026-10-20T00:52:53.023485Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "rebuilt from the saved file: 225 blocks\n" ] } ], "source": [ "from tmol.ligand import inject_params_file\n", "\n", "reloaded_db = inject_params_file(ParameterDatabase.get_default(), str(tmol_path))\n", "reloaded_pose = pose_stack_from_atom_array(\n", " ligand_structure, device, param_db=ParameterDatabase.get_default(),\n", " ligand_params_files=[str(tmol_path)], no_optH=True,\n", ")\n", "print(f\"rebuilt from the saved file: {reloaded_pose.max_n_blocks} blocks\")\n" ] }, { "cell_type": "markdown", "id": "0713cd5c", "metadata": {}, "source": [ "#### Ligand \"splitting\"" ] }, { "cell_type": "markdown", "id": "420725c0", "metadata": {}, "source": [ "A large or multi-fragment ligand can be cut into separately-tracked blocks: chemistry is perceived once for the whole molecule, and a per-atom `tmol_fragment_id` annotation (`FRAGMENT_ID_ANNOTATION`) marks which side of a chosen bond each atom falls on. There's no automatic cut-point selection -- the cut below comes from picking a bridging bond in a test fixture ligand and splitting its bond graph into the two halves that remain connected without it." ] }, { "cell_type": "code", "execution_count": 8, "id": "271ef2d9", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:52:53.029231Z", "iopub.status.busy": "2026-10-20T00:52:53.028917Z", "iopub.status.idle": "2026-10-20T00:52:54.384221Z", "shell.execute_reply": "2026-10-20T00:52:54.382735Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "cutting bond ('C1', 'C2') splits the ligand into fragments of 17 and 42 atoms\n" ] } ], "source": [ "import numpy as np\n", "import biotite.structure as struc\n", "import biotite.structure.io\n", "from tmol.ligand import FRAGMENT_ID_ANNOTATION, load_params_file\n", "split_structure = biotite.structure.io.load_structure(\n", " str(data_path(\"protein_ligand_test\", \"ace.tmol.nomin.cif\")), model=1, include_bonds=True\n", ")\n", "if isinstance(split_structure, struc.AtomArrayStack):\n", " split_structure = split_structure[0]\n", "split_params_path = data_path(\"protein_ligand_test\", \"ace.xtal-lig.mmff94.tmol\")\n", "ligand_restype = load_params_file(split_params_path)[0].residue_type\n", "\n", "cut = (\"C1\", \"C2\")\n", "adjacency = {atom.name: set() for atom in ligand_restype.atoms}\n", "for a, b, *_ in ligand_restype.bonds:\n", " if {a, b} == set(cut):\n", " continue\n", " adjacency[a].add(b)\n", " adjacency[b].add(a)\n", "\n", "\n", "def connected_component(start):\n", " seen, stack = {start}, [start]\n", " while stack:\n", " atom = stack.pop()\n", " for neighbor in adjacency[atom]:\n", " if neighbor not in seen:\n", " seen.add(neighbor)\n", " stack.append(neighbor)\n", " return seen\n", "\n", "\n", "fragment_1_atoms = connected_component(cut[0])\n", "fragment_2_atoms = connected_component(cut[1])\n", "print(f\"cutting bond {cut} splits the ligand into fragments of {len(fragment_1_atoms)} and {len(fragment_2_atoms)} atoms\")\n" ] }, { "cell_type": "code", "execution_count": 9, "id": "dbf78c00", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:52:54.393413Z", "iopub.status.busy": "2026-10-20T00:52:54.392149Z", "iopub.status.idle": "2026-10-20T00:52:57.088989Z", "shell.execute_reply": "2026-10-20T00:52:57.088030Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ligand split into blocks [574, 575] of 576 total\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n" ] }, { "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_atom_mask = split_structure.res_name == \"LG1\"\n", "fragment_ids = np.zeros(split_structure.array_length(), dtype=np.int32)\n", "for atom_index in np.flatnonzero(ligand_atom_mask):\n", " name = str(split_structure.atom_name[atom_index]).strip()\n", " if name in fragment_1_atoms:\n", " fragment_ids[atom_index] = 1\n", " elif name in fragment_2_atoms:\n", " fragment_ids[atom_index] = 2\n", "split_structure.set_annotation(FRAGMENT_ID_ANNOTATION, fragment_ids)\n", "\n", "split_pose_stack, split_build_context = pose_stack_from_atom_array(\n", " split_structure, device, prepare_ligands=True, ligand_params_files=[str(split_params_path)],\n", " no_optH=True, param_db=ParameterDatabase.get_default(), return_context=True,\n", ")\n", "fragment_blocks = [entry.block_ind for entry in split_pose_stack.split_block_mapping.entries]\n", "print(f\"ligand split into blocks {fragment_blocks} of {split_pose_stack.max_n_blocks} total\")\n", "\n", "split_ligand_block_mask = torch.zeros((1, split_pose_stack.max_n_blocks), dtype=torch.bool)\n", "split_ligand_block_mask[0, fragment_blocks] = True\n", "split_ligand_atom_mask = res_mask_to_coord_mask(split_pose_stack, split_ligand_block_mask)\n", "viewer = tmol.view(split_pose_stack, highlighted=split_ligand_atom_mask[split_pose_stack.real_atoms])\n", "viewer.show()\n" ] }, { "cell_type": "markdown", "id": "547fa25b", "metadata": {}, "source": [ "Each fragment block now scores independently via the same block-pair technique seen earlier (section 2)." ] }, { "cell_type": "code", "execution_count": 10, "id": "f69dbda3", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:52:57.112921Z", "iopub.status.busy": "2026-10-20T00:52:57.112540Z", "iopub.status.idle": "2026-10-20T00:52:59.340716Z", "shell.execute_reply": "2026-10-20T00:52:59.339719Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "fragment block 574: -0.73\n", "fragment block 575: 12.12\n" ] } ], "source": [ "split_score_function = beta2016_score_function(device, param_db=split_build_context.parameter_database)\n", "split_block_scorer = split_score_function.render_block_pair_scoring_module(split_pose_stack)\n", "split_block_pair = split_block_scorer(split_pose_stack.coords)[0]\n", "for block in fragment_blocks:\n", " contribution = split_block_pair[block].sum() + split_block_pair[:, block].sum() - split_block_pair[block, block]\n", " print(f\"fragment block {block}: {float(contribution):.2f}\")\n" ] }, { "cell_type": "markdown", "id": "8dad96f3", "metadata": {}, "source": [ "### NCAAs" ] }, { "cell_type": "markdown", "id": "28395e96", "metadata": {}, "source": [ "#### D-amino acids and AIB, handled by default" ] }, { "cell_type": "markdown", "id": "3de25181", "metadata": {}, "source": [ "D-amino acids and AIB (alpha-aminoisobutyric acid) are first-class entries in the default chemical database, not ligands -- they load like any other residue, no `prepare_ligands` flag needed. We use a small curated fixture, a helical peptide built from AIB and leucine." ] }, { "cell_type": "code", "execution_count": 11, "id": "184af379", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:52:59.342732Z", "iopub.status.busy": "2026-10-20T00:52:59.342551Z", "iopub.status.idle": "2026-10-20T00:53:00.055255Z", "shell.execute_reply": "2026-10-20T00:53:00.053115Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "n_poses=1, max_n_blocks=9\n", "sequence (AIB has no one-letter code, shown as X): LXAXLXQXL\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n" ] }, { "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": [ "aib_structure = atom_array_from_file(data_path(\"ncaa_fixtures\", \"uic1_aib_9bzt.cif\"))\n", "aib_pose_stack = pose_stack_from_atom_array(aib_structure, device, param_db=param_db, no_optH=True)\n", "print(f\"n_poses={aib_pose_stack.n_poses}, max_n_blocks={aib_pose_stack.max_n_blocks}\")\n", "print(\"sequence (AIB has no one-letter code, shown as X):\", tmol.get_sequence(aib_pose_stack))\n", "\n", "aib_block_mask = torch.zeros((1, aib_pose_stack.max_n_blocks), dtype=torch.bool)\n", "for block in range(aib_pose_stack.max_n_blocks):\n", " if aib_pose_stack.block_type(0, block).name3 == \"AIB\":\n", " aib_block_mask[0, block] = True\n", "aib_atom_mask = res_mask_to_coord_mask(aib_pose_stack, aib_block_mask)\n", "viewer = tmol.view(aib_pose_stack, highlighted=aib_atom_mask[aib_pose_stack.real_atoms])\n", "viewer.show()\n" ] }, { "cell_type": "markdown", "id": "82a58158", "metadata": {}, "source": [ "#### Most NCAAs go through the ligand pipeline, like SEP" ] }, { "cell_type": "markdown", "id": "a2e1f9d8", "metadata": {}, "source": [ "Less common modifications -- SEP (phosphoserine) here -- aren't in the default database, so they load like a free-molecule ligand: as a residue, with its polymer connections replaced by chemical stubs, run through the same `prepare_ligands=True` pipeline described above, then the stubs restored." ] }, { "cell_type": "code", "execution_count": 12, "id": "8cf7a3b5", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:53:00.058187Z", "iopub.status.busy": "2026-10-20T00:53:00.057800Z", "iopub.status.idle": "2026-10-20T00:53:02.731388Z", "shell.execute_reply": "2026-10-20T00:53:02.729900Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "n_poses=1, max_n_blocks=7, SEP at block 3\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n" ] }, { "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": [ "sep_structure = atom_array_from_file(data_path(\"ncaa_fixtures\", \"phosphopeptide_5ema.cif\"))\n", "sep_pose_stack, sep_build_context = pose_stack_from_atom_array(\n", " sep_structure, device, prepare_ligands=True, param_db=param_db, return_context=True\n", ")\n", "sep_block = next(\n", " block for block in range(sep_pose_stack.max_n_blocks)\n", " if sep_pose_stack.block_type(0, block).name3 == \"SEP\"\n", ")\n", "print(f\"n_poses={sep_pose_stack.n_poses}, max_n_blocks={sep_pose_stack.max_n_blocks}, SEP at block {sep_block}\")\n", "\n", "sep_block_mask = torch.zeros((1, sep_pose_stack.max_n_blocks), dtype=torch.bool)\n", "sep_block_mask[0, sep_block] = True\n", "sep_atom_mask = res_mask_to_coord_mask(sep_pose_stack, sep_block_mask)\n", "viewer = tmol.view(sep_pose_stack, highlighted=sep_atom_mask[sep_pose_stack.real_atoms])\n", "viewer.show()\n" ] }, { "cell_type": "markdown", "id": "0dc19434", "metadata": {}, "source": [ "#### Packing at a SEP position" ] }, { "cell_type": "markdown", "id": "98c4f825", "metadata": {}, "source": [ "The same samplers as any repack (the Dunbrack library, `FixedAAChiSampler`, `IncludeCurrentSampler`) apply here too. `build_rotamers` exposes the actual generated conformers before packing picks among them." ] }, { "cell_type": "code", "execution_count": 13, "id": "2a99ac0f", "metadata": { "execution": { "iopub.execute_input": "2026-10-20T00:53:02.734087Z", "iopub.status.busy": "2026-10-20T00:53:02.733808Z", "iopub.status.idle": "2026-10-20T00:53:05.774573Z", "shell.execute_reply": "2026-10-20T00:53:05.767571Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "rotamers generated for the SEP block: 244\n", "packed SEP position successfully\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n", "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n", "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n", "/home/runner/work/tmol/tmol/.venv/lib/python3.12/site-packages/biotite/structure/io/pdb/file.py:629: DeprecationWarning: The chararray class is deprecated and will be removed in a future release. Use an ndarray with a string or bytes dtype instead.\n", " record = np.char.array(np.where(array.hetero, \"HETATM\", \"ATOM\"))\n" ] }, { "data": { "text/html": [ "\n", "
\n", " \n", "
\n", " \n", "
\n", "
SEP at block 3, pre-packing
\n", "
\n", "

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

\n", "
\n", "\n", "
\n", "
\n", "
packed from 244 sampled rotamers
\n", "
\n", "

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

\n", "
\n", "\n", "
\n", "
\n", "
original vs. packed
\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": [ "from tmol.pack import SetPackerTask\n", "from tmol.pack.rotamer import build_rotamers\n", "\n", "sep_task = PackerTask(sep_pose_stack, PackerPalette())\n", "sep_task.restrict_to_repacking()\n", "sep_shell = torch.zeros_like(sep_task.is_real_block)\n", "sep_shell[0, sep_block] = True\n", "sep_task.disable_packing_by_block_mask(~sep_shell)\n", "sep_task.add_conformer_sampler(create_dunbrack_sampler_from_database(sep_build_context.parameter_database, device))\n", "sep_task.add_conformer_sampler(FixedAAChiSampler())\n", "sep_task.add_conformer_sampler(IncludeCurrentSampler())\n", "\n", "_, sep_rotamer_set = build_rotamers(\n", " sep_pose_stack, SetPackerTask.from_packer_task(sep_task), sep_pose_stack.packed_block_types.chem_db\n", ")\n", "print(f\"rotamers generated for the SEP block: {int(sep_rotamer_set.n_rots_for_block[0, sep_block])}\")\n", "\n", "sep_score_function = beta2016_score_function(device, param_db=sep_build_context.parameter_database)\n", "torch.manual_seed(SEED)\n", "sep_packed = pack_rotamers(sep_pose_stack, sep_score_function, sep_task)\n", "print(\"packed SEP position successfully\")\n", "\n", "display(tmol.overlay_view(\n", " {\"original\": sep_pose_stack, \"packed\": sep_packed},\n", " notes={\n", " \"original\": f\"SEP at block {sep_block}, pre-packing\",\n", " \"packed\": f\"packed from {int(sep_rotamer_set.n_rots_for_block[0, sep_block])} sampled rotamers\",\n", " },\n", "))\n" ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "T4" }, "kernelspec": { "display_name": "Python 3", "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.11.12" }, "tmol": { "gpu_smoke": true } }, "nbformat": 4, "nbformat_minor": 5 }