Skip to content

pair

Classes:

  • Pair

    Facilitates the creation of morphs.

Pair

Pair(ligA, ligZ, config=None, **kwargs)

Facilitates the creation of morphs. It offers functionality related to a pair of ligands (a transformation).

:param ligA: The ligand to be used as the starting state for the transformation. :type ligA: :class:Ligand or string :param ligZ: The ligand to be used as the ending point of the transformation. :type ligZ: :class:Ligand or string :param config: The configuration object holding all settings. :type config: :class:Config

fixme - list all relevant kwargs here

param ligand_net_charge: integer, net charge of each ligand (has to be the same)

Methods:

  • superimpose

    Please see :class:Config class for the documentation of kwargs. The passed kwargs overwrite the config

  • set_suptop

    Attach a SuperimposedTopology object along with the ParmEd objects for the ligA and ligZ.

  • make_atom_names_unique

    Ensure that each that atoms across the two ligands have unique names.

  • check_json_file

    Performance optimisation in case TIES is rerun again. Return the first matched atoms which

  • merge_frcmod_files

    Merges the .frcmod files generated for each ligand separately, simply by adding them together.

  • overlap_fractions

    Calculate the size of the common area.

Source code in ties/pair.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def __init__(self, ligA, ligZ, config=None, **kwargs):
    """
    Please use the Config class for the documentation of the possible kwargs.
    Each kwarg is passed to the config class.

    fixme - list all relevant kwargs here

        param ligand_net_charge: integer, net charge of each ligand (has to be the same)
    """

    # create a new config if it is not provided
    self.config = ties.config.Config() if config is None else config

    # channel all config variables to the config class
    self.config.set_configs(**kwargs)

    # tell Config about the ligands if necessary
    if self.config.ligands is None:
        self.config.ligands = [ligA, ligZ]

    # create ligands if they're just paths
    if isinstance(ligA, ties.ligand.Ligand):
        self.ligA = ligA
    else:
        self.ligA = ties.ligand.Ligand(ligA, self.config)

    if isinstance(ligZ, ties.ligand.Ligand):
        self.ligZ = ligZ
    else:
        self.ligZ = ties.ligand.Ligand(ligZ, self.config)

    # initialise the handles to the molecules that morph
    self.current_ligA = self.ligA.current
    self.current_ligZ = self.ligZ.current

    self.internal_name = f'{self.ligA.internal_name}_{self.ligZ.internal_name}'
    self.mol2 = None
    self.pdb = None
    self.summary = None
    self.suptop = None
    self.mda_l1 = None
    self.mda_l2 = None
    self.distance = None

superimpose

superimpose(**kwargs)

Please see :class:Config class for the documentation of kwargs. The passed kwargs overwrite the config object passed in the constructor.

fixme - list all relevant kwargs here

:param use_element_in_superimposition: bool whether the superimposition should rely on the element initially, before refining the results with a more specific check of the atom type. :param manually_matched_atom_pairs: :param manually_mismatched_pairs: :param redistribute_q_over_unmatched:

Source code in ties/pair.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def superimpose(self, **kwargs):
    """
    Please see :class:`Config` class for the documentation of kwargs. The passed kwargs overwrite the config
    object passed in the constructor.

    fixme - list all relevant kwargs here

    :param use_element_in_superimposition: bool whether the superimposition should rely on the element initially,
        before refining the results with a more specific check of the atom type.
    :param manually_matched_atom_pairs:
    :param manually_mismatched_pairs:
    :param redistribute_q_over_unmatched:
    """
    self.config.set_configs(**kwargs)

    # use ParmEd to load the files
    # fixme - move this to the Morph class instead of this place,
    # fixme - should not squash all messsages. For example, wrong type file should not be squashed
    leftlig_atoms, leftlig_bonds, rightlig_atoms, rightlig_bonds, parmed_ligA, parmed_ligZ = \
        get_atoms_bonds_from_mol2(self.current_ligA, self.current_ligZ,
                                  use_general_type=self.config.use_element_in_superimposition)
    # fixme - manual match should be improved here and allow for a sensible format.

    # in case the atoms were renamed, pass the names via the map renaming map
    # TODO
    # ligZ_old_new_atomname_map
    new_mismatch_names = []
    for a, z in self.config.manually_mismatched_pairs:
        new_names = (self.ligA.rev_renaming_map[a], self.ligZ.rev_renaming_map[z])
        logger.debug(f'Selecting mismatching atoms. The mismatch {(a, z)}) was renamed to {new_names}')
        new_mismatch_names.append(new_names)

    # assign
    # fixme - Ideally I would reuse the ParmEd data for this,
    # ParmEd can use bonds if they are present - fixme
    # map atom IDs to their objects
    ligand1_nodes = {}
    for atomNode in leftlig_atoms:
        ligand1_nodes[atomNode.id] = atomNode
    # link them together
    for nfrom, nto, btype in leftlig_bonds:
        ligand1_nodes[nfrom].bind_to(ligand1_nodes[nto], btype)

    ligand2_nodes = {}
    for atomNode in rightlig_atoms:
        ligand2_nodes[atomNode.id] = atomNode
    for nfrom, nto, btype in rightlig_bonds:
        ligand2_nodes[nfrom].bind_to(ligand2_nodes[nto], btype)

    # fixme - this should be moved out of here,
    #  ideally there would be a function in the main interface for this
    manual_match = [] if self.config.manually_matched_atom_pairs is None else self.config.manually_matched_atom_pairs
    starting_node_pairs = []
    for l_aname, r_aname in manual_match:
        # find the starting node pairs, ie the manually matched pair(s)
        found_left_node = None
        for id, ln in ligand1_nodes.items():
            if l_aname == ln.name:
                found_left_node = ln
        if found_left_node is None:
            raise ValueError(f'Manual Matching: could not find an atom name: "{l_aname}" in the left molecule')

        found_right_node = None
        for id, ln in ligand2_nodes.items():
            if r_aname == ln.name:
                found_right_node = ln
        if found_right_node is None:
            raise ValueError(f'Manual Matching: could not find an atom name: "{r_aname}" in the right molecule')

        starting_node_pairs.append([found_left_node, found_right_node])

    if starting_node_pairs:
        logger.debug(f'Starting nodes will be used: {starting_node_pairs}')

    logging_key = str(self)

    # fixme - simplify to only take the ParmEd as input
    suptop = superimpose_topologies(ligand1_nodes.values(), ligand2_nodes.values(),
                                     disjoint_components=self.config.allow_disjoint_components,
                                     net_charge_filter=True,
                                     pair_charge_atol=self.config.atom_pair_q_atol,
                                     net_charge_threshold=self.config.net_charge_threshold,
                                     redistribute_charges_over_unmatched=self.config.redistribute_q_over_unmatched,
                                     ignore_charges_completely=self.config.ignore_charges_completely,
                                     ignore_bond_types=True,
                                     ignore_coords=False,
                                     align_molecules=self.config.align_molecules_using_mcs,
                                     use_general_type=self.config.use_element_in_superimposition,
                                     # fixme - not the same ... use_element_in_superimposition,
                                     use_only_element=False,
                                     check_atom_names_unique=True,  # fixme - remove?
                                     starting_pairs_heuristics=self.config.starting_pairs_heuristics,  # fixme - add to config
                                     force_mismatch=new_mismatch_names,
                                     starting_node_pairs=starting_node_pairs,
                                     parmed_ligA=parmed_ligA, parmed_ligZ=parmed_ligZ,
                                     starting_pair_seed=self.config.superimposition_starting_pair,
                                     logging_key=logging_key,
                                     config=self.config)

    self.set_suptop(suptop, parmed_ligA, parmed_ligZ)
    # attach the used config to the suptop

    if suptop is not None:
        suptop.config = self.config
        # attach the morph to the suptop
        suptop.morph = self

    return suptop

set_suptop

set_suptop(suptop, parmed_ligA, parmed_ligZ)

Attach a SuperimposedTopology object along with the ParmEd objects for the ligA and ligZ.

:param suptop: :class:SuperimposedTopology :param parmed_ligA: An ParmEd for the ligA :param parmed_ligZ: An ParmEd for the ligZ

Source code in ties/pair.py
196
197
198
199
200
201
202
203
204
205
206
def set_suptop(self, suptop, parmed_ligA, parmed_ligZ):
    """
    Attach a SuperimposedTopology object along with the ParmEd objects for the ligA and ligZ.

    :param suptop: :class:`SuperimposedTopology`
    :param parmed_ligA: An ParmEd for the ligA
    :param parmed_ligZ: An ParmEd for the ligZ
    """
    self.suptop = suptop
    self.parmed_ligA = parmed_ligA
    self.parmed_ligZ = parmed_ligZ

make_atom_names_unique

make_atom_names_unique(out_ligA_filename=None, out_ligZ_filename=None, save=True)

Ensure that each that atoms across the two ligands have unique names.

While renaming atoms, start with the element (C, N, ..) followed by the count so far (e.g. C1, C2, N1).

Resnames are set to "INI" and "FIN", this is useful for the hybrid dual topology.

:param out_ligA_filename: The new filenames for the ligands with renamed atoms. If None, the default naming convention is used. :type out_ligA_filename: string or bool :param out_ligZ_filename: The new filenames for the ligands with renamed atoms. If None, the default naming convention is used. :type out_ligZ_filename: string or bool :param save: Whether to save to the disk the ligands after renaming the atoms :type save: bool

Source code in ties/pair.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def make_atom_names_unique(self, out_ligA_filename=None, out_ligZ_filename=None, save=True):
    """
    Ensure that each that atoms across the two ligands have unique names.

    While renaming atoms, start with the element (C, N, ..) followed by
     the count so far (e.g. C1, C2, N1).

    Resnames are set to "INI" and "FIN", this is useful for the hybrid dual topology.

    :param out_ligA_filename: The new filenames for the ligands with renamed atoms. If None, the default
        naming convention is used.
    :type out_ligA_filename: string or bool
    :param out_ligZ_filename: The new filenames for the ligands with renamed atoms. If None, the default
        naming convention is used.
    :type out_ligZ_filename: string or bool
    :param save: Whether to save to the disk the ligands after renaming the atoms
    :type save: bool
    """

    # The A ligand is a template for the renaming
    self.ligA.correct_atom_names()

    # load both ligands
    left = parmed.load_file(str(self.ligA.current), structure=True)
    right = parmed.load_file(str(self.ligZ.current), structure=True)

    common_atom_names = {a.name for a in right.atoms}.intersection({a.name for a in left.atoms})
    atom_names_overlap = len(common_atom_names) > 0

    if atom_names_overlap or not self.ligZ.are_atom_names_correct():
        logger.debug(f'Renaming ({self.ligA.internal_name}) molecule ({self.ligZ.internal_name}) atom names are either reused or do not follow the correct format. ')
        if atom_names_overlap:
            logger.debug(f'Common atom names: {common_atom_names}')
        name_counter_L_nodes = ties.helpers.get_atom_names_counter(left.atoms)
        _, renaming_map = ties.helpers.get_new_atom_names(right.atoms, name_counter=name_counter_L_nodes)
        self.ligZ.renaming_map = renaming_map

    # rename the residue names to INI and FIN
    for atom in left.atoms:
        atom.residue = 'INI'
    for atom in right.atoms:
        atom.residue = 'FIN'

    # fixme - instead of using the save parameter, have a method pair.save(filename1, filename2) and
    #  call it when necessary.
    # prepare the destination directory
    if not save:
        return

    if out_ligA_filename is None:
        cwd = self.config.pair_unique_atom_names_dir / f'{self.ligA.internal_name}_{self.ligZ.internal_name}'
        cwd.mkdir(parents=True, exist_ok=True)

        self.current_ligA = cwd / (self.ligA.internal_name + '.mol2')
        self.current_ligZ = cwd / (self.ligZ.internal_name + '.mol2')
    else:
        self.current_ligA = out_ligA_filename
        self.current_ligZ = out_ligZ_filename

    # save the updated atom names
    left.save(str(self.current_ligA))
    right.save(str(self.current_ligZ))

check_json_file

check_json_file()

Performance optimisation in case TIES is rerun again. Return the first matched atoms which can be used as a seed for the superimposition.

:return: If the superimposition was computed before, and the .json file is available, gets one of the matched atoms. :rtype: [(ligA_atom, ligZ_atom)]

Source code in ties/pair.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def check_json_file(self):
    """
    Performance optimisation in case TIES is rerun again. Return the first matched atoms which
    can be used as a seed for the superimposition.

    :return: If the superimposition was computed before, and the .json file is available,
        gets one of the matched atoms.
    :rtype: [(ligA_atom, ligZ_atom)]
    """
    matching_json = self.config.workdir / f'fep_{self.ligA.internal_name}_{self.ligZ.internal_name}.json'
    if not matching_json.is_file():
        return None

    return [list(json.load(matching_json.open())['matched'].items())[0]]

merge_frcmod_files

merge_frcmod_files(ligcom=None)

Merges the .frcmod files generated for each ligand separately, simply by adding them together.

The duplication has no effect on the final generated topology parm7 top file.

We are also testing the .frcmod here with the user's force field in order to check if the merge works correctly.

:param ligcom: Either "lig" if only ligands are present, or "com" if the complex is present. Helps with the directory structure. :type ligcom: string "lig" or "com"

Source code in ties/pair.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
def merge_frcmod_files(self, ligcom=None):
    """
    Merges the .frcmod files generated for each ligand separately, simply by adding them together.

    The duplication has no effect on the final generated topology parm7 top file.

    We are also testing the .frcmod here with the user's force field in order to check if
    the merge works correctly.

    :param ligcom: Either "lig" if only ligands are present, or "com" if the complex is present.
        Helps with the directory structure.
    :type ligcom: string "lig" or "com"
    """
    ambertools_tleap = self.config.ambertools_tleap
    ambertools_script_dir = self.config.ambertools_script_dir
    if self.config.protein is None:
        protein_ff = None
    else:
        protein_ff = self.config.protein_ff

    ligand_ff = self.config.ligand_ff

    frcmod_info1 = ties.helpers.parse_frcmod_sections(self.ligA.frcmod)
    frcmod_info2 = ties.helpers.parse_frcmod_sections(self.ligZ.frcmod)

    cwd = self.config.workdir

    # fixme: use the provided cwd here, otherwise this will not work if the wrong cwd is used
    # have some conf module instead of this
    if ligcom:
        morph_frcmod = cwd / f'ties-{self.ligA.internal_name}-{self.ligZ.internal_name}' / ligcom / 'build' / 'hybrid.frcmod'
    else:
        # fixme - clean up
        morph_frcmod = cwd / f'ties-{self.ligA.internal_name}-{self.ligZ.internal_name}' / 'build' / 'hybrid.frcmod'
    morph_frcmod.parent.mkdir(parents=True, exist_ok=True)
    with open(morph_frcmod, 'w') as FOUT:
        FOUT.write('merged frcmod\n')

        for section in ['MASS', 'BOND', 'ANGLE',
                        'DIHE', 'IMPROPER', 'NONBON']:
            section_lines = frcmod_info1[section] + frcmod_info2[section]
            FOUT.write('{0:s}\n'.format(section))
            for line in section_lines:
                FOUT.write('{0:s}'.format(line))
            FOUT.write('\n')

        FOUT.write('\n\n')

    # this is our current frcmod file
    self.frcmod = morph_frcmod

    # as part of the .frcmod writing
    # insert dummy angles/dihedrals if a morph .frcmod requires
    # new terms between the appearing/disappearing atoms
    # this is a trick to make sure tleap has everything it needs to generate the .top file
    correction_introduced = self._check_hybrid_frcmod(ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff)
    if correction_introduced:
        # move the .frcmod which turned out to be insufficient according to the test
        shutil.move(morph_frcmod, str(self.frcmod) + '.uncorrected' )
        # now copy in place the corrected version
        shutil.copy(self.frcmod, morph_frcmod)

overlap_fractions

overlap_fractions()

Calculate the size of the common area.

:return: Four decimals capturing: 1) the fraction of the common size with respect to the ligA topology, 2) the fraction of the common size with respect to the ligZ topology, 3) the percentage of the disappearing atoms in the disappearing molecule 4) the percentage of the appearing atoms in the appearing molecule :rtype: [float, float, float, float]

Source code in ties/pair.py
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def overlap_fractions(self):
    """
    Calculate the size of the common area.

    :return: Four decimals capturing: 1) the fraction of the common size with respect to the ligA topology,
        2) the fraction of the common size with respect to the ligZ topology,
        3) the percentage of the disappearing atoms in the disappearing molecule
        4) the percentage of the appearing atoms  in the appearing molecule
    :rtype: [float, float, float, float]
    """

    if self.suptop is None:
        return 0, 0, float('inf'), float('inf')
    else:
        mcs_size = len(self.suptop.matched_pairs)

    matched_fraction_left = mcs_size / float(len(self.suptop.top1))
    matched_fraction_right = mcs_size / float(len(self.suptop.top2))
    disappearing_atoms_fraction = (len(self.suptop.top1) - mcs_size) \
                               / float(len(self.suptop.top1)) * 100
    appearing_atoms_fraction = (len(self.suptop.top2) - mcs_size) \
                               / float(len(self.suptop.top2)) * 100

    return matched_fraction_left, matched_fraction_right, disappearing_atoms_fraction, appearing_atoms_fraction