Skip to content

Commit 86174cb

Browse files
authored
Merge pull request #522 from akalpokas/feature_custom_ROI_maps
2 parents 07765cb + 6fe7759 commit 86174cb

6 files changed

Lines changed: 330 additions & 21 deletions

File tree

32.7 KB
Loading
32 KB
Loading
27.3 KB
Loading

doc/source/tutorials/protein_mutations.rst

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ figure out the residue index of our residue of interest (ROI):
198198
199199
We can see that the residue with the index value of 8 are different
200200
between the two proteins. Let’s pass this value to the
201-
```BioSimSpace.Align.matchAtoms`` <https://biosimspace.openbiosim.org/api/generated/BioSimSpace.Align.matchAtoms.html#BioSimSpace.Align.matchAtoms>`__
201+
`BioSimSpace.Align.matchAtoms <https://biosimspace.openbiosim.org/api/generated/BioSimSpace.Align.matchAtoms.html>`__
202202
function:
203203

204204
.. code:: ipython3
@@ -398,3 +398,169 @@ simulations <https://github.com/OpenBioSim/biosimspace_tutorials/tree/main/04_fe
398398
solvated_system = BSS.Solvent.tip3p(molecule=merged_system, box=box, angles=angles, ion_conc=0.15)
399399
400400
BSS.IO.saveMolecules("holo_aldose_reductase_v47i", solvated_system, ["gro87", "grotop"])
401+
402+
Advanced Case - Bond Creation/Annihilation Transformations
403+
==========================================================
404+
405+
In this tutorial we will use BioSimSpace’s mapping functionality to set
406+
up alchemical calculations involving proline mutations in a protein.
407+
Specifically, we will look at the Leu-to-Pro mutations in OMTKY3 to its
408+
receptors as `detailed in this
409+
study <https://pubs.acs.org/doi/full/10.1021/acs.jctc.1c00214>`__.
410+
411+
.. code:: ipython3
412+
413+
wt = BSS.IO.readMolecules(
414+
BSS.IO.expand(BSS.tutorialUrl(), f"1choFH_apo_wt_flare_processed.pdb")
415+
)[0]
416+
mut = BSS.IO.readMolecules(
417+
BSS.IO.expand(BSS.tutorialUrl(), f"1choFH_apo_mut_flare_processed.pdb")
418+
)[0]
419+
420+
wt = BSS.Parameters.ff14SB(wt, ensure_compatible=False).getMolecule()
421+
mut = BSS.Parameters.ff14SB(mut, ensure_compatible=False).getMolecule()
422+
423+
424+
425+
Comparing the residues between two proteins shows us that the residues
426+
at index 15 are different between the proteins
427+
428+
.. code:: ipython3
429+
430+
roi = []
431+
for i, res in enumerate(wt.getResidues()):
432+
if res.name() != mut.getResidues()[i].name():
433+
print(res, mut.getResidues()[i])
434+
roi.append(res.index())
435+
436+
437+
.. parsed-literal::
438+
439+
<BioSimSpace.Residue: name='LEU', molecule=5, index=15, nAtoms=19> <BioSimSpace.Residue: name='PRO', molecule=7, index=15, nAtoms=14>
440+
441+
442+
By default, the
443+
`BioSimSpace.Align.matchAtoms <https://biosimspace.openbiosim.org/api/generated/BioSimSpace.Align.matchAtoms.html>`__
444+
would fail to create a mapping for the ROI region, as the underlying
445+
RDKit MCS algorithm would be unable to determine a mapping between two
446+
molecular graphs with a fundamental topological mismatch. Because
447+
Proline’s sidechain forms a cyclic system with the backbone, its atoms
448+
exist in a ring topology. The algorithm restricts the mapping of cyclic
449+
atoms to acyclic atoms (a behavior governed by parameters like
450+
``ringMatchesRingOnly``) to preserve the chemical integrity of the
451+
substructure. Consequently, a 1:1 mapping between the ring-bound
452+
sidechain of Proline and the acyclic sidechain of the Leucine residue
453+
cannot be determined.
454+
455+
Instead we can use the ``custom_roi_map`` argument of the
456+
`BioSimSpace.Align.matchAtoms <https://biosimspace.openbiosim.org/api/generated/BioSimSpace.Align.matchAtoms.html>`__
457+
to override the RDKit MCS mapping. For example we can force an empty
458+
mapping between the two residues:
459+
460+
.. code:: ipython3
461+
462+
mapping = BSS.Align.matchAtoms(molecule0=wt, molecule1=mut, roi=[15], custom_roi_map={})
463+
464+
.. code:: ipython3
465+
466+
BSS.Align.viewMapping(wt, mut, mapping, roi=15, pixels=500)
467+
468+
469+
470+
.. image:: images/custom_roi_no_map.png
471+
:width: 800
472+
473+
474+
If we know the correct 1:1 atom mapping between the two residues, we can
475+
pass that to the ``custom_roi_map`` which will allows us to setup an
476+
alchemical bond transformation for mutating the leucine residue to
477+
proline. Note that **absolute atom indices need to be passed, i.e the
478+
indices of the residues in the context of the whole protein**. We can
479+
use something like PyMol to help us map the atoms in the right order:
480+
481+
.. image:: images/1choFH_mapping_pymol.png
482+
:width: 800
483+
484+
.. code:: ipython3
485+
486+
mapping = BSS.Align.matchAtoms(molecule0=wt, molecule1=mut, roi=[15], custom_roi_map={204:204,205:205,203:203,202:202,211:208,206:206,213:210,207:207,214:211,210:213})
487+
488+
.. code:: ipython3
489+
490+
BSS.Align.viewMapping(wt, mut, mapping, roi=15, pixels=500)
491+
492+
493+
494+
.. image:: images/custom_roi_ring_break_map.png
495+
:width: 800
496+
497+
498+
We can then use ``allow_ring_breaking=True`` argument of the
499+
`BioSimSpace.Align.merge <https://biosimspace.openbiosim.org/api/generated/BioSimSpace.Align.merge.html>`__
500+
to create the required alchemical transformation:
501+
502+
.. code:: ipython3
503+
504+
aligned_wt = BSS.Align.rmsdAlign(molecule0=wt, mapping=mapping, molecule1=mut)
505+
merged_protein = BSS.Align.merge(aligned_wt, mut, mapping, allow_ring_breaking=True, roi=[15])
506+
507+
But how do we actually know that the merge has built a perturbable
508+
molecule that now has a bond annihilation or creation involved? We can
509+
use Sire’s conversion features to check what kinds of alchemical
510+
modifications are happening in our perturbable molecule. You can check
511+
out the corresponding `Sire
512+
tutorial <https://sire.openbiosim.org/tutorial/part07/04_merge.html>`__
513+
for more details.
514+
515+
.. code:: ipython3
516+
517+
merged_protein_sire = merged_protein._sire_object
518+
pert = merged_protein_sire.perturbation()
519+
pert_omm = pert.to_openmm(map={"coordinates":"coordinates0"})
520+
521+
pert_omm.changed_bonds(to_pandas=True)
522+
523+
524+
525+
526+
527+
.. raw:: html
528+
529+
<div>
530+
<table border="1" class="dataframe">
531+
<thead>
532+
<tr style="text-align: right;">
533+
<th></th>
534+
<th>bond</th>
535+
<th>length0</th>
536+
<th>length1</th>
537+
<th>k0</th>
538+
<th>k1</th>
539+
</tr>
540+
</thead>
541+
<tbody>
542+
<tr>
543+
<th>0</th>
544+
<td>N:203-H:211</td>
545+
<td>0.1010</td>
546+
<td>0.1449</td>
547+
<td>363171.2</td>
548+
<td>282001.6</td>
549+
</tr>
550+
<tr>
551+
<th>1</th>
552+
<td>CG:208-H:211</td>
553+
<td>0.1526</td>
554+
<td>0.1526</td>
555+
<td>0.0</td>
556+
<td>259408.0</td>
557+
</tr>
558+
</tbody>
559+
</table>
560+
</div>
561+
562+
563+
564+
By comparing the ``k0`` and ``k1`` values in the changed bond dataframe,
565+
we can see that the transformation is going to result in a bond being
566+
created.

src/BioSimSpace/Align/_align.py

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ def matchAtoms(
713713
complete_rings_only=True,
714714
max_scoring_matches=1000,
715715
roi=None,
716+
custom_roi_map=None,
716717
prune_perturbed_constraints=False,
717718
prune_crossing_constraints=False,
718719
prune_atom_types=False,
@@ -778,6 +779,12 @@ def matchAtoms(
778779
The region of interest to match.
779780
Consists of a list of ROI residue indices.
780781
782+
custom_roi_map : dict
783+
A dictionary that maps atom indices in molecule0 to those in
784+
molecule1 within the region of interest. This allows the user
785+
override the MCS mapping within the region of interest.
786+
Only relevant when 'roi' is not None.
787+
781788
prune_perturbed_constraints : bool
782789
Whether to remove hydrogen atoms that are perturbed to heavy atoms
783790
from the mapping. This option should be True when creating mappings
@@ -851,6 +858,11 @@ def matchAtoms(
851858
852859
>>> import BioSimSpace as BSS
853860
>>> mapping = BSS.Align.matchAtoms(molecule0, molecule1, roi=[12, 13, 14])
861+
862+
Find the mapping between two molecules with a custom region of interest mapping.
863+
864+
>>> import BioSimSpace as BSS
865+
>>> mapping = BSS.Align.matchAtoms(molecule0, molecule1, roi=[12], custom_roi_map={0 : 10, 3 : 7})
854866
"""
855867

856868
if roi is None:
@@ -872,9 +884,10 @@ def matchAtoms(
872884
)
873885
else:
874886
return _roiMatch(
875-
molecule0=molecule0,
876-
molecule1=molecule1,
877-
roi=roi,
887+
molecule0,
888+
molecule1,
889+
roi,
890+
custom_roi_map,
878891
prune_perturbed_constraints=prune_perturbed_constraints,
879892
prune_crossing_constraints=prune_crossing_constraints,
880893
prune_atom_types=prune_atom_types,
@@ -1215,6 +1228,7 @@ def _roiMatch(
12151228
molecule0,
12161229
molecule1,
12171230
roi,
1231+
custom_roi_map,
12181232
prune_perturbed_constraints=False,
12191233
prune_crossing_constraints=False,
12201234
prune_atom_types=False,
@@ -1240,6 +1254,11 @@ def _roiMatch(
12401254
The region of interest to match.
12411255
Consists of a list of ROI residue indices
12421256
1257+
custom_roi_map : dict
1258+
A dictionary that maps atom indices in molecule0 to those in
1259+
molecule1 within the region of interest. This allows the user
1260+
override the MCS mapping within the region of interest.
1261+
12431262
prune_perturbed_constraints : bool
12441263
Whether to remove hydrogen atoms that are perturbed to heavy atoms
12451264
from the mapping. This option should be True when creating mappings
@@ -1308,6 +1327,12 @@ def _roiMatch(
13081327
13091328
>>> import BioSimSpace as BSS
13101329
>>> mapping = BSS.Align._align._roiMatch(molecule0, molecule1, roi=[12], use_kartograf=True)
1330+
1331+
Find the mapping between two molecules with a custom region of interest mapping.
1332+
1333+
>>> import BioSimSpace as BSS
1334+
>>> mapping = BSS.Align._align._roiMatch(molecule0, molecule1, roi=[12], custom_roi_map={0 : 10, 3 : 7})
1335+
13111336
"""
13121337
from .._SireWrappers import Molecule as _Molecule
13131338

@@ -1413,22 +1438,41 @@ def _roiMatch(
14131438
res0_extracted, res1_extracted, kartograf_kwargs
14141439
)
14151440
mapping = kartograf_mapping.componentA_to_componentB
1441+
1442+
# Prevent the MCS mapping from being generated if a custom ROI mapping is provided.
1443+
elif custom_roi_map is not None:
1444+
# Validate custom_roi_map is a dict and is inside the ROI region
1445+
if not isinstance(custom_roi_map, dict):
1446+
raise TypeError("'custom_roi_map' must be of type 'dict'")
1447+
for k, v in custom_roi_map.items():
1448+
if k not in res0_idx:
1449+
raise ValueError(
1450+
f"Key {k} in 'custom_roi_map' is not in the ROI region of molecule0."
1451+
)
1452+
if v not in res1_idx:
1453+
raise ValueError(
1454+
f"Value {v} in 'custom_roi_map' is not in the ROI region of molecule1."
1455+
)
1456+
mapping = None
14161457
else:
14171458
mapping = matchAtoms(
14181459
res0_extracted,
14191460
res1_extracted,
14201461
)
14211462

1422-
# Look up the absolute atom indices in the molecule
1423-
res0_lookup_table = list(mapping.keys())
1424-
absolute_mapped_atoms_res0 = [res0_idx[i] for i in res0_lookup_table]
1463+
# Look up the absolute atom indices in the molecule if not using a custom ROI mapping.
1464+
if custom_roi_map is not None:
1465+
absolute_roi_mapping = custom_roi_map
1466+
else:
1467+
res0_lookup_table = list(mapping.keys())
1468+
absolute_mapped_atoms_res0 = [res0_idx[i] for i in res0_lookup_table]
14251469

1426-
res1_lookup_table = list(mapping.values())
1427-
absolute_mapped_atoms_res1 = [res1_idx[i] for i in res1_lookup_table]
1470+
res1_lookup_table = list(mapping.values())
1471+
absolute_mapped_atoms_res1 = [res1_idx[i] for i in res1_lookup_table]
14281472

1429-
absolute_roi_mapping = dict(
1430-
zip(absolute_mapped_atoms_res0, absolute_mapped_atoms_res1)
1431-
)
1473+
absolute_roi_mapping = dict(
1474+
zip(absolute_mapped_atoms_res0, absolute_mapped_atoms_res1)
1475+
)
14321476

14331477
# If we are at the last residue of interest, we don't need to worry
14341478
# too much about the after ROI region as this region will be all of the

0 commit comments

Comments
 (0)