Custom mapping for Monster

In the placement operation without prior merging (‘none’ or ‘expand’), a custom mapping can be supplied to the placement operation in the form {'hit1': {1: 2, 3: 4}, 'hit2': {1: 6, 2: 8}}, where for some hits, specified by name (i.e. mol.Prop('_Name')), is assigned the required correspondence between hit atom index and followup atom index.

In monster/mcs_mapping and monster/_place_modes/_expand.py is the code that allows this. In particular the SpecialCompareAtoms class is used to control the MCS comparison.

SpecialCompareAtoms

The mapping as discussed in GitHub issue #23 is in the format

mapping = { 'hit1': {1:1,2:5} 'hit2': {3:3,4:4,4:6}}

The hit index is first, followup index is the second. The index -1 for a followup index is the same as not providing the hit index, it is described here solely for clarity not for use.

mapping = { 'hit1': {1:1,2:5, 3:-1} 'hit2': {3:3,4:4,4:6}}

The index -2 for a followup index will result in the hit atom index not matching any followup index.

mapping = { 'hit1': {1:1,2:5, 3:-2} 'hit2': {3:3,4:4,4:6}}

If exclusive_mapping argument of SpecialCompareAtoms.__init__ is True, then if a followup index is present in one hit, but not in a second hit, then no atom of the second hit will match that followup atom. A negative index for a hit atom index means that no atom in that hit will match the corresponding followup index.

mapping = { 'hit1': {1:1,2:5,-1:3, -2: 7} 'hit2': {3:3,4:4,4:6}}

However, a positive integer on a different hit overrides it, therefore, in the above followup atom 3 cannot be matched to any atom in hit1, but will match atom 3 in hit2. Followup atom 7 will not match either.

Filters

However, this does not prevent the result being a mapping without those indices. For that the method Monster.get_mcs_mappings makes sure that the mapping does contain those atoms.

Example

A nice test case comes from an Angewandte Chemie paper with the following merger:

merger

This is a nice case because it features a troublesome mapping, because the primary hit, F04, which looks like a Hantzsch thiazole synthesis product, presents can be flipped either way upon investigation of the crystal map and the followup bold4 is indeed a flipped merger. Therefore, without a custom_map the followup is incorrect.

Where I to make a flipped thiazole F04 I could do:

with Chem.SDMolSupplier('test_mols/5SB7_mergers.sdf') as reader:
    mols = list(reader)

monster = Monster([mols[0]])
monster.place(Chem.Mol(mols[0]),    # it technically saves a copy already, but one should always be careful
              merging_mode='expansion',
              custom_map={'F04': {-1: 4,  # no amine
                                  4: -2,  # no amine
                                  12: 12,
                                  6: 13,
                                  13: 6,
                                  15: 14,
                                  14: 15}}
              )
monster.show_comparison()
monster.to_nglview()

This requires identifying the atom indices. To find out the atom indices I could do:

monster.draw_nicely(monster.positioned_mol)

Nota bene maxime: atom indices change when going from a Chem.Mol to SMILES. In SMILES and mol files the atom indices are as they are read. But when you do the conversion Chem.MolToSmiles(mol) the indices are changed. Therefore, for Victor, which before May 2022 (v. 0.9) accepted only SMILES, one should get the followup index atoms as they appear in the SMILES. When a followup in the format Chem.Mol is passed to Victor.place (funtools.singledispatchmethod overloaded) the staticmethod Monster.renumber_followup_custom_map is called to fix these allowing one to provide the indices as they are in the Chem.Mol, which are renumbered behind the scenes. However, calling Monster.convert_origins_to_custom_map will return indices in the renumbered format, so cannot be used as a reference (unless Monster.renumber_followup_custom_map is used on it to convert this from monster.position_mol to the actual original mol).

To find out how the two hits look in 3D, I could do:

monster.to_nglview()
from fragmenstein import Igor

Igor.init_pyrosetta()

victor = Victor(hits=mols[:2], pdb_filename='template.pdb', ligand_resi='1X')

victor.place(mols[3],
             long_name=mols[3].GetProp('_Name'),
              merging_mode='expansion',
             custom_map={'F36': {1: 7},
                          'F04': {0: -1,  # no amine
                                  -1: 7,  # no amine
                                  13: 5,  # to N
                                  12: 13,  # root to Ph
                                  6: 14,  # to C
                                  }
                          }

              )
victor.show_comparison()
victor.to_nglview()
victor.validate(mols[3])['reference2minimized_rmsd'] < 1