From 65c9c6ad7e6d310ea2568e9c6ac491119cb926e1 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 14 Feb 2025 12:57:43 +0000 Subject: [PATCH] Deployed d17dfd3 to latest with MkDocs 1.6.1 and mike 2.1.3 --- latest/api/config/index.html | 203 ++++++++++++++++++++++++----- latest/objects.inv | Bin 2386 -> 2423 bytes latest/reference/config/index.html | 203 ++++++++++++++++++++++++----- latest/search/search_index.json | 2 +- 4 files changed, 335 insertions(+), 73 deletions(-) diff --git a/latest/api/config/index.html b/latest/api/config/index.html index aca446b..44e1561 100644 --- a/latest/api/config/index.html +++ b/latest/api/config/index.html @@ -1037,6 +1037,24 @@ + + +
  • + + +  _guess_ligands_contain_q + + + +
  • + +
  • + + +  _get_first_ligand_net_q + + +
  • @@ -1937,6 +1955,24 @@ +
  • + +
  • + + +  _guess_ligands_contain_q + + + +
  • + +
  • + + +  _get_first_ligand_net_q + + +
  • @@ -3382,6 +3418,101 @@

    +

    + _guess_ligands_contain_q + + +

    +
    _guess_ligands_contain_q()
    +
    + +
    + +

    Checks if the first .mol2 file contains charges. +:return:

    + +
    + Source code in ties/config.py +
    456
    +457
    +458
    +459
    +460
    +461
    +462
    +463
    +464
    +465
    +466
    +467
    +468
    +469
    +470
    +471
    def _guess_ligands_contain_q(self):
    +    """
    +    Checks if the first .mol2 file contains charges.
    +    :return:
    +    """
    +    # if all ligands are .mol2, then charges are provided
    +    if all(l.suffix.lower() == '.mol2' for l in self.ligand_files):
    +        # if all atoms have q = 0 that means they're a placeholder
    +        u = parmed.load_file(str(list(self.ligand_files)[0]), structure=True)
    +        all_q_0 = all(a.charge == 0 for a in u.atoms)
    +        if all_q_0:
    +            return False
    +
    +        return True
    +
    +    return False
    +
    +
    +
    + +
    + +
    + + +

    + _get_first_ligand_net_q + + +

    +
    _get_first_ligand_net_q()
    +
    + +
    + +

    :return: Returns the net charge from the parmed file with partial charges.

    + +
    + Source code in ties/config.py +
    473
    +474
    +475
    +476
    +477
    +478
    +479
    +480
    +481
    def _get_first_ligand_net_q(self):
    +    """
    +    :return: Returns the net charge from the parmed file with partial charges.
    +    """
    +
    +    # if all atoms have q = 0 that means they're a placeholder
    +    u = parmed.load_file(str(list(self.ligand_files)[0]), structure=True)
    +    net_q = sum(a.charge == 0 for a in u.atoms)
    +    return net_q
    +
    +
    +
    + +
    + +
    + +

    get_element_map @@ -3400,27 +3531,27 @@

    Source code in ties/config.py -
    839
    -840
    -841
    -842
    -843
    -844
    -845
    -846
    -847
    -848
    -849
    -850
    -851
    -852
    -853
    -854
    -855
    +              
    855
     856
     857
     858
    -859
    @staticmethod
    +859
    +860
    +861
    +862
    +863
    +864
    +865
    +866
    +867
    +868
    +869
    +870
    +871
    +872
    +873
    +874
    +875
    @staticmethod
     def get_element_map():
         """
     
    @@ -3470,23 +3601,7 @@ 

    Source code in ties/config.py -
    865
    -866
    -867
    -868
    -869
    -870
    -871
    -872
    -873
    -874
    -875
    -876
    -877
    -878
    -879
    -880
    -881
    +              
    881
     882
     883
     884
    @@ -3504,7 +3619,23 @@ 

    896 897 898 -899

    def get_serializable(self):
    +899
    +900
    +901
    +902
    +903
    +904
    +905
    +906
    +907
    +908
    +909
    +910
    +911
    +912
    +913
    +914
    +915
    def get_serializable(self):
         """
         Get a JSON serializable structure of the config.
     
    diff --git a/latest/objects.inv b/latest/objects.inv
    index 4a05463a9bb28ccb16e9318e293addec6eaa8343..30ad7197bfeedeeea303645b0698cbd390b00c24 100644
    GIT binary patch
    delta 2324
    zcmV+v3G4RK68932e1EtNzUNm=)y_4ZN+0*OnW>~IwVU0_Y|boNqGWDUq?)9<$NBXE
    zNy*k*pseQXt!)z@_y7U$1KM(CKGfGAE0s5befZl4%?hTO%-E+NK7RP?Uo8B7`mAKZ
    zOE)YN)EF2=YyK(o-%f^|NXaZIxHgsuUQ(G8317T8@uHEL<$p?UA~ejHR@x94W+|6Z
    z5EO(Zsgz(Dtjmzba9NTn^NXd{>P>l}-bl{PABsy0Q>bbsnY3v6>ddrNN*F?=W!c4l
    z5ifjE6^oausjjli^HaQ}6^zzcvHQYEj$WHp70i!o@6UjD(nZ+?wq7ynuGoQkDh}XI1}R&1HLA}0+N
    zPiR(aR)gK@2%)&qh0efPL<%*Mzch^a!C*qu2II}pp
    zNK2k|h6uTe$_AX-73|n%7nYOT1vJqJ+;kvO@;PC$*%f7Jv|RCLkb?7n=H^
    zSUKYo#injO5JuqU_%Lg_$A;-)x1pdt>h{x3q(z|M=&pt@Jr28{dpf(Xoa+m5qxH|7
    zrGMb0zx?y-)mT==Oa2T_ET{2u|6#S@*$%MJt%-m`Q8#Ej43855woU5b
    z!L}HlJxC+EXAi^-_p=9POa$REmRKP?!ZJmK##rHtA}|)oBQ(Z3lN1HBLMx$hFdP#a
    zY>{w^LU=%QmWql(*m|zVfK5oMc;PW)6^$^WuVUqAxvW@_N3Pr&%Y_EPQC>UXmVfy#
    zJlG5wh8Esn#?UxhG}$hoMb0b^?XVm9h+mpgYq6p`Jpq<c
    z+-MK%-Iz#gVSF^l#|(5q!DJ#$ox6+cE}gSaXIWgqSp)tx9goV=4DrZF2SM`C8`3
    ziOgIfbcgW$5Z6e(Ik0NUV^sNI)ajQm^D~@|8`SD%Co&_W8;Kk4W8c;rXE2Gr34GiN+1F}shGW`OO{n18+i1j>4g00IEj
    zEr7w^g>2xKlScbrE$?TplY|z5leO$#l{RAd7zXAL4PAk071E}DLEWDyu$}o?E
    zzst;*W5GVmulEo6?|8_eY0x|;RKV4OcPoGO?20yQ+cZ5K^39O{4oIg_eTLoCx*Cj3v4h!sqXS0$Kk>b6a{%nr5aIyJy_!?(M`6=_r8iC1!nfW
    zRBKkQwPYNu{XU#mT)@q;@F&E_WGCM5?DEg5bc*|pfJrQk`xZvSjdguet3{b(wYEV?
    z6^ZnLt~Y8(uR|??Qq!DEP3ud#W%e|ZQmFI2KkdDh3xD6G0p2--!TJ@RN=Xb@aQVE^
    z$#I}IZ$6}?Me*c_h@9(Cj)!ed7J=afn9d*iszC}xQ7u1*>h+`!wU&C%d%5OXl}x5o
    zE|g^O+D@w)V!TeoW9W(u`qA%+qtj*6T^;xON}=cGg*6k@7s@U?i4(T!UtfRycM45y
    zt}=h134ibJD;3;#z$Mffx>+h_DQKn%HLkS<9jf>4v9U+e^
    z>6*mPE|c0Qlkd}TZ}t9GhE)iNyN4^m6x@%|)8?J;fPDKGlu)~FG+pm`*r%!g79vU>
    uooqpFQIhw_;zB=(`^QY!ma}7!y}6o1YDhuN@eKuuB%TD(ZSa4BIs0YHLWsbf%yOmU>?AVoc&M1fkDR@AD1AsHD{Q5Ki
    zQsSEpii7oSB%!~?qtTDAF_ts)p}ziDsk{;F!{0t=Rxr(E#y
    zx?!21#=t0A^G})ob~5Zl1urG3lwcV^8qydpOHyTC54Bcr%6|*>MsjZcP+VG)sj60z
    zNsE@R&P-dSgdtQ~mRMFZDKgCO0!Dx*YyDyC7=(Sl@!Th);F9iM;
    zNHKQ+f_)3~BV3}zl+L-N7EGYv0=tzZvxEs&!GV)1>_^ea%yK0Wl7?vx_V+(}4hoT?
    zV6aLv0z0Zpn160qv1Q_loHSTGp;@h24R)&|gyKdQI)i%}Dbz^*(lFu=U(yPe7bQ<+
    zPKqLSyw?;d1U6#gL?oPeII}ppNJ|b<&Mc&*T#^^0Tt#IA&g=?yY_ki?$?XD~XasIL
    zkSO__Fj;bm-Jko05_{3iT$IR>qTa7mO4pZQr7>pKmw&BP2Ck498x8P?p+{cbS}V)Y
    z_S25WUn9*R`dQ7-jb-4g)hlzWYYhO2VVW9zA%srDodYDsQf*xzc6)`yXbijgVwnF3
    zG6LjsJ!_s5mk0<(aXY6=Q{&i#KgtKNKrxT%y?2tp~yg+#DZfP50O^J?u6Vv`5{3
    zx{0(16dc{v@TIz}`?;sH`^ve#5I0)?+*t}v`pZARUX5i{yyVZ|#Bv%h_a9aZp6w9)
    z{eN@hR5QUB_7i>?jywINOYp^peRh|D>3JQSVke=>()kGSDn@1#g93@b(^;t)2v-RW
    zfmkJ55>yQl0~r~
    zk6gJm8VL=8ei&s9ivdq*NT*VK?#-zcl5pVnug)0)H&4
    zg$Be@TxdWv-xY252g
    z1vnabz!|hP#!jsHIqn71jCx8+f~8ZJh|dukSTy#{I%?3xhZOJ93ccjZMhTl
    zM!A6p0*C!_2n=|Jd^b>7pwCFn1Aic%!y{+qVyxe(36a^a@xb^LY~rRdOr^hgKqAj5m!5X5h$~5
    zzioLer+sVwtzJFfEc`f;nSbjv?hw8o;u@(p2Uab4j4B_DI{orxeumR=gJ#(5L}p}k
    zBXPrh?AxkwhJzNRPp}xr=Z8Etpge`0ID62!P)A}BQUm_^azN>emzEZv30*L{HM4+^?zsu-|Nz#!&i@6
    zoV#S*HC@!#iiJ9zu4EO<5ZEo5-SdSFS~$Cpt5~O>z=Y;EU_ahz
    zg%^4N6&#U;Dte_2^Emjs%zQZ(?8E$e|B(OQj(BhU6N#)*k5+xBuDQ-u;@XjV19_%o
    z8-{9^yl2;JEtDOpK7W)Q_cjO7ZUXBy$g%p}QImT)|Lz)1MYSU^?2A<*ymY8}$Mn3v
    zcBO3~>EPs{D$bycDZ%c*+FxK!c7@tGa2hnv2^Da)pl;=_zK(K+mqfLHQ`EJZ2QP}=
    zU3FcHYme@B8Dk>%(pE+xnc{hp<+g2nLyWs&LAAM76|b=Nhp972lot7&wPjn6X4xCMOlwTIpn}gF3j#
    zif3ug)aNF3&1l*P>C}dFB$>LLoA0#H?(IR4@kM7U)m{GMI9%9)q98A+R3jR&2WuQ9
    zy2&>A{;ncOfq$8OFV&irYfmx`)_xz(D=y$>S@;v;W3m(PzwGj5RXWA}hk!{ejr%8z
    zh8yeNNv$?zj@8-*B~>KS2fE&=FNwcv?!h&5s`Bp+VQZ>$$ug+RDkLHq3;@`P!#p#=TN<#
    z)S=c=?|CoRT&t4Fl*)yY465z4sv*YfL_CJB$lyKtJ#lopY`UxCs;?A!ZeCb3L4Bj_
    z!jm{*tN!)%*MFzb)aEMl2b%EyzEZ*c3%G(Q`?u4n4wS`aTn9#_&m5
       
       
    +
    +        
    +          
  • + + +  _guess_ligands_contain_q + + + +
  • + +
  • + + +  _get_first_ligand_net_q + + +
  • @@ -1966,6 +1984,24 @@ +
  • + +
  • + + +  _guess_ligands_contain_q + + + +
  • + +
  • + + +  _get_first_ligand_net_q + + +
  • @@ -3460,6 +3496,101 @@

    +

    + _guess_ligands_contain_q + + +

    +
    _guess_ligands_contain_q()
    +
    + +
    + +

    Checks if the first .mol2 file contains charges. +:return:

    + +
    + Source code in ties/config.py +
    456
    +457
    +458
    +459
    +460
    +461
    +462
    +463
    +464
    +465
    +466
    +467
    +468
    +469
    +470
    +471
    def _guess_ligands_contain_q(self):
    +    """
    +    Checks if the first .mol2 file contains charges.
    +    :return:
    +    """
    +    # if all ligands are .mol2, then charges are provided
    +    if all(l.suffix.lower() == '.mol2' for l in self.ligand_files):
    +        # if all atoms have q = 0 that means they're a placeholder
    +        u = parmed.load_file(str(list(self.ligand_files)[0]), structure=True)
    +        all_q_0 = all(a.charge == 0 for a in u.atoms)
    +        if all_q_0:
    +            return False
    +
    +        return True
    +
    +    return False
    +
    +
    +
    + +
    + +
    + + +

    + _get_first_ligand_net_q + + +

    +
    _get_first_ligand_net_q()
    +
    + +
    + +

    :return: Returns the net charge from the parmed file with partial charges.

    + +
    + Source code in ties/config.py +
    473
    +474
    +475
    +476
    +477
    +478
    +479
    +480
    +481
    def _get_first_ligand_net_q(self):
    +    """
    +    :return: Returns the net charge from the parmed file with partial charges.
    +    """
    +
    +    # if all atoms have q = 0 that means they're a placeholder
    +    u = parmed.load_file(str(list(self.ligand_files)[0]), structure=True)
    +    net_q = sum(a.charge == 0 for a in u.atoms)
    +    return net_q
    +
    +
    +
    + +
    + +
    + +

    get_element_map @@ -3478,27 +3609,27 @@

    Source code in ties/config.py -
    839
    -840
    -841
    -842
    -843
    -844
    -845
    -846
    -847
    -848
    -849
    -850
    -851
    -852
    -853
    -854
    -855
    +              
    855
     856
     857
     858
    -859
    @staticmethod
    +859
    +860
    +861
    +862
    +863
    +864
    +865
    +866
    +867
    +868
    +869
    +870
    +871
    +872
    +873
    +874
    +875
    @staticmethod
     def get_element_map():
         """
     
    @@ -3548,23 +3679,7 @@ 

    Source code in ties/config.py -
    865
    -866
    -867
    -868
    -869
    -870
    -871
    -872
    -873
    -874
    -875
    -876
    -877
    -878
    -879
    -880
    -881
    +              
    881
     882
     883
     884
    @@ -3582,7 +3697,23 @@ 

    896 897 898 -899

    def get_serializable(self):
    +899
    +900
    +901
    +902
    +903
    +904
    +905
    +906
    +907
    +908
    +909
    +910
    +911
    +912
    +913
    +914
    +915
    def get_serializable(self):
         """
         Get a JSON serializable structure of the config.
     
    diff --git a/latest/search/search_index.json b/latest/search/search_index.json
    index 27ab4f4..6dda64c 100644
    --- a/latest/search/search_index.json
    +++ b/latest/search/search_index.json
    @@ -1 +1 @@
    -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Overview","text":"

    TIES is a python library for the relative binding free energy (RBFE) calculations.

    TIES superimposes ligands and prepares the input files for molecular dynamics simulations. These simulations can be carried out with either NAMD or OpenMM using the package TIES_MD.

    For the details about the protocol and its validation please refer to the following publications

    Mateusz K. Bieniek, Agastya P. Bhati, Shunzhou Wan, and Peter V. Coveney. Ties 20: relative binding free energy with a flexible superimposition algorithm and partial ring morphing. Journal of Chemical Theory and Computation, 17(2):1250\u20131265, 2021. PMID: 33486956. doi:10.1021/acs.jctc.0c01179.

    "},{"location":"installation/","title":"Installation","text":"

    The easiest way to start is to use the latest published conda-forge package. Create a new environment and use:

    conda install ties\n
    "},{"location":"installation/#development-version","title":"Development version","text":"

    Whereas most of the dependancies can be installed with pip, ambertools has to be either compiled. The easiest way is to use the conda-forge environment:

    mamba env create -f environment.yml\nconda activate ties \npip install --no-deps . \n
    "},{"location":"publications/","title":"Publications","text":"

    We list here a list of publication that utilised the software for carrying our RBFE calculations.

    Mateusz K. Bieniek, Agastya P. Bhati, Shunzhou Wan, and Peter V. Coveney. Ties 20: relative binding free energy with a flexible superimposition algorithm and partial ring morphing. Journal of Chemical Theory and Computation, 17(2):1250\u20131265, 2021. PMID: 33486956. doi:10.1021/acs.jctc.0c01179.

    Mateusz K Bieniek, Alexander D Wade, Agastya P Bhati, Shunzhou Wan, and Peter V Coveney. TIES 2.0: A Dual-Topology Open Source Relative Binding Free Energy Builder with Web Portal. Journal of Chemical Information and Modeling, 63(3):718\u2013724, 2023. URL: https://doi.org/10.1021/acs.jcim.2c01596, doi:10.1021/acs.jcim.2c01596.

    "},{"location":"superimposition/","title":"Superimposition","text":"

    superimposition

    "},{"location":"theory/","title":"TIES Protocol","text":""},{"location":"theory/#superimposition-and-defining-the-alchemical-region","title":"Superimposition and defining the alchemical region","text":"

    Any two pairs are superimposed using a recursive joint traversal of two molecules starting from any two pairs.

    A heuristics (on by default) reduces the search space by selecting the rarer atoms that are present across the two molecules as the starting points for the traversal, decreasing substantially the computational cost.

    "},{"location":"theory/#charge-treatment","title":"Charge treatment","text":"

    TIES 20 supports the transformation between ligands that have the same net charge.

    We employ a dual topology approach which divides the atoms in each transformation into three groups:

    1. Joint region. This is the region of the molecule where the atoms are the same meaning that they are shared across the two ligands in the transformation.
    2. Disappearing region. Atoms present only in the starting ligand of the transformation which are fully represented at lambda=0 and which will be scaled accordingly during the lambda progression.
    3. Appearing region. Atoms present only in the ending ligand of the transformation and therefore not present at lambda=0. These atoms start appearing during the lambda progression and are fully represented at lambda=1.

    When the two ligands in a transformation are superimposed together, the treatment of charges depends on which group they belong to.

    "},{"location":"theory/#joint-region-matched-atoms-and-their-charges","title":"Joint region: matched atoms and their charges","text":"

    In the joint region of the transformation, first --q-pair-tolerance is used to determine whether the two original atoms are truly the same atoms. If their charges differ by more than this value (default 0.1e), then the two atoms will be added to the alchemical regions (Disappearing and appearing).

    It is possible that a lot of matched atoms in the joint region, with each pair being within 0.1e of each other, cumulatively have rather different charges between the starting and the ending ligand. For this reason, TIES 20 sums the differences between the starting and the ending atoms in the joint region, and if the total is larger than -netqtol (default 0.1e) then we further expand the alchemical region until the \"appearing\" and \"disappearing\" regions in the joint region are of a sufficiently similar net charge.

    Abiding by -netqtol rule has the further effect that, inversely, the alchemical regions (disappearing and appearing regions), will have very similar net charges - which is a necessary condition for the calculation of the partial derivative of the potential energy with respect to the lambda.

    If -netqtol rule is violated, different schemes for the removal of the matched atoms in the joint region are tried to satisfy the net charge limit. The scheme that removes fewest matched pairs, is used. In other words, TIES 20 is trying to use the smallest alchemical region possible while satisfying the rule.

    Note that we are not summing together the absolute differences in charges in the joint region. This means that if one atom pair has 0.02e charge difference, and another pair has -0.02e charge difference, then their total is zero. In other words, we are not worried about the distribution of the differences in charges in the joint region.

    The hydrogen charges are considered by absorbing them into the heavy atoms.

    The charges in the joint region for each pair are averaged.

    The last step is redistribution, where the final goal is that the net charge is the same in the Appearing and in the Disappearing alchemical region. After averaging the charges in the joint region, its overall charge summed with the charge of each alchemical region should be equal to the whole molecule net charge: :math:q_{joint} + q_{appearing} == q_{joint} + q_{disappearing} == q_{molecule}. Therefore, after averaging the charges, :math:q_{molecule} - q_{joint} - q_{appearing} is distributed equally in the region :math:q_{appearing}. The same rule is applied in :math:q_{disappearing}.

    "},{"location":"api/config/","title":"Config","text":""},{"location":"api/config/#ties.Config","title":"Config","text":"
    Config(**kwargs)\n

    The configuration with parameters that can be used to define the entire protocol. The settings can be overridden later in the actual classes.

    The settings are stored as properties in the object and can be overwritten.

    Methods:

    • get_element_map \u2013

      :return:

    • get_serializable \u2013

      Get a JSON serializable structure of the config.

    Attributes:

    • workdir \u2013

      Working directory for antechamber calls.

    • protein \u2013

      Path to the protein

    • ligand_files \u2013

      A list of ligand filenames.

    • ambertools_home \u2013

      Ambertools HOME path. If not configured, the env variable AMBERHOME as AMBER_PREFIX will be checked.

    • ambertools_antechamber \u2013

      Antechamber path based on the .ambertools_home

    • ambertools_parmchk2 \u2013

      Parmchk2 path based on the .ambertools_home

    • ambertools_tleap \u2013

      Tleap path based on the .ambertools_home

    • antechamber_dr \u2013

      Whether to use -dr setting when calling antechamber.

    • ligand_net_charge \u2013

      The ligand charge. If not provided, neutral charge is assumed.

    • coordinates_file \u2013

      A file from which coordinate can be taken.

    • atom_pair_q_atol \u2013

      It defines the maximum difference in charge

    • net_charge_threshold \u2013

      Defines how much the superimposed regions can, in total, differ in charge.

    • ignore_charges_completely \u2013

      Ignore the charges during the superimposition. Useful for debugging.

    • allow_disjoint_components \u2013

      Defines whether there might be multiple superimposed areas that are

    • use_element_in_superimposition \u2013

      Use element rather than the actual atom type for the superimposition

    • align_molecules_using_mcs \u2013

      After determining the maximum common substructure (MCS),

    • use_original_coor \u2013

      Antechamber when assigning charges can modify the charges slightly.

    • ligands_contain_q \u2013

      If not provided, it tries to deduce whether charges are provided.

    • superimposition_starting_pair \u2013

      Set a starting pair for the superimposition to narrow down the MCS search.

    • manually_matched_atom_pairs \u2013

      Either a list of pairs or a file with a list of pairs of atoms

    • manually_mismatched_pairs \u2013

      A path to a file with a list of a pairs that should be mismatched.

    • protein_ff \u2013

      The protein forcefield to be used by ambertools for the protein parameterisation.

    • md_engine \u2013

      The MD engine, with the supported values NAMD2.13, NAMD2.14, NAMD3 and OpenMM

    • ligand_ff \u2013

      The forcefield for the ligand.

    • ligand_ff_name \u2013

      Either GAFF or GAFF2

    • redistribute_q_over_unmatched \u2013

      The superimposed and matched atoms have every slightly different charges.

    • use_hybrid_single_dual_top \u2013

      Hybrid single dual topology (experimental). Currently not implemented.

    • ligand_tleap_in \u2013

      The name of the tleap input file for ambertools for the ligand.

    • complex_tleap_in \u2013

      The tleap input file for the complex.

    • prep_dir \u2013

      Path to the prep directory. Currently in the workdir

    • pair_morphfrcmods_dir \u2013

      Path to the .frcmod files for the morph.

    • pair_morphfrmocs_tests_dir \u2013

      Path to the location where a test is carried out with .frcmod

    • pair_unique_atom_names_dir \u2013

      Location of the morph files with unique filenames.

    • lig_unique_atom_names_dir \u2013

      Directory location for files with unique atom names.

    • lig_frcmod_dir \u2013

      Directory location with the .frcmod created for each ligand.

    • lig_acprep_dir \u2013

      Directory location where the .ac charges are converted into the .mol2 format.

    • lig_dir \u2013

      Directory location with the .mol2 files.

    Source code in ties/config.py
    def __init__(self, **kwargs):\n    # set the path to the scripts\n    self.code_root = pathlib.Path(os.path.dirname(__file__))\n\n    # scripts/input files,\n    # these are specific to the host\n    self.script_dir = self.code_root / 'scripts'\n    self.namd_script_dir = self.script_dir / 'namd'\n    self.ambertools_script_dir = self.script_dir / 'ambertools'\n    self.tleap_check_protein = self.ambertools_script_dir / 'check_prot.in'\n    self.vmd_vis_script = self.script_dir / 'vmd' / 'vis_morph.vmd'\n    self.vmd_vis_script_sh = self.script_dir / 'vmd' / 'vis_morph.sh'\n\n    self._workdir = None\n    self._antechamber_dr = False\n    self._ambertools_home = None\n\n    self._protein = None\n\n    self._ligand_net_charge = None\n    self._atom_pair_q_atol = 0.1\n    self._net_charge_threshold = 0.1\n    self._redistribute_q_over_unmatched = True\n    self._allow_disjoint_components = False\n    # use only the element in the superimposition rather than the specific atom type\n    self._use_element = False\n    self._use_element_in_superimposition = True\n    self.starting_pairs_heuristics = True\n    # weights in choosing the best MCS, the weighted sum of \"(1 - MCS fraction) and RMSD\".\n    self.weights = [1, 0.5]\n\n    # coordinates\n    self._align_molecules_using_mcs = False\n    self._use_original_coor = False\n    self._coordinates_file = None\n\n    self._ligand_files = set()\n    self._manually_matched_atom_pairs = None\n    self._manually_mismatched_pairs = None\n    self._ligands_contain_q = None\n\n    self._ligand_tleap_in = None\n    self._complex_tleap_in = None\n\n    self._superimposition_starting_pair = None\n\n    self._protein_ff = None\n    self._ligand_ff = 'leaprc.gaff'\n    self._ligand_ff_name = 'gaff'\n\n    # MD/NAMD production input file\n    self._md_engine = 'namd'\n    #default to modern CPU version\n    self.namd_version = '2.14'\n    self._lambda_rep_dir_tree = False\n\n    # experimental\n    self._use_hybrid_single_dual_top = False\n    self._ignore_charges_completely = False\n\n    self.ligands = None\n\n    # if True, do not allow ligands with the same ligand name\n    self.uses_cmd = False\n\n    # assign all the initial configuration values\n    self.set_configs(**kwargs)\n
    "},{"location":"api/config/#ties.Config.workdir","title":"workdir property writable","text":"
    workdir\n

    Working directory for antechamber calls. If None, a temporary directory in /tmp/ will be used.

    :return: Work dir :rtype: str

    "},{"location":"api/config/#ties.Config.protein","title":"protein property writable","text":"
    protein\n

    Path to the protein

    :return: Protein filename :rtype: str

    "},{"location":"api/config/#ties.Config.ligand_files","title":"ligand_files property writable","text":"
    ligand_files\n

    A list of ligand filenames. :return:

    "},{"location":"api/config/#ties.Config.ambertools_home","title":"ambertools_home property writable","text":"
    ambertools_home\n

    Ambertools HOME path. If not configured, the env variable AMBERHOME as AMBER_PREFIX will be checked.

    :return: ambertools path

    "},{"location":"api/config/#ties.Config.ambertools_antechamber","title":"ambertools_antechamber property","text":"
    ambertools_antechamber\n

    Antechamber path based on the .ambertools_home

    :return:

    "},{"location":"api/config/#ties.Config.ambertools_parmchk2","title":"ambertools_parmchk2 property","text":"
    ambertools_parmchk2\n

    Parmchk2 path based on the .ambertools_home :return:

    "},{"location":"api/config/#ties.Config.ambertools_tleap","title":"ambertools_tleap property","text":"
    ambertools_tleap\n

    Tleap path based on the .ambertools_home :return:

    "},{"location":"api/config/#ties.Config.antechamber_dr","title":"antechamber_dr property writable","text":"
    antechamber_dr\n

    Whether to use -dr setting when calling antechamber.

    :return:

    "},{"location":"api/config/#ties.Config.ligand_net_charge","title":"ligand_net_charge property writable","text":"
    ligand_net_charge\n

    The ligand charge. If not provided, neutral charge is assumed. The charge is necessary for calling antechamber (-nc).

    :return:

    "},{"location":"api/config/#ties.Config.coordinates_file","title":"coordinates_file property writable","text":"
    coordinates_file\n

    A file from which coordinate can be taken.

    :return:

    "},{"location":"api/config/#ties.Config.atom_pair_q_atol","title":"atom_pair_q_atol property writable","text":"
    atom_pair_q_atol\n

    It defines the maximum difference in charge between any two superimposed atoms a1 and a2. If the two atoms differ in charge more than this value, they will be unmatched and added to the alchemical regions.

    :return: default (0.1e) :rtype: float

    "},{"location":"api/config/#ties.Config.net_charge_threshold","title":"net_charge_threshold property writable","text":"
    net_charge_threshold\n

    Defines how much the superimposed regions can, in total, differ in charge. If the total exceeds the thresholds, atom pairs will be unmatched until the threshold is met.

    :return: default (0.1e) :rtype: float

    "},{"location":"api/config/#ties.Config.ignore_charges_completely","title":"ignore_charges_completely property writable","text":"
    ignore_charges_completely\n

    Ignore the charges during the superimposition. Useful for debugging. :return: default (False) :rtype: bool

    "},{"location":"api/config/#ties.Config.allow_disjoint_components","title":"allow_disjoint_components property writable","text":"
    allow_disjoint_components\n

    Defines whether there might be multiple superimposed areas that are separated by alchemical region.

    :return: default (False) :rtype: bool

    "},{"location":"api/config/#ties.Config.use_element_in_superimposition","title":"use_element_in_superimposition property writable","text":"
    use_element_in_superimposition\n

    Use element rather than the actual atom type for the superimposition during the joint-traversal of the two molecules.

    :return: default (False) :rtype: bool

    "},{"location":"api/config/#ties.Config.align_molecules_using_mcs","title":"align_molecules_using_mcs property writable","text":"
    align_molecules_using_mcs\n

    After determining the maximum common substructure (MCS), use it to align the coordinates of the second molecule to the first.

    :return: default (False) :rtype: bool

    "},{"location":"api/config/#ties.Config.use_original_coor","title":"use_original_coor property writable","text":"
    use_original_coor\n

    Antechamber when assigning charges can modify the charges slightly. If that's the case, use the original charges in order to correct this slight divergence in coordinates.

    :return: default (?) :rtype: bool

    "},{"location":"api/config/#ties.Config.ligands_contain_q","title":"ligands_contain_q property writable","text":"
    ligands_contain_q\n

    If not provided, it tries to deduce whether charges are provided. If all charges are set to 0, then it assumes that charges are not provided.

    If set to False explicitly, charges are ignored and computed again.

    :return: default (None) :rtype: bool

    "},{"location":"api/config/#ties.Config.superimposition_starting_pair","title":"superimposition_starting_pair property writable","text":"
    superimposition_starting_pair\n

    Set a starting pair for the superimposition to narrow down the MCS search. E.g. \"C2-C12\"

    :rtype: str

    "},{"location":"api/config/#ties.Config.manually_matched_atom_pairs","title":"manually_matched_atom_pairs property writable","text":"
    manually_matched_atom_pairs\n

    Either a list of pairs or a file with a list of pairs of atoms that should be superimposed/matched.

    :return:

    "},{"location":"api/config/#ties.Config.manually_mismatched_pairs","title":"manually_mismatched_pairs property writable","text":"
    manually_mismatched_pairs\n

    A path to a file with a list of a pairs that should be mismatched.

    "},{"location":"api/config/#ties.Config.protein_ff","title":"protein_ff property writable","text":"
    protein_ff\n

    The protein forcefield to be used by ambertools for the protein parameterisation.

    :return: default (leaprc.ff19SB) :rtype: string

    "},{"location":"api/config/#ties.Config.md_engine","title":"md_engine property writable","text":"
    md_engine\n

    The MD engine, with the supported values NAMD2.13, NAMD2.14, NAMD3 and OpenMM

    :return: NAMD2.13, NAMD2.14, NAMD3 and OpenMM :rtype: string

    "},{"location":"api/config/#ties.Config.ligand_ff","title":"ligand_ff property","text":"
    ligand_ff\n

    The forcefield for the ligand.

    "},{"location":"api/config/#ties.Config.ligand_ff_name","title":"ligand_ff_name property writable","text":"
    ligand_ff_name\n

    Either GAFF or GAFF2

    :return:

    "},{"location":"api/config/#ties.Config.redistribute_q_over_unmatched","title":"redistribute_q_over_unmatched property writable","text":"
    redistribute_q_over_unmatched\n

    The superimposed and matched atoms have every slightly different charges. Taking an average charge between any two atoms introduces imbalances in the net charge of the alchemical regions, due to the different charge distribution.

    :return: default(True)

    "},{"location":"api/config/#ties.Config.use_hybrid_single_dual_top","title":"use_hybrid_single_dual_top property writable","text":"
    use_hybrid_single_dual_top\n

    Hybrid single dual topology (experimental). Currently not implemented.

    :return: default(False).

    "},{"location":"api/config/#ties.Config.ligand_tleap_in","title":"ligand_tleap_in property","text":"
    ligand_tleap_in\n

    The name of the tleap input file for ambertools for the ligand.

    :return: Default ('leap_ligand.in') :rtype: string

    "},{"location":"api/config/#ties.Config.complex_tleap_in","title":"complex_tleap_in property","text":"
    complex_tleap_in\n

    The tleap input file for the complex.

    :return: Default 'leap_complex.in' :type: string

    "},{"location":"api/config/#ties.Config.prep_dir","title":"prep_dir property","text":"
    prep_dir\n

    Path to the prep directory. Currently in the workdir

    :return: Default (workdir/prep)

    "},{"location":"api/config/#ties.Config.pair_morphfrcmods_dir","title":"pair_morphfrcmods_dir property","text":"
    pair_morphfrcmods_dir\n

    Path to the .frcmod files for the morph.

    :return: Default (workdir/prep/morph_frcmods)

    "},{"location":"api/config/#ties.Config.pair_morphfrmocs_tests_dir","title":"pair_morphfrmocs_tests_dir property","text":"
    pair_morphfrmocs_tests_dir\n

    Path to the location where a test is carried out with .frcmod

    :return: Default (workdir/prep/morph_frcmods/tests)

    "},{"location":"api/config/#ties.Config.pair_unique_atom_names_dir","title":"pair_unique_atom_names_dir property","text":"
    pair_unique_atom_names_dir\n

    Location of the morph files with unique filenames.

    :return: Default (workdir/prep/morph_unique_atom_names)

    "},{"location":"api/config/#ties.Config.lig_unique_atom_names_dir","title":"lig_unique_atom_names_dir property","text":"
    lig_unique_atom_names_dir\n

    Directory location for files with unique atom names.

    :return: Default (workdir/prep/unique_atom_names)

    "},{"location":"api/config/#ties.Config.lig_frcmod_dir","title":"lig_frcmod_dir property","text":"
    lig_frcmod_dir\n

    Directory location with the .frcmod created for each ligand.

    :return: Default (workdir/prep/ligand_frcmods)

    "},{"location":"api/config/#ties.Config.lig_acprep_dir","title":"lig_acprep_dir property","text":"
    lig_acprep_dir\n

    Directory location where the .ac charges are converted into the .mol2 format.

    :return: Default (workdir/prep/acprep_to_mol2)

    "},{"location":"api/config/#ties.Config.lig_dir","title":"lig_dir property","text":"
    lig_dir\n

    Directory location with the .mol2 files.

    :return: Default (workdir/mol2)

    "},{"location":"api/config/#ties.Config.get_element_map","title":"get_element_map staticmethod","text":"
    get_element_map()\n

    :return:

    Source code in ties/config.py
    @staticmethod\ndef get_element_map():\n    \"\"\"\n\n\n    :return:\n    \"\"\"\n    # Get the mapping of atom types to elements\n    element_map_filename = pathlib.Path(os.path.dirname(__file__)) / 'data' / 'element_atom_type_map.txt'\n    # remove the comments lines with #\n    lines = filter(lambda l: not l.strip().startswith('#') and not l.strip() == '', open(element_map_filename).readlines())\n    # convert into a dictionary\n\n    element_map = {}\n    for line in lines:\n        element, atom_types = line.split('=')\n\n        for atom_type in atom_types.split():\n            element_map[atom_type.strip()] = element.strip()\n\n    return element_map\n
    "},{"location":"api/config/#ties.Config.get_serializable","title":"get_serializable","text":"
    get_serializable()\n

    Get a JSON serializable structure of the config.

    pathlib.Path is not JSON serializable, so replace it with str

    todo - consider capturing all information about the system here, including each suptop.get_serializable() so that you can record specific information such as the charge changes etc.

    :return: Dictionary {key:value} with the settings :rtype: Dictionary

    Source code in ties/config.py
    def get_serializable(self):\n    \"\"\"\n    Get a JSON serializable structure of the config.\n\n    pathlib.Path is not JSON serializable, so replace it with str\n\n    todo - consider capturing all information about the system here,\n    including each suptop.get_serializable() so that you can record\n    specific information such as the charge changes etc.\n\n    :return: Dictionary {key:value} with the settings\n    :rtype: Dictionary\n    \"\"\"\n\n    host_specific = ['code_root', 'script_dir0', 'namd_script_dir',\n                     'ambertools_script_dir', 'tleap_check_protein', 'vmd_vis_script']\n\n    ser = {}\n    for k, v in self.__dict__.items():\n        if k in host_specific:\n            continue\n\n        if type(v) is pathlib.PosixPath:\n            v = str(v)\n\n        # account for the ligands being pathlib objects\n        if k == 'ligands' and v is not None:\n            # a list of ligands, convert to strings\n            v = [str(l) for l in v]\n        if k == '_ligand_files':\n            continue\n\n        ser[k] = v\n\n    return ser\n
    "},{"location":"api/ligand/","title":"Ligand","text":""},{"location":"api/ligand/#ties.Ligand","title":"Ligand","text":"
    Ligand(ligand, config=None, save=True)\n

    The ligand helper class. Helps to load and manage the different copies of the ligand file. Specifically, it tracks the different copies of the original input files as it is transformed (e.g. charge assignment).

    :param ligand: ligand filepath :type ligand: string :param config: Optional configuration from which the relevant ligand settings can be used :type config: :class:Config :param save: write a file with unique atom names for further inspection :type save: bool

    Methods:

    • convert_acprep_to_mol2 \u2013

      If the file is not a prep/ac file, this function does not do anything.

    • are_atom_names_correct \u2013

      Checks if atom names:

    • correct_atom_names \u2013

      Ensure that each atom name:

    • antechamber_prepare_mol2 \u2013

      Converts the ligand into a .mol2 format.

    • removeDU_atoms \u2013

      Ambertools antechamber creates sometimes DU dummy atoms.

    • generate_frcmod \u2013

      params

    • overwrite_coordinates_with \u2013

      Load coordinates from another file and overwrite the coordinates in the current file.

    Attributes:

    • renaming_map \u2013

      Otherwise, key: newName, value: oldName.

    Source code in ties/ligand.py
    def __init__(self, ligand, config=None, save=True):\n    \"\"\"Constructor method\n    \"\"\"\n\n    self.save = save\n    # save workplace root\n    self.config = Config() if config is None else config\n    self.config.ligand_files = ligand\n\n    self.original_input = Path(ligand).absolute()\n\n    # internal name without an extension\n    self.internal_name = self.original_input.stem\n\n    # ligand names have to be unique\n    if self.internal_name in Ligand._USED_FILENAMES and self.config.uses_cmd:\n        raise ValueError(f'ERROR: the ligand filename {self.internal_name} is not unique in the list of ligands. ')\n    else:\n        Ligand._USED_FILENAMES.add(self.internal_name)\n\n    # last used representative Path file\n    self.current = self.original_input\n\n    # internal index\n    # TODO - move to config\n    self.index = Ligand.LIG_COUNTER\n    Ligand.LIG_COUNTER += 1\n\n    self._renaming_map = None\n    self.ligand_with_uniq_atom_names = None\n\n    # If .ac format (ambertools, similar to .pdb), convert it to .mol2 using antechamber\n    self.convert_acprep_to_mol2()\n
    "},{"location":"api/ligand/#ties.Ligand.renaming_map","title":"renaming_map property writable","text":"
    renaming_map\n

    Otherwise, key: newName, value: oldName.

    If None, means no renaming took place.

    "},{"location":"api/ligand/#ties.Ligand.convert_acprep_to_mol2","title":"convert_acprep_to_mol2","text":"
    convert_acprep_to_mol2()\n

    If the file is not a prep/ac file, this function does not do anything. Antechamber is called to convert the .prepi/.prep/.ac file into a .mol2 file.

    Returns: the name of the original file, or of it was .prepi, a new filename with .mol2

    Source code in ties/ligand.py
    def convert_acprep_to_mol2(self):\n    \"\"\"\n    If the file is not a prep/ac file, this function does not do anything.\n    Antechamber is called to convert the .prepi/.prep/.ac file into a .mol2 file.\n\n    Returns: the name of the original file, or of it was .prepi, a new filename with .mol2\n    \"\"\"\n\n    if self.current.suffix.lower() not in ('.ac', '.prep'):\n        return\n\n    filetype = {'.ac': 'ac', '.prep': 'prepi'}[self.current.suffix.lower()]\n\n    cwd = self.config.lig_acprep_dir / self.internal_name\n    if not cwd.is_dir():\n        cwd.mkdir(parents=True, exist_ok=True)\n\n    # prepare the .mol2 files with antechamber (ambertools), assign BCC charges if necessary\n    logger.debug(f'Antechamber: converting {filetype} to mol2')\n    new_current = cwd / (self.internal_name + '.mol2')\n\n    log_filename = cwd / \"antechamber_conversion.log\"\n    with open(log_filename, 'w') as LOG:\n        try:\n            subprocess.run([self.config.ambertools_antechamber,\n                            '-i', self.current, '-fi', filetype,\n                            '-o', new_current, '-fo', 'mol2',\n                            '-dr', self.config.antechamber_dr],\n                           stdout=LOG, stderr=LOG,\n                           check=True, text=True,\n                           cwd=cwd, timeout=30)\n        except subprocess.CalledProcessError as E:\n            raise Exception('An error occurred during the antechamber conversion from .ac to .mol2 data type. '\n                            f'The output was saved in the directory: {cwd}'\n                            f'Please see the log file for the exact error information: {log_filename}') from E\n\n    # update\n    self.original_ac = self.current\n    self.current = new_current\n    logger.debug(f'Converted .ac file to .mol2. The location of the new file: {self.current}')\n
    "},{"location":"api/ligand/#ties.Ligand.are_atom_names_correct","title":"are_atom_names_correct","text":"
    are_atom_names_correct()\n
    Checks if atom names
    • are unique
    • have a correct format \"LettersNumbers\" e.g. C17
    Source code in ties/ligand.py
    def are_atom_names_correct(self):\n    \"\"\"\n    Checks if atom names:\n     - are unique\n     - have a correct format \"LettersNumbers\" e.g. C17\n    \"\"\"\n    ligand = parmed.load_file(str(self.current), structure=True)\n    atom_names = [a.name for a in ligand.atoms]\n\n    are_uniqe = len(set(atom_names)) == len(atom_names)\n\n    return are_uniqe and self._do_atom_names_have_correct_format(atom_names)\n
    "},{"location":"api/ligand/#ties.Ligand._do_atom_names_have_correct_format","title":"_do_atom_names_have_correct_format staticmethod","text":"
    _do_atom_names_have_correct_format(names)\n

    Check if the atom name is followed by a number, e.g. \"C15\" Note that the full atom name cannot be more than 4 characters. This is because the PDB format does not allow for more characters which can lead to inconsistencies.

    :param names: a list of atom names :type names: list[str] :return True if they all follow the correct format.

    Source code in ties/ligand.py
    @staticmethod\ndef _do_atom_names_have_correct_format(names):\n    \"\"\"\n    Check if the atom name is followed by a number, e.g. \"C15\"\n    Note that the full atom name cannot be more than 4 characters.\n    This is because the PDB format does not allow for more\n    characters which can lead to inconsistencies.\n\n    :param names: a list of atom names\n    :type names: list[str]\n    :return True if they all follow the correct format.\n    \"\"\"\n    for name in names:\n        # cannot exceed 4 characters\n        if len(name) > 4:\n            return False\n\n        # count letters before any digit\n        letter_count = 0\n        for letter in name:\n            if not letter.isalpha():\n                break\n\n            letter_count += 1\n\n        # at least one character\n        if letter_count == 0:\n            return False\n\n        # extrac the number suffix\n        atom_number = name[letter_count:]\n        try:\n            int(atom_number)\n        except:\n            return False\n\n    return True\n
    "},{"location":"api/ligand/#ties.Ligand.correct_atom_names","title":"correct_atom_names","text":"
    correct_atom_names()\n
    Ensure that each atom name
    • is unique
    • has letter followed by digits
    • has max 4 characters

    E.g. C17, NX23

    :param self.save: if the path is provided, the updated file will be saved with the unique names and a handle to the new file (ParmEd) will be returned.

    Source code in ties/ligand.py
    def correct_atom_names(self):\n    \"\"\"\n    Ensure that each atom name:\n     - is unique\n     - has letter followed by digits\n     - has max 4 characters\n    E.g. C17, NX23\n\n    :param self.save: if the path is provided, the updated file\n        will be saved with the unique names and a handle to the new file (ParmEd) will be returned.\n    \"\"\"\n    if self.are_atom_names_correct():\n        return\n\n    logger.debug(f'Ligand {self.internal_name} will have its atom names renamed. ')\n\n    ligand = parmed.load_file(str(self.current), structure=True)\n\n    logger.debug(f'Atom names in the molecule ({self.original_input}/{self.internal_name}) are either not unique '\n          f'or do not follow NameDigit format (e.g. C15). Renaming')\n    _, renaming_map = ties.helpers.get_new_atom_names(ligand.atoms)\n    self._renaming_map = renaming_map\n    logger.debug(f'Rename map: {renaming_map}')\n\n    # save the output here\n    os.makedirs(self.config.lig_unique_atom_names_dir, exist_ok=True)\n\n    ligand_with_uniq_atom_names = self.config.lig_unique_atom_names_dir / (self.internal_name + self.current.suffix)\n    if self.save:\n        ligand.save(str(ligand_with_uniq_atom_names))\n\n    self.ligand_with_uniq_atom_names = ligand_with_uniq_atom_names\n    self.parmed = ligand\n    # this object is now represented by the updated ligand\n    self.current = ligand_with_uniq_atom_names\n
    "},{"location":"api/ligand/#ties.Ligand.antechamber_prepare_mol2","title":"antechamber_prepare_mol2","text":"
    antechamber_prepare_mol2(**kwargs)\n

    Converts the ligand into a .mol2 format.

    BCC charges are generated if missing or requested. It calls antechamber (the charge type -c is not used if user prefers to use their charges). Any DU atoms created in the antechamber call are removed.

    :param atom_type: Atom type bla bla :type atom_type: :param net_charge: :type net_charge: int

    Source code in ties/ligand.py
    def antechamber_prepare_mol2(self, **kwargs):\n    \"\"\"\n    Converts the ligand into a .mol2 format.\n\n    BCC charges are generated if missing or requested.\n    It calls antechamber (the charge type -c is not used if user prefers to use their charges).\n    Any DU atoms created in the antechamber call are removed.\n\n    :param atom_type: Atom type bla bla\n    :type atom_type:\n    :param net_charge:\n    :type net_charge: int\n    \"\"\"\n    self.config.set_configs(**kwargs)\n\n    if self.config.ligands_contain_q or not self.config.antechamber_charge_type:\n        logger.info(f'Antechamber: User-provided atom charges will be reused ({self.current.name})')\n\n    mol2_cwd = self.config.lig_dir / self.internal_name\n\n    # prepare the directory\n    mol2_cwd.mkdir(parents=True, exist_ok=True)\n    mol2_target = mol2_cwd / f'{self.internal_name}.mol2'\n\n    # do not redo if the target file exists\n    if not (mol2_target).is_file():\n        log_filename = mol2_cwd / \"antechamber.log\"\n        with open(log_filename, 'w') as LOG:\n            try:\n                cmd = [self.config.ambertools_antechamber,\n                       '-i', self.current,\n                       '-fi', self.current.suffix[1:],\n                       '-o', mol2_target,\n                       '-fo', 'mol2',\n                       '-at', self.config.ligand_ff_name,\n                       '-nc', str(self.config.ligand_net_charge),\n                       '-dr', str(self.config.antechamber_dr)\n                       ] +  self.config.antechamber_charge_type\n                subprocess.run(cmd,\n                               cwd=mol2_cwd,\n                               stdout=LOG, stderr=LOG,\n                               check=True, text=True,\n                               timeout=60 * 30  # 30 minutes\n                               )\n            except subprocess.CalledProcessError as ProcessError:\n                raise Exception(f'Could not convert the ligand into .mol2 file with antechamber. '\n                                f'See the log and its directory: {log_filename} . '\n                                f'Command used: {\" \".join(map(str, cmd))}') from ProcessError\n        logger.debug(f'Converted {self.original_input} into .mol2, Log: {log_filename}')\n    else:\n        logger.info(f'File {mol2_target} already exists. Skipping. ')\n\n    self.antechamber_mol2 = mol2_target\n    self.current = mol2_target\n\n    # remove any DUMMY DU atoms in the .mol2 atoms\n    self.removeDU_atoms()\n
    "},{"location":"api/ligand/#ties.Ligand.removeDU_atoms","title":"removeDU_atoms","text":"
    removeDU_atoms()\n

    Ambertools antechamber creates sometimes DU dummy atoms. These are not created when BCC charges are computed from scratch. They are only created if you reuse existing charges. They appear to be a side effect. We remove the dummy atoms therefore.

    Source code in ties/ligand.py
    def removeDU_atoms(self):\n    \"\"\"\n    Ambertools antechamber creates sometimes DU dummy atoms.\n    These are not created when BCC charges are computed from scratch.\n    They are only created if you reuse existing charges.\n    They appear to be a side effect. We remove the dummy atoms therefore.\n    \"\"\"\n    mol2 = parmed.load_file(str(self.current), structure=True)\n    # check if there are any DU atoms\n    has_DU = any(a.type == 'DU' for a in mol2.atoms)\n    if not has_DU:\n        return\n\n    # make a backup copy before (to simplify naming)\n    shutil.move(self.current, self.current.parent / ('lig.beforeRemovingDU' + self.current.suffix))\n\n    # remove DU type atoms and save the file\n    for atom in mol2.atoms:\n        if atom.name != 'DU':\n            continue\n\n        atom.residue.delete_atom(atom)\n    # save the updated molecule\n    mol2.save(str(self.current))\n    logger.debug('Removed dummy atoms with type \"DU\"')\n
    "},{"location":"api/ligand/#ties.Ligand.generate_frcmod","title":"generate_frcmod","text":"
    generate_frcmod(**kwargs)\n

    params - parmchk2 - atom_type

    Source code in ties/ligand.py
    def generate_frcmod(self, **kwargs):\n    \"\"\"\n        params\n         - parmchk2\n         - atom_type\n    \"\"\"\n    self.config.set_configs(**kwargs)\n\n    logger.debug(f'INFO: frcmod for {self} was computed before. Not repeating.')\n    if hasattr(self, 'frcmod'):\n        return\n\n    # fixme - work on the file handles instaed of the constant stitching\n    logger.debug(f'Parmchk2: generate the .frcmod for {self.internal_name}.mol2')\n\n    # prepare cwd\n    cwd = self.config.lig_frcmod_dir / self.internal_name\n    if not cwd.is_dir():\n        cwd.mkdir(parents=True, exist_ok=True)\n\n    target_frcmod = f'{self.internal_name}.frcmod'\n    log_filename = cwd / \"parmchk2.log\"\n    with open(log_filename, 'w') as LOG:\n        try:\n            subprocess.run([self.config.ambertools_parmchk2,\n                            '-i', self.current,\n                            '-o', target_frcmod,\n                            '-f', 'mol2',\n                            '-s', self.config.ligand_ff_name],\n                           stdout=LOG, stderr=LOG,\n                           check= True, text=True,\n                           cwd= cwd, timeout=20,  # 20 seconds\n                            )\n        except subprocess.CalledProcessError as E:\n            raise Exception(f\"GAFF Error: Could not generate FRCMOD for file: {self.current} . \"\n                            f'See more here: {log_filename}') from E\n\n    logger.debug(f'Parmchk2: created frcmod: {target_frcmod}')\n    self.frcmod = cwd / target_frcmod\n
    "},{"location":"api/ligand/#ties.Ligand.overwrite_coordinates_with","title":"overwrite_coordinates_with","text":"
    overwrite_coordinates_with(file, output_file)\n

    Load coordinates from another file and overwrite the coordinates in the current file.

    Source code in ties/ligand.py
    def overwrite_coordinates_with(self, file, output_file):\n    \"\"\"\n    Load coordinates from another file and overwrite the coordinates in the current file.\n    \"\"\"\n\n    # load the current atoms with ParmEd\n    template = parmed.load_file(str(self.current), structure=True)\n\n    # load the file with the coordinates we want to use\n    coords = parmed.load_file(str(file), structure=True)\n\n    # fixme: use the atom names\n    by_atom_name = True\n    by_index = False\n    by_general_atom_type = False\n\n    # mol2_filename will be overwritten!\n    logger.info(f'Writing to {self.current} the coordinates from {file}. ')\n\n    coords_sum = np.sum(coords.atoms.positions)\n\n    if by_atom_name and by_index:\n        raise ValueError('Cannot have both. They are exclusive')\n    elif not by_atom_name and not by_index:\n        raise ValueError('Either option has to be selected.')\n\n    if by_general_atom_type:\n        for mol2_atom in template.atoms:\n            found_match = False\n            for ref_atom in coords.atoms:\n                if element_from_type[mol2_atom.type.upper()] == element_from_type[ref_atom.type.upper()]:\n                    found_match = True\n                    mol2_atom.position = ref_atom.position\n                    break\n            assert found_match, \"Could not find the following atom in the original file: \" + mol2_atom.name\n    if by_atom_name:\n        for mol2_atom in template.atoms:\n            found_match = False\n            for ref_atom in coords.atoms:\n                if mol2_atom.name.upper() == ref_atom.name.upper():\n                    found_match = True\n                    mol2_atom.position = ref_atom.position\n                    break\n            assert found_match, \"Could not find the following atom name across the two files: \" + mol2_atom.name\n    elif by_index:\n        for mol2_atom, ref_atom in zip(template.atoms, coords.atoms):\n            atype = element_from_type[mol2_atom.type.upper()]\n            reftype = element_from_type[ref_atom.type.upper()]\n            if atype != reftype:\n                raise Exception(\n                    f\"The found general type {atype} does not equal to the reference type {reftype} \")\n\n            mol2_atom.position = ref_atom.position\n\n    if np.testing.assert_almost_equal(coords_sum, np.sum(mda_template.atoms.positions), decimal=2):\n        logger.debug('Different positions sums:', coords_sum, np.sum(mda_template.atoms.positions))\n        raise Exception('Copying of the coordinates did not work correctly')\n\n    # save the output file\n    mda_template.atoms.write(output_file)\n
    "},{"location":"api/pair/","title":"Pair","text":""},{"location":"api/pair/#ties.Pair","title":"Pair","text":"
    Pair(ligA, ligZ, config=None, **kwargs)\n

    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)\n

    Methods:

    • superimpose \u2013

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

    • set_suptop \u2013

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

    • make_atom_names_unique \u2013

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

    • check_json_file \u2013

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

    • merge_frcmod_files \u2013

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

    • overlap_fractions \u2013

      Calculate the size of the common area.

    Source code in ties/pair.py
    def __init__(self, ligA, ligZ, config=None, **kwargs):\n    \"\"\"\n    Please use the Config class for the documentation of the possible kwargs.\n    Each kwarg is passed to the config class.\n\n    fixme - list all relevant kwargs here\n\n        param ligand_net_charge: integer, net charge of each ligand (has to be the same)\n    \"\"\"\n\n    # create a new config if it is not provided\n    self.config = ties.config.Config() if config is None else config\n\n    # channel all config variables to the config class\n    self.config.set_configs(**kwargs)\n\n    # tell Config about the ligands if necessary\n    if self.config.ligands is None:\n        self.config.ligands = [ligA, ligZ]\n\n    # create ligands if they're just paths\n    if isinstance(ligA, ties.ligand.Ligand):\n        self.ligA = ligA\n    else:\n        self.ligA = ties.ligand.Ligand(ligA, self.config)\n\n    if isinstance(ligZ, ties.ligand.Ligand):\n        self.ligZ = ligZ\n    else:\n        self.ligZ = ties.ligand.Ligand(ligZ, self.config)\n\n    # initialise the handles to the molecules that morph\n    self.current_ligA = self.ligA.current\n    self.current_ligZ = self.ligZ.current\n\n    self.internal_name = f'{self.ligA.internal_name}_{self.ligZ.internal_name}'\n    self.mol2 = None\n    self.pdb = None\n    self.summary = None\n    self.suptop = None\n    self.mda_l1 = None\n    self.mda_l2 = None\n    self.distance = None\n
    "},{"location":"api/pair/#ties.Pair.superimpose","title":"superimpose","text":"
    superimpose(**kwargs)\n

    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
    def superimpose(self, **kwargs):\n    \"\"\"\n    Please see :class:`Config` class for the documentation of kwargs. The passed kwargs overwrite the config\n    object passed in the constructor.\n\n    fixme - list all relevant kwargs here\n\n    :param use_element_in_superimposition: bool whether the superimposition should rely on the element initially,\n        before refining the results with a more specific check of the atom type.\n    :param manually_matched_atom_pairs:\n    :param manually_mismatched_pairs:\n    :param redistribute_q_over_unmatched:\n    \"\"\"\n    self.config.set_configs(**kwargs)\n\n    # use ParmEd to load the files\n    # fixme - move this to the Morph class instead of this place,\n    # fixme - should not squash all messsages. For example, wrong type file should not be squashed\n    leftlig_atoms, leftlig_bonds, rightlig_atoms, rightlig_bonds, parmed_ligA, parmed_ligZ = \\\n        get_atoms_bonds_from_mol2(self.current_ligA, self.current_ligZ,\n                                  use_general_type=self.config.use_element_in_superimposition)\n    # fixme - manual match should be improved here and allow for a sensible format.\n\n    # in case the atoms were renamed, pass the names via the map renaming map\n    # TODO\n    # ligZ_old_new_atomname_map\n    new_mismatch_names = []\n    for a, z in self.config.manually_mismatched_pairs:\n        new_names = (self.ligA.rev_renaming_map[a], self.ligZ.rev_renaming_map[z])\n        logger.debug(f'Selecting mismatching atoms. The mismatch {(a, z)}) was renamed to {new_names}')\n        new_mismatch_names.append(new_names)\n\n    # assign\n    # fixme - Ideally I would reuse the ParmEd data for this,\n    # ParmEd can use bonds if they are present - fixme\n    # map atom IDs to their objects\n    ligand1_nodes = {}\n    for atomNode in leftlig_atoms:\n        ligand1_nodes[atomNode.id] = atomNode\n    # link them together\n    for nfrom, nto, btype in leftlig_bonds:\n        ligand1_nodes[nfrom].bind_to(ligand1_nodes[nto], btype)\n\n    ligand2_nodes = {}\n    for atomNode in rightlig_atoms:\n        ligand2_nodes[atomNode.id] = atomNode\n    for nfrom, nto, btype in rightlig_bonds:\n        ligand2_nodes[nfrom].bind_to(ligand2_nodes[nto], btype)\n\n    # fixme - this should be moved out of here,\n    #  ideally there would be a function in the main interface for this\n    manual_match = [] if self.config.manually_matched_atom_pairs is None else self.config.manually_matched_atom_pairs\n    starting_node_pairs = []\n    for l_aname, r_aname in manual_match:\n        # find the starting node pairs, ie the manually matched pair(s)\n        found_left_node = None\n        for id, ln in ligand1_nodes.items():\n            if l_aname == ln.name:\n                found_left_node = ln\n        if found_left_node is None:\n            raise ValueError(f'Manual Matching: could not find an atom name: \"{l_aname}\" in the left molecule')\n\n        found_right_node = None\n        for id, ln in ligand2_nodes.items():\n            if r_aname == ln.name:\n                found_right_node = ln\n        if found_right_node is None:\n            raise ValueError(f'Manual Matching: could not find an atom name: \"{r_aname}\" in the right molecule')\n\n        starting_node_pairs.append([found_left_node, found_right_node])\n\n    if starting_node_pairs:\n        logger.debug(f'Starting nodes will be used: {starting_node_pairs}')\n\n    # fixme - simplify to only take the ParmEd as input\n    suptop = superimpose_topologies(ligand1_nodes.values(), ligand2_nodes.values(),\n                                     disjoint_components=self.config.allow_disjoint_components,\n                                     net_charge_filter=True,\n                                     pair_charge_atol=self.config.atom_pair_q_atol,\n                                     net_charge_threshold=self.config.net_charge_threshold,\n                                     redistribute_charges_over_unmatched=self.config.redistribute_q_over_unmatched,\n                                     ignore_charges_completely=self.config.ignore_charges_completely,\n                                     ignore_bond_types=True,\n                                     ignore_coords=False,\n                                     align_molecules=self.config.align_molecules_using_mcs,\n                                     use_general_type=self.config.use_element_in_superimposition,\n                                     # fixme - not the same ... use_element_in_superimposition,\n                                     use_only_element=False,\n                                     check_atom_names_unique=True,  # fixme - remove?\n                                     starting_pairs_heuristics=self.config.starting_pairs_heuristics,  # fixme - add to config\n                                     force_mismatch=new_mismatch_names,\n                                     starting_node_pairs=starting_node_pairs,\n                                     parmed_ligA=parmed_ligA, parmed_ligZ=parmed_ligZ,\n                                     starting_pair_seed=self.config.superimposition_starting_pair,\n                                     config=self.config)\n\n    self.set_suptop(suptop, parmed_ligA, parmed_ligZ)\n    # attach the used config to the suptop\n\n    if suptop is not None:\n        suptop.config = self.config\n        # attach the morph to the suptop\n        suptop.morph = self\n\n    return suptop\n
    "},{"location":"api/pair/#ties.Pair.set_suptop","title":"set_suptop","text":"
    set_suptop(suptop, parmed_ligA, parmed_ligZ)\n

    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
    def set_suptop(self, suptop, parmed_ligA, parmed_ligZ):\n    \"\"\"\n    Attach a SuperimposedTopology object along with the ParmEd objects for the ligA and ligZ.\n\n    :param suptop: :class:`SuperimposedTopology`\n    :param parmed_ligA: An ParmEd for the ligA\n    :param parmed_ligZ: An ParmEd for the ligZ\n    \"\"\"\n    self.suptop = suptop\n    self.parmed_ligA = parmed_ligA\n    self.parmed_ligZ = parmed_ligZ\n
    "},{"location":"api/pair/#ties.Pair.make_atom_names_unique","title":"make_atom_names_unique","text":"
    make_atom_names_unique(out_ligA_filename=None, out_ligZ_filename=None, save=True)\n

    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
    def make_atom_names_unique(self, out_ligA_filename=None, out_ligZ_filename=None, save=True):\n    \"\"\"\n    Ensure that each that atoms across the two ligands have unique names.\n\n    While renaming atoms, start with the element (C, N, ..) followed by\n     the count so far (e.g. C1, C2, N1).\n\n    Resnames are set to \"INI\" and \"FIN\", this is useful for the hybrid dual topology.\n\n    :param out_ligA_filename: The new filenames for the ligands with renamed atoms. If None, the default\n        naming convention is used.\n    :type out_ligA_filename: string or bool\n    :param out_ligZ_filename: The new filenames for the ligands with renamed atoms. If None, the default\n        naming convention is used.\n    :type out_ligZ_filename: string or bool\n    :param save: Whether to save to the disk the ligands after renaming the atoms\n    :type save: bool\n    \"\"\"\n\n    # The A ligand is a template for the renaming\n    self.ligA.correct_atom_names()\n\n    # load both ligands\n    left = parmed.load_file(str(self.ligA.current), structure=True)\n    right = parmed.load_file(str(self.ligZ.current), structure=True)\n\n    common_atom_names = {a.name for a in right.atoms}.intersection({a.name for a in left.atoms})\n    atom_names_overlap = len(common_atom_names) > 0\n\n    if atom_names_overlap or not self.ligZ.are_atom_names_correct():\n        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. ')\n        if atom_names_overlap:\n            logger.debug(f'Common atom names: {common_atom_names}')\n        name_counter_L_nodes = ties.helpers.get_atom_names_counter(left.atoms)\n        _, renaming_map = ties.helpers.get_new_atom_names(right.atoms, name_counter=name_counter_L_nodes)\n        self.ligZ.renaming_map = renaming_map\n\n    # rename the residue names to INI and FIN\n    for atom in left.atoms:\n        atom.residue = 'INI'\n    for atom in right.atoms:\n        atom.residue = 'FIN'\n\n    # fixme - instead of using the save parameter, have a method pair.save(filename1, filename2) and\n    #  call it when necessary.\n    # prepare the destination directory\n    if not save:\n        return\n\n    if out_ligA_filename is None:\n        cwd = self.config.pair_unique_atom_names_dir / f'{self.ligA.internal_name}_{self.ligZ.internal_name}'\n        cwd.mkdir(parents=True, exist_ok=True)\n\n        self.current_ligA = cwd / (self.ligA.internal_name + '.mol2')\n        self.current_ligZ = cwd / (self.ligZ.internal_name + '.mol2')\n    else:\n        self.current_ligA = out_ligA_filename\n        self.current_ligZ = out_ligZ_filename\n\n    # save the updated atom names\n    left.save(str(self.current_ligA))\n    right.save(str(self.current_ligZ))\n
    "},{"location":"api/pair/#ties.Pair.check_json_file","title":"check_json_file","text":"
    check_json_file()\n

    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
    def check_json_file(self):\n    \"\"\"\n    Performance optimisation in case TIES is rerun again. Return the first matched atoms which\n    can be used as a seed for the superimposition.\n\n    :return: If the superimposition was computed before, and the .json file is available,\n        gets one of the matched atoms.\n    :rtype: [(ligA_atom, ligZ_atom)]\n    \"\"\"\n    matching_json = self.config.workdir / f'fep_{self.ligA.internal_name}_{self.ligZ.internal_name}.json'\n    if not matching_json.is_file():\n        return None\n\n    return [list(json.load(matching_json.open())['matched'].items())[0]]\n
    "},{"location":"api/pair/#ties.Pair.merge_frcmod_files","title":"merge_frcmod_files","text":"
    merge_frcmod_files(ligcom=None)\n

    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
    def merge_frcmod_files(self, ligcom=None):\n    \"\"\"\n    Merges the .frcmod files generated for each ligand separately, simply by adding them together.\n\n    The duplication has no effect on the final generated topology parm7 top file.\n\n    We are also testing the .frcmod here with the user's force field in order to check if\n    the merge works correctly.\n\n    :param ligcom: Either \"lig\" if only ligands are present, or \"com\" if the complex is present.\n        Helps with the directory structure.\n    :type ligcom: string \"lig\" or \"com\"\n    \"\"\"\n    ambertools_tleap = self.config.ambertools_tleap\n    ambertools_script_dir = self.config.ambertools_script_dir\n    if self.config.protein is None:\n        protein_ff = None\n    else:\n        protein_ff = self.config.protein_ff\n\n    ligand_ff = self.config.ligand_ff\n\n    frcmod_info1 = ties.helpers.parse_frcmod_sections(self.ligA.frcmod)\n    frcmod_info2 = ties.helpers.parse_frcmod_sections(self.ligZ.frcmod)\n\n    cwd = self.config.workdir\n\n    # fixme: use the provided cwd here, otherwise this will not work if the wrong cwd is used\n    # have some conf module instead of this\n    if ligcom:\n        morph_frcmod = cwd / f'ties-{self.ligA.internal_name}-{self.ligZ.internal_name}' / ligcom / 'build' / 'hybrid.frcmod'\n    else:\n        # fixme - clean up\n        morph_frcmod = cwd / f'ties-{self.ligA.internal_name}-{self.ligZ.internal_name}' / 'build' / 'hybrid.frcmod'\n    morph_frcmod.parent.mkdir(parents=True, exist_ok=True)\n    with open(morph_frcmod, 'w') as FOUT:\n        FOUT.write('merged frcmod\\n')\n\n        for section in ['MASS', 'BOND', 'ANGLE',\n                        'DIHE', 'IMPROPER', 'NONBON']:\n            section_lines = frcmod_info1[section] + frcmod_info2[section]\n            FOUT.write('{0:s}\\n'.format(section))\n            for line in section_lines:\n                FOUT.write('{0:s}'.format(line))\n            FOUT.write('\\n')\n\n        FOUT.write('\\n\\n')\n\n    # this is our current frcmod file\n    self.frcmod = morph_frcmod\n\n    # as part of the .frcmod writing\n    # insert dummy angles/dihedrals if a morph .frcmod requires\n    # new terms between the appearing/disappearing atoms\n    # this is a trick to make sure tleap has everything it needs to generate the .top file\n    correction_introduced = self._check_hybrid_frcmod(ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff)\n    if correction_introduced:\n        # move the .frcmod which turned out to be insufficient according to the test\n        shutil.move(morph_frcmod, str(self.frcmod) + '.uncorrected' )\n        # now copy in place the corrected version\n        shutil.copy(self.frcmod, morph_frcmod)\n
    "},{"location":"api/pair/#ties.Pair._check_hybrid_frcmod","title":"_check_hybrid_frcmod","text":"
    _check_hybrid_frcmod(ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff)\n

    Check that the output library can be used to create a valid amber topology. Add missing terms with no force to pass the topology creation. Returns the corrected .frcmod content, otherwise throws an exception.

    Source code in ties/pair.py
    def _check_hybrid_frcmod(self, ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff):\n    \"\"\"\n    Check that the output library can be used to create a valid amber topology.\n    Add missing terms with no force to pass the topology creation.\n    Returns the corrected .frcmod content, otherwise throws an exception.\n    \"\"\"\n    # prepare the working directory\n    cwd = self.config.pair_morphfrmocs_tests_dir / self.internal_name\n    if not cwd.is_dir():\n        cwd.mkdir(parents=True, exist_ok=True)\n\n    if protein_ff is None:\n        protein_ff = '# no protein ff needed'\n    else:\n        protein_ff = 'source ' + protein_ff\n\n    # prepare the superimposed .mol2 file if needed\n    if not hasattr(self.suptop, 'mol2'):\n        self.suptop.write_mol2()\n\n    # prepare tleap input\n    leap_in_test = 'leap_test_morph.in'\n    leap_in_conf = open(ambertools_script_dir / leap_in_test).read()\n    open(cwd / leap_in_test, 'w').write(leap_in_conf.format(\n                            mol2=os.path.relpath(self.suptop.mol2, cwd),\n                            frcmod=os.path.relpath(self.frcmod, cwd),\n                            protein_ff=protein_ff, ligand_ff=ligand_ff))\n\n    # attempt generating the .top\n    logger.debug('Create amber7 topology .top')\n    try:\n        tleap_process = subprocess.run([ambertools_tleap, '-s', '-f', leap_in_test],\n                                       cwd=cwd, text=True, timeout=20,\n                                       capture_output=True, check=True)\n    except subprocess.CalledProcessError as err:\n        raise Exception(\n            f'ERROR: Testing the topology with tleap broke. Return code: {err.returncode} '\n            f'ERROR: Ambertools output: {err.stdout}') from err\n\n    # save stdout and stderr\n    open(cwd / 'tleap_scan_check.log', 'w').write(tleap_process.stdout + tleap_process.stderr)\n\n    if 'Errors = 0' in tleap_process.stdout:\n        logger.debug('Test hybrid .frcmod: OK, no dummy angle/dihedrals inserted.')\n        return False\n\n    # extract the missing angles/dihedrals\n    missing_bonds = set()\n    missing_angles = []\n    missing_dihedrals = []\n    for line in tleap_process.stdout.splitlines():\n        if \"Could not find bond parameter for:\" in line:\n            bond = line.split(':')[-1].strip()\n            missing_bonds.add(bond)\n        elif \"Could not find angle parameter:\" in line or \\\n                \"Could not find angle parameter for atom types:\" in line:\n            cols = line.split(':')\n            angle = cols[-1].strip()\n            if angle not in missing_angles:\n                missing_angles.append(angle)\n        elif \"No torsion terms for\" in line:\n            cols = line.split()\n            torsion = cols[-1].strip()\n            if torsion not in missing_dihedrals:\n                missing_dihedrals.append(torsion)\n\n    modified_hybrid_frcmod = cwd / f'{self.internal_name}_corrected.frcmod'\n    if missing_angles or missing_dihedrals:\n        logger.debug('Adding dummy bonds+angles+dihedrals to frcmod to generate .top')\n        # read the original frcmod\n        frcmod_lines = open(self.frcmod).readlines()\n        # overwriting the .frcmod with dummy angles/dihedrals\n        with open(modified_hybrid_frcmod, 'w') as NEW_FRCMOD:\n            for line in frcmod_lines:\n                NEW_FRCMOD.write(line)\n                if 'BOND' in line:\n                    for bond  in missing_bonds:\n                        dummy_bond = f'{bond:<14}0  180  \\t\\t# Dummy bond\\n'\n                        NEW_FRCMOD.write(dummy_bond)\n                        logger.debug(f'Added dummy bond: \"{dummy_bond}\"')\n                if 'ANGLE' in line:\n                    for angle in missing_angles:\n                        dummy_angle = f'{angle:<14}0  120.010  \\t\\t# Dummy angle\\n'\n                        NEW_FRCMOD.write(dummy_angle)\n                        logger.debug(f'Added dummy angle: \"{dummy_angle}\"')\n                if 'DIHE' in line:\n                    for dihedral in missing_dihedrals:\n                        dummy_dihedral = f'{dihedral:<14}1  0.00  180.000  2.000   \\t\\t# Dummy dihedrals\\n'\n                        NEW_FRCMOD.write(dummy_dihedral)\n                        logger.debug(f'Added dummy dihedral: \"{dummy_dihedral}\"')\n\n        # update our tleap input test to use the corrected file\n        leap_in_test_corrected = cwd / 'leap_test_morph_corrected.in'\n        open(leap_in_test_corrected, 'w').write(leap_in_conf.format(\n                            mol2=os.path.relpath(self.suptop.mol2, cwd),\n                            frcmod=os.path.relpath(modified_hybrid_frcmod, cwd),\n                            protein_ff=protein_ff, ligand_ff=ligand_ff))\n\n        # verify that adding the dummy angles/dihedrals worked\n        tleap_process = subprocess.run([ambertools_tleap, '-s', '-f', leap_in_test_corrected],\n                                       cwd=cwd, text=True, timeout=60 * 10, capture_output=True, check=True)\n\n        if not \"Errors = 0\" in tleap_process.stdout:\n            raise Exception('ERROR: Could not generate the .top file after adding dummy angles/dihedrals')\n\n\n    logger.debug('Morph .frcmod after the insertion of dummy angle/dihedrals: OK')\n    # set this .frcmod as the correct one now,\n    self.frcmod_before_correction = self.frcmod\n    self.frcmod = modified_hybrid_frcmod\n    return True\n
    "},{"location":"api/pair/#ties.Pair.overlap_fractions","title":"overlap_fractions","text":"
    overlap_fractions()\n

    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
    def overlap_fractions(self):\n    \"\"\"\n    Calculate the size of the common area.\n\n    :return: Four decimals capturing: 1) the fraction of the common size with respect to the ligA topology,\n        2) the fraction of the common size with respect to the ligZ topology,\n        3) the percentage of the disappearing atoms in the disappearing molecule\n        4) the percentage of the appearing atoms  in the appearing molecule\n    :rtype: [float, float, float, float]\n    \"\"\"\n\n    if self.suptop is None:\n        return 0, 0, float('inf'), float('inf')\n    else:\n        mcs_size = len(self.suptop.matched_pairs)\n\n    matched_fraction_left = mcs_size / float(len(self.suptop.top1))\n    matched_fraction_right = mcs_size / float(len(self.suptop.top2))\n    disappearing_atoms_fraction = (len(self.suptop.top1) - mcs_size) \\\n                               / float(len(self.suptop.top1)) * 100\n    appearing_atoms_fraction = (len(self.suptop.top2) - mcs_size) \\\n                               / float(len(self.suptop.top2)) * 100\n\n    return matched_fraction_left, matched_fraction_right, disappearing_atoms_fraction, appearing_atoms_fraction\n
    "},{"location":"reference/","title":"Index","text":""},{"location":"reference/#ties","title":"ties","text":"

    Modules:

    • analysis \u2013

      Updated OOP approach to data analysis.

    • cli \u2013

      Exposes a terminal interface to TIES 20.

    • config \u2013
    • generator \u2013
    • helpers \u2013

      A list of functions with a clear purpose that does

    • ligand \u2013
    • ligandmap \u2013
    • md \u2013
    • namd_generator \u2013

      Load two ligands, run the topology superimposer, and then

    • pair \u2013
    • protein \u2013
    • scripts \u2013
    • topology_superimposer \u2013

      The main module responsible for the superimposition.

    "},{"location":"reference/SUMMARY/","title":"SUMMARY","text":"
    • ties
      • analysis
      • cli
      • config
      • generator
      • helpers
      • ligand
      • ligandmap
      • md
      • namd_generator
      • pair
      • protein
      • topology_superimposer
    "},{"location":"reference/analysis/","title":" analysis","text":""},{"location":"reference/analysis/#ties.analysis","title":"analysis","text":"

    Updated OOP approach to data analysis.

    Classes:

    • Replica \u2013

      Replica loads in and parses the actual information.

    • Lambda \u2013

      Reflect the real lambda rather than the \"general\" lambda.

    • Contribution \u2013

      Reflects one type of interactions. For example, appearing electrostatics, or dissapearing VDW.

    • DGSystem \u2013

      Single step dG system.

    • TCSystem \u2013

      Thermodynamics Cycle System.

    "},{"location":"reference/analysis/#ties.analysis.Replica","title":"Replica","text":"

    Replica loads in and parses the actual information. Multiple replicas can work on the lambda. So replica is defined by lambda and by its directory path. This representation should contain all the details necessary.

    "},{"location":"reference/analysis/#ties.analysis.Lambda","title":"Lambda","text":"

    Reflect the real lambda rather than the \"general\" lambda. However, stores the information about the \"general\" lambda as well. This class contains at least 1 replica.

    "},{"location":"reference/analysis/#ties.analysis.Contribution","title":"Contribution","text":"

    Reflects one type of interactions. For example, appearing electrostatics, or dissapearing VDW. This class contains lambdas with their replicas. It can calculate the integral and plot different information relevant to each contribution.

    "},{"location":"reference/analysis/#ties.analysis.DGSystem","title":"DGSystem","text":"

    Single step dG system.

    Contains 4 contributions: disappearing and appearing electrostatics and vdw Uses contributions to calculate dG. Contains lots of dG analysis and plotting.

    "},{"location":"reference/analysis/#ties.analysis.TCSystem","title":"TCSystem","text":"

    Thermodynamics Cycle System.

    Contains 2 Systems, each providing one dG. This way it can provide the ddG. Contains lots of ddG analysis and plotting.

    "},{"location":"reference/cli/","title":" cli","text":""},{"location":"reference/cli/#ties.cli","title":"cli","text":"

    Exposes a terminal interface to TIES 20.

    Classes:

    • ArgparseChecker \u2013

    Functions:

    • get_new_atom_names \u2013

      todo - add unit tests

    • get_atom_names_counter \u2013

      name_counter: a dictionary with atom as the key such as 'N', 'C', etc,

    • parse_frcmod_sections \u2013

      Copied from the previous TIES. It's simpler and this approach must be fine then.

    "},{"location":"reference/cli/#ties.cli.ArgparseChecker","title":"ArgparseChecker","text":"

    Methods:

    • str2bool \u2013

      ArgumentParser tool to figure out the bool value

    • logging_lvl \u2013

      ArgumentParser tool to figure out the bool value

    "},{"location":"reference/cli/#ties.cli.ArgparseChecker.str2bool","title":"str2bool staticmethod","text":"
    str2bool(v)\n

    ArgumentParser tool to figure out the bool value

    Source code in ties/helpers.py
    @staticmethod\ndef str2bool(v):\n    \"ArgumentParser tool to figure out the bool value\"\n    if isinstance(v, bool):\n        return v\n    if v.lower() in ('yes', 'true', 't', 'y', '1'):\n        return True\n    elif v.lower() in ('no', 'false', 'f', 'n', '0'):\n        return False\n    else:\n        raise argparse.ArgumentTypeError('Boolean value expected.')\n
    "},{"location":"reference/cli/#ties.cli.ArgparseChecker.logging_lvl","title":"logging_lvl staticmethod","text":"
    logging_lvl(v)\n

    ArgumentParser tool to figure out the bool value

    Source code in ties/helpers.py
    @staticmethod\ndef logging_lvl(v):\n    \"ArgumentParser tool to figure out the bool value\"\n    logging_levels = {\n        'NOTSET': logging.NOTSET,\n          'DEBUG': logging.DEBUG,\n          'INFO': logging.INFO,\n          'WARNING': logging.WARNING,\n          'ERROR': logging.ERROR,\n          'CRITICAL': logging.CRITICAL,\n          # extras\n           \"ALL\": logging.INFO,\n           \"FALSE\": logging.ERROR\n                      }\n\n    if isinstance(v, bool) and v is True:\n        return logging.WARNING\n    elif isinstance(v, bool) and v is False:\n        # effectively we disable logging until an error happens\n        return logging.ERROR\n    elif v.upper() in logging_levels:\n        return logging_levels[v.upper()]\n    else:\n        raise argparse.ArgumentTypeError('Meaningful logging level expected.')\n
    "},{"location":"reference/cli/#ties.cli.get_new_atom_names","title":"get_new_atom_names","text":"
    get_new_atom_names(atoms, name_counter=None)\n

    todo - add unit tests

    @parameter/returns name_counter: a dictionary with atom as the key such as 'N', 'C', etc, the counter keeps track of the last used counter for each name. Empty means that the counting will start from 1. input atoms: mdanalysis atoms

    Source code in ties/helpers.py
    def get_new_atom_names(atoms, name_counter=None):\n    \"\"\"\n    todo - add unit tests\n\n    @parameter/returns name_counter: a dictionary with atom as the key such as 'N', 'C', etc,\n    the counter keeps track of the last used counter for each name.\n    Empty means that the counting will start from 1.\n    input atoms: mdanalysis atoms\n    \"\"\"\n    if name_counter is None:\n        name_counter = {}\n\n    # {new_uniqe_name: old_atom_name}\n    reverse_renaming_map = {}\n\n    for atom in atoms:\n        # count the letters before any digit\n        letter_count = 0\n        for letter in atom.name:\n            if not letter.isalpha():\n                break\n\n            letter_count += 1\n\n        # use max 3 letters from the atom name\n        letter_count = min(letter_count, 3)\n\n        letters = atom.name[:letter_count]\n\n        # how many atoms do we have with these letters? ie C1, C2, C3 -> 3\n        last_used_counter = name_counter.get(letters, 0) + 1\n\n        # rename\n        new_name = letters + str(last_used_counter)\n\n        # if the name is longer than 4 character,\n        # shorten the number of letters\n        if len(new_name) > 4:\n            # the name is too long, use only the first character\n            new_name = letters[:4-len(str(last_used_counter))] + str(last_used_counter)\n\n            # we assume that there is fewer than 1000 atoms with that name\n            assert len(str(last_used_counter)) < 1000\n\n        reverse_renaming_map[new_name] = atom.name\n\n        atom.name = new_name\n\n        # update the counter\n        name_counter[letters] = last_used_counter\n\n    return name_counter, reverse_renaming_map\n
    "},{"location":"reference/cli/#ties.cli.get_atom_names_counter","title":"get_atom_names_counter","text":"
    get_atom_names_counter(atoms)\n

    name_counter: a dictionary with atom as the key such as 'N', 'C', etc, the counter keeps track of the last used counter for each name. Ie if there are C1, C2, C3, this will return {'C':3} as the last counter.

    Source code in ties/helpers.py
    def get_atom_names_counter(atoms):\n    \"\"\"\n    name_counter: a dictionary with atom as the key such as 'N', 'C', etc,\n    the counter keeps track of the last used counter for each name.\n    Ie if there are C1, C2, C3, this will return {'C':3} as the last counter.\n    \"\"\"\n    name_counter = {}\n\n    for atom in atoms:\n        # get the first letters that is not a character\n        afterLetters = [i for i, l in enumerate(atom.name) if l.isalpha()][-1] + 1\n\n        atom_name = atom.name[:afterLetters]\n        atom_number = int(atom.name[afterLetters:])\n\n        # we are starting the counter from 0 as we always add 1 later on\n        last_used_counter = name_counter.get(atom_name, 0)\n\n        # update the counter\n        name_counter[atom_name] = max(last_used_counter + 1, atom_number)\n\n    return name_counter\n
    "},{"location":"reference/cli/#ties.cli.parse_frcmod_sections","title":"parse_frcmod_sections","text":"
    parse_frcmod_sections(filename)\n

    Copied from the previous TIES. It's simpler and this approach must be fine then.

    Source code in ties/helpers.py
    def parse_frcmod_sections(filename):\n    \"\"\"\n    Copied from the previous TIES. It's simpler and this approach must be fine then.\n    \"\"\"\n    frcmod_info = {}\n    section = 'REMARK'\n\n    with open(filename) as F:\n        for line in F:\n            start_line = line[0:9].strip()\n\n            if start_line in ['MASS', 'BOND', 'IMPROPER',\n                              'NONBON', 'ANGLE', 'DIHE']:\n                section = start_line\n                frcmod_info[section] = []\n            elif line.strip() and section != 'REMARK':\n                frcmod_info[section].append(line)\n\n    return frcmod_info\n
    "},{"location":"reference/config/","title":" config","text":""},{"location":"reference/config/#ties.config","title":"config","text":"

    Classes:

    • Config \u2013

      The configuration with parameters that can be used to define the entire protocol.

    "},{"location":"reference/config/#ties.config.Config","title":"Config","text":"
    Config(**kwargs)\n

    The configuration with parameters that can be used to define the entire protocol. The settings can be overridden later in the actual classes.

    The settings are stored as properties in the object and can be overwritten.

    Methods:

    • get_element_map \u2013

      :return:

    • get_serializable \u2013

      Get a JSON serializable structure of the config.

    Attributes:

    • workdir \u2013

      Working directory for antechamber calls.

    • protein \u2013

      Path to the protein

    • ligand_files \u2013

      A list of ligand filenames.

    • ambertools_home \u2013

      Ambertools HOME path. If not configured, the env variable AMBERHOME as AMBER_PREFIX will be checked.

    • ambertools_antechamber \u2013

      Antechamber path based on the .ambertools_home

    • ambertools_parmchk2 \u2013

      Parmchk2 path based on the .ambertools_home

    • ambertools_tleap \u2013

      Tleap path based on the .ambertools_home

    • antechamber_dr \u2013

      Whether to use -dr setting when calling antechamber.

    • ligand_net_charge \u2013

      The ligand charge. If not provided, neutral charge is assumed.

    • coordinates_file \u2013

      A file from which coordinate can be taken.

    • atom_pair_q_atol \u2013

      It defines the maximum difference in charge

    • net_charge_threshold \u2013

      Defines how much the superimposed regions can, in total, differ in charge.

    • ignore_charges_completely \u2013

      Ignore the charges during the superimposition. Useful for debugging.

    • allow_disjoint_components \u2013

      Defines whether there might be multiple superimposed areas that are

    • use_element_in_superimposition \u2013

      Use element rather than the actual atom type for the superimposition

    • align_molecules_using_mcs \u2013

      After determining the maximum common substructure (MCS),

    • use_original_coor \u2013

      Antechamber when assigning charges can modify the charges slightly.

    • ligands_contain_q \u2013

      If not provided, it tries to deduce whether charges are provided.

    • superimposition_starting_pair \u2013

      Set a starting pair for the superimposition to narrow down the MCS search.

    • manually_matched_atom_pairs \u2013

      Either a list of pairs or a file with a list of pairs of atoms

    • manually_mismatched_pairs \u2013

      A path to a file with a list of a pairs that should be mismatched.

    • protein_ff \u2013

      The protein forcefield to be used by ambertools for the protein parameterisation.

    • md_engine \u2013

      The MD engine, with the supported values NAMD2.13, NAMD2.14, NAMD3 and OpenMM

    • ligand_ff \u2013

      The forcefield for the ligand.

    • ligand_ff_name \u2013

      Either GAFF or GAFF2

    • redistribute_q_over_unmatched \u2013

      The superimposed and matched atoms have every slightly different charges.

    • use_hybrid_single_dual_top \u2013

      Hybrid single dual topology (experimental). Currently not implemented.

    • ligand_tleap_in \u2013

      The name of the tleap input file for ambertools for the ligand.

    • complex_tleap_in \u2013

      The tleap input file for the complex.

    • prep_dir \u2013

      Path to the prep directory. Currently in the workdir

    • pair_morphfrcmods_dir \u2013

      Path to the .frcmod files for the morph.

    • pair_morphfrmocs_tests_dir \u2013

      Path to the location where a test is carried out with .frcmod

    • pair_unique_atom_names_dir \u2013

      Location of the morph files with unique filenames.

    • lig_unique_atom_names_dir \u2013

      Directory location for files with unique atom names.

    • lig_frcmod_dir \u2013

      Directory location with the .frcmod created for each ligand.

    • lig_acprep_dir \u2013

      Directory location where the .ac charges are converted into the .mol2 format.

    • lig_dir \u2013

      Directory location with the .mol2 files.

    Source code in ties/config.py
    def __init__(self, **kwargs):\n    # set the path to the scripts\n    self.code_root = pathlib.Path(os.path.dirname(__file__))\n\n    # scripts/input files,\n    # these are specific to the host\n    self.script_dir = self.code_root / 'scripts'\n    self.namd_script_dir = self.script_dir / 'namd'\n    self.ambertools_script_dir = self.script_dir / 'ambertools'\n    self.tleap_check_protein = self.ambertools_script_dir / 'check_prot.in'\n    self.vmd_vis_script = self.script_dir / 'vmd' / 'vis_morph.vmd'\n    self.vmd_vis_script_sh = self.script_dir / 'vmd' / 'vis_morph.sh'\n\n    self._workdir = None\n    self._antechamber_dr = False\n    self._ambertools_home = None\n\n    self._protein = None\n\n    self._ligand_net_charge = None\n    self._atom_pair_q_atol = 0.1\n    self._net_charge_threshold = 0.1\n    self._redistribute_q_over_unmatched = True\n    self._allow_disjoint_components = False\n    # use only the element in the superimposition rather than the specific atom type\n    self._use_element = False\n    self._use_element_in_superimposition = True\n    self.starting_pairs_heuristics = True\n    # weights in choosing the best MCS, the weighted sum of \"(1 - MCS fraction) and RMSD\".\n    self.weights = [1, 0.5]\n\n    # coordinates\n    self._align_molecules_using_mcs = False\n    self._use_original_coor = False\n    self._coordinates_file = None\n\n    self._ligand_files = set()\n    self._manually_matched_atom_pairs = None\n    self._manually_mismatched_pairs = None\n    self._ligands_contain_q = None\n\n    self._ligand_tleap_in = None\n    self._complex_tleap_in = None\n\n    self._superimposition_starting_pair = None\n\n    self._protein_ff = None\n    self._ligand_ff = 'leaprc.gaff'\n    self._ligand_ff_name = 'gaff'\n\n    # MD/NAMD production input file\n    self._md_engine = 'namd'\n    #default to modern CPU version\n    self.namd_version = '2.14'\n    self._lambda_rep_dir_tree = False\n\n    # experimental\n    self._use_hybrid_single_dual_top = False\n    self._ignore_charges_completely = False\n\n    self.ligands = None\n\n    # if True, do not allow ligands with the same ligand name\n    self.uses_cmd = False\n\n    # assign all the initial configuration values\n    self.set_configs(**kwargs)\n
    "},{"location":"reference/config/#ties.config.Config.workdir","title":"workdir property writable","text":"
    workdir\n

    Working directory for antechamber calls. If None, a temporary directory in /tmp/ will be used.

    :return: Work dir :rtype: str

    "},{"location":"reference/config/#ties.config.Config.protein","title":"protein property writable","text":"
    protein\n

    Path to the protein

    :return: Protein filename :rtype: str

    "},{"location":"reference/config/#ties.config.Config.ligand_files","title":"ligand_files property writable","text":"
    ligand_files\n

    A list of ligand filenames. :return:

    "},{"location":"reference/config/#ties.config.Config.ambertools_home","title":"ambertools_home property writable","text":"
    ambertools_home\n

    Ambertools HOME path. If not configured, the env variable AMBERHOME as AMBER_PREFIX will be checked.

    :return: ambertools path

    "},{"location":"reference/config/#ties.config.Config.ambertools_antechamber","title":"ambertools_antechamber property","text":"
    ambertools_antechamber\n

    Antechamber path based on the .ambertools_home

    :return:

    "},{"location":"reference/config/#ties.config.Config.ambertools_parmchk2","title":"ambertools_parmchk2 property","text":"
    ambertools_parmchk2\n

    Parmchk2 path based on the .ambertools_home :return:

    "},{"location":"reference/config/#ties.config.Config.ambertools_tleap","title":"ambertools_tleap property","text":"
    ambertools_tleap\n

    Tleap path based on the .ambertools_home :return:

    "},{"location":"reference/config/#ties.config.Config.antechamber_dr","title":"antechamber_dr property writable","text":"
    antechamber_dr\n

    Whether to use -dr setting when calling antechamber.

    :return:

    "},{"location":"reference/config/#ties.config.Config.ligand_net_charge","title":"ligand_net_charge property writable","text":"
    ligand_net_charge\n

    The ligand charge. If not provided, neutral charge is assumed. The charge is necessary for calling antechamber (-nc).

    :return:

    "},{"location":"reference/config/#ties.config.Config.coordinates_file","title":"coordinates_file property writable","text":"
    coordinates_file\n

    A file from which coordinate can be taken.

    :return:

    "},{"location":"reference/config/#ties.config.Config.atom_pair_q_atol","title":"atom_pair_q_atol property writable","text":"
    atom_pair_q_atol\n

    It defines the maximum difference in charge between any two superimposed atoms a1 and a2. If the two atoms differ in charge more than this value, they will be unmatched and added to the alchemical regions.

    :return: default (0.1e) :rtype: float

    "},{"location":"reference/config/#ties.config.Config.net_charge_threshold","title":"net_charge_threshold property writable","text":"
    net_charge_threshold\n

    Defines how much the superimposed regions can, in total, differ in charge. If the total exceeds the thresholds, atom pairs will be unmatched until the threshold is met.

    :return: default (0.1e) :rtype: float

    "},{"location":"reference/config/#ties.config.Config.ignore_charges_completely","title":"ignore_charges_completely property writable","text":"
    ignore_charges_completely\n

    Ignore the charges during the superimposition. Useful for debugging. :return: default (False) :rtype: bool

    "},{"location":"reference/config/#ties.config.Config.allow_disjoint_components","title":"allow_disjoint_components property writable","text":"
    allow_disjoint_components\n

    Defines whether there might be multiple superimposed areas that are separated by alchemical region.

    :return: default (False) :rtype: bool

    "},{"location":"reference/config/#ties.config.Config.use_element_in_superimposition","title":"use_element_in_superimposition property writable","text":"
    use_element_in_superimposition\n

    Use element rather than the actual atom type for the superimposition during the joint-traversal of the two molecules.

    :return: default (False) :rtype: bool

    "},{"location":"reference/config/#ties.config.Config.align_molecules_using_mcs","title":"align_molecules_using_mcs property writable","text":"
    align_molecules_using_mcs\n

    After determining the maximum common substructure (MCS), use it to align the coordinates of the second molecule to the first.

    :return: default (False) :rtype: bool

    "},{"location":"reference/config/#ties.config.Config.use_original_coor","title":"use_original_coor property writable","text":"
    use_original_coor\n

    Antechamber when assigning charges can modify the charges slightly. If that's the case, use the original charges in order to correct this slight divergence in coordinates.

    :return: default (?) :rtype: bool

    "},{"location":"reference/config/#ties.config.Config.ligands_contain_q","title":"ligands_contain_q property writable","text":"
    ligands_contain_q\n

    If not provided, it tries to deduce whether charges are provided. If all charges are set to 0, then it assumes that charges are not provided.

    If set to False explicitly, charges are ignored and computed again.

    :return: default (None) :rtype: bool

    "},{"location":"reference/config/#ties.config.Config.superimposition_starting_pair","title":"superimposition_starting_pair property writable","text":"
    superimposition_starting_pair\n

    Set a starting pair for the superimposition to narrow down the MCS search. E.g. \"C2-C12\"

    :rtype: str

    "},{"location":"reference/config/#ties.config.Config.manually_matched_atom_pairs","title":"manually_matched_atom_pairs property writable","text":"
    manually_matched_atom_pairs\n

    Either a list of pairs or a file with a list of pairs of atoms that should be superimposed/matched.

    :return:

    "},{"location":"reference/config/#ties.config.Config.manually_mismatched_pairs","title":"manually_mismatched_pairs property writable","text":"
    manually_mismatched_pairs\n

    A path to a file with a list of a pairs that should be mismatched.

    "},{"location":"reference/config/#ties.config.Config.protein_ff","title":"protein_ff property writable","text":"
    protein_ff\n

    The protein forcefield to be used by ambertools for the protein parameterisation.

    :return: default (leaprc.ff19SB) :rtype: string

    "},{"location":"reference/config/#ties.config.Config.md_engine","title":"md_engine property writable","text":"
    md_engine\n

    The MD engine, with the supported values NAMD2.13, NAMD2.14, NAMD3 and OpenMM

    :return: NAMD2.13, NAMD2.14, NAMD3 and OpenMM :rtype: string

    "},{"location":"reference/config/#ties.config.Config.ligand_ff","title":"ligand_ff property","text":"
    ligand_ff\n

    The forcefield for the ligand.

    "},{"location":"reference/config/#ties.config.Config.ligand_ff_name","title":"ligand_ff_name property writable","text":"
    ligand_ff_name\n

    Either GAFF or GAFF2

    :return:

    "},{"location":"reference/config/#ties.config.Config.redistribute_q_over_unmatched","title":"redistribute_q_over_unmatched property writable","text":"
    redistribute_q_over_unmatched\n

    The superimposed and matched atoms have every slightly different charges. Taking an average charge between any two atoms introduces imbalances in the net charge of the alchemical regions, due to the different charge distribution.

    :return: default(True)

    "},{"location":"reference/config/#ties.config.Config.use_hybrid_single_dual_top","title":"use_hybrid_single_dual_top property writable","text":"
    use_hybrid_single_dual_top\n

    Hybrid single dual topology (experimental). Currently not implemented.

    :return: default(False).

    "},{"location":"reference/config/#ties.config.Config.ligand_tleap_in","title":"ligand_tleap_in property","text":"
    ligand_tleap_in\n

    The name of the tleap input file for ambertools for the ligand.

    :return: Default ('leap_ligand.in') :rtype: string

    "},{"location":"reference/config/#ties.config.Config.complex_tleap_in","title":"complex_tleap_in property","text":"
    complex_tleap_in\n

    The tleap input file for the complex.

    :return: Default 'leap_complex.in' :type: string

    "},{"location":"reference/config/#ties.config.Config.prep_dir","title":"prep_dir property","text":"
    prep_dir\n

    Path to the prep directory. Currently in the workdir

    :return: Default (workdir/prep)

    "},{"location":"reference/config/#ties.config.Config.pair_morphfrcmods_dir","title":"pair_morphfrcmods_dir property","text":"
    pair_morphfrcmods_dir\n

    Path to the .frcmod files for the morph.

    :return: Default (workdir/prep/morph_frcmods)

    "},{"location":"reference/config/#ties.config.Config.pair_morphfrmocs_tests_dir","title":"pair_morphfrmocs_tests_dir property","text":"
    pair_morphfrmocs_tests_dir\n

    Path to the location where a test is carried out with .frcmod

    :return: Default (workdir/prep/morph_frcmods/tests)

    "},{"location":"reference/config/#ties.config.Config.pair_unique_atom_names_dir","title":"pair_unique_atom_names_dir property","text":"
    pair_unique_atom_names_dir\n

    Location of the morph files with unique filenames.

    :return: Default (workdir/prep/morph_unique_atom_names)

    "},{"location":"reference/config/#ties.config.Config.lig_unique_atom_names_dir","title":"lig_unique_atom_names_dir property","text":"
    lig_unique_atom_names_dir\n

    Directory location for files with unique atom names.

    :return: Default (workdir/prep/unique_atom_names)

    "},{"location":"reference/config/#ties.config.Config.lig_frcmod_dir","title":"lig_frcmod_dir property","text":"
    lig_frcmod_dir\n

    Directory location with the .frcmod created for each ligand.

    :return: Default (workdir/prep/ligand_frcmods)

    "},{"location":"reference/config/#ties.config.Config.lig_acprep_dir","title":"lig_acprep_dir property","text":"
    lig_acprep_dir\n

    Directory location where the .ac charges are converted into the .mol2 format.

    :return: Default (workdir/prep/acprep_to_mol2)

    "},{"location":"reference/config/#ties.config.Config.lig_dir","title":"lig_dir property","text":"
    lig_dir\n

    Directory location with the .mol2 files.

    :return: Default (workdir/mol2)

    "},{"location":"reference/config/#ties.config.Config.get_element_map","title":"get_element_map staticmethod","text":"
    get_element_map()\n

    :return:

    Source code in ties/config.py
    @staticmethod\ndef get_element_map():\n    \"\"\"\n\n\n    :return:\n    \"\"\"\n    # Get the mapping of atom types to elements\n    element_map_filename = pathlib.Path(os.path.dirname(__file__)) / 'data' / 'element_atom_type_map.txt'\n    # remove the comments lines with #\n    lines = filter(lambda l: not l.strip().startswith('#') and not l.strip() == '', open(element_map_filename).readlines())\n    # convert into a dictionary\n\n    element_map = {}\n    for line in lines:\n        element, atom_types = line.split('=')\n\n        for atom_type in atom_types.split():\n            element_map[atom_type.strip()] = element.strip()\n\n    return element_map\n
    "},{"location":"reference/config/#ties.config.Config.get_serializable","title":"get_serializable","text":"
    get_serializable()\n

    Get a JSON serializable structure of the config.

    pathlib.Path is not JSON serializable, so replace it with str

    todo - consider capturing all information about the system here, including each suptop.get_serializable() so that you can record specific information such as the charge changes etc.

    :return: Dictionary {key:value} with the settings :rtype: Dictionary

    Source code in ties/config.py
    def get_serializable(self):\n    \"\"\"\n    Get a JSON serializable structure of the config.\n\n    pathlib.Path is not JSON serializable, so replace it with str\n\n    todo - consider capturing all information about the system here,\n    including each suptop.get_serializable() so that you can record\n    specific information such as the charge changes etc.\n\n    :return: Dictionary {key:value} with the settings\n    :rtype: Dictionary\n    \"\"\"\n\n    host_specific = ['code_root', 'script_dir0', 'namd_script_dir',\n                     'ambertools_script_dir', 'tleap_check_protein', 'vmd_vis_script']\n\n    ser = {}\n    for k, v in self.__dict__.items():\n        if k in host_specific:\n            continue\n\n        if type(v) is pathlib.PosixPath:\n            v = str(v)\n\n        # account for the ligands being pathlib objects\n        if k == 'ligands' and v is not None:\n            # a list of ligands, convert to strings\n            v = [str(l) for l in v]\n        if k == '_ligand_files':\n            continue\n\n        ser[k] = v\n\n    return ser\n
    "},{"location":"reference/generator/","title":" generator","text":""},{"location":"reference/generator/#ties.generator","title":"generator","text":"

    Functions:

    • join_frcmod_files \u2013

      This implementation should be used. Switch to join_frcmod_files2.

    • correct_fep_tempfactor \u2013

      fixme - this function does not need to use the file?

    • get_PBC_coords \u2013

      Return [x, y, z]

    • extract_PBC_oct_from_tleap_log \u2013

      http://ambermd.org/namd/namd_amber.html

    • prepare_antechamber_parmchk2 \u2013

      Prepare the ambertools scripts.

    • get_protein_net_charge \u2013

      Use automatic ambertools solvation of a single component to determine what is the next charge of the system.

    • prepareFile \u2013

      Either copies or sets up a relative link between the files.

    • set_coor_from_ref_by_named_pairs \u2013

      Set coordinates but use atom names provided by the user.

    • update_PBC_in_namd_input \u2013

      fixme - rename this file since it generates the .eq files

    • create_constraint_files \u2013

      :param original_pdb:

    • init_namd_file_min \u2013

      :param from_dir:

    • generate_namd_prod \u2013

      :param namd_prod:

    • generate_namd_eq \u2013

      :param namd_eq:

    • redistribute_charges \u2013

      Calculate the original charges in the matched component.

    "},{"location":"reference/generator/#ties.generator._merge_frcmod_section","title":"_merge_frcmod_section","text":"
    _merge_frcmod_section(ref_lines, other_lines)\n

    A helper function for merging lines in .frcmod files. Note that the order has to be kept. This is because some lines need to follow other lines. In this case, we exclude lines that are present in ref_lines. fixme - since there are duplicate lines, we need to check the duplicates and their presence,

    Source code in ties/generator.py
    def _merge_frcmod_section(ref_lines, other_lines):\n    \"\"\"\n    A helper function for merging lines in .frcmod files.\n    Note that the order has to be kept. This is because some lines need to follow other lines.\n    In this case, we exclude lines that are present in ref_lines.\n    fixme - since there are duplicate lines, we need to check the duplicates and their presence,\n    \"\"\"\n    merged_section = copy.copy(ref_lines)\n    for line in other_lines:\n        if line not in ref_lines:\n            merged_section.append(line)\n\n    return merged_section\n
    "},{"location":"reference/generator/#ties.generator.join_frcmod_files","title":"join_frcmod_files","text":"
    join_frcmod_files(f1, f2, output_filepath)\n

    This implementation should be used. Switch to join_frcmod_files2. This version might be removed if the simple approach is fine.

    Source code in ties/generator.py
    def join_frcmod_files(f1, f2, output_filepath):\n    \"\"\"\n    This implementation should be used. Switch to join_frcmod_files2.\n    This version might be removed if the simple approach is fine.\n    \"\"\"\n    # fixme - load f1 and f2\n\n    def get_section(name, rlines):\n        \"\"\"\n        Chips away from the lines until the section is ready\n\n        fixme is there a .frcmod reader in ambertools?\n        http://ambermd.org/FileFormats.php#frcmod\n        \"\"\"\n        section_names = ['MASS', 'BOND', 'ANGLE', 'DIHE', 'IMPROPER', 'NONBON']\n        assert name in rlines.pop().strip()\n\n        section = []\n        while not (len(rlines) == 0 or any(rlines[-1].startswith(sname) for sname in section_names)):\n            nextl = rlines.pop().strip()\n            if nextl == '':\n                continue\n            # depending on the column name, parse differently\n            if name == 'ANGLE':\n                # e.g.\n                # c -cc-na   86.700     123.270   same as c2-cc-na, penalty score=  2.6\n                atom_types = nextl[:8]\n                other = nextl[9:].split()[::-1]\n                # The harmonic force constants for the angle \"ITT\"-\"JTT\"-\n                #                     \"KTT\" in units of kcal/mol/(rad**2) (radians are the\n                #                     traditional unit for angle parameters in force fields).\n                harmonicForceConstant = float(other.pop())\n                # TEQ        The equilibrium bond angle for the above angle in degrees.\n                eq_bond_angle = float(other.pop())\n                # the overall angle\n                section.append([atom_types, harmonicForceConstant, eq_bond_angle])\n            elif name == 'DIHE':\n                # e.g.\n                # ca-ca-cd-cc   1    0.505       180.000           2.000      same as c2-ce-ca-ca, penalty score=229.0\n                atom_types = nextl[:11]\n                other = nextl[11:].split()[::-1]\n                \"\"\"\n                IDIVF      The factor by which the torsional barrier is divided.\n                    Consult Weiner, et al., JACS 106:765 (1984) p. 769 for\n                    details. Basically, the actual torsional potential is\n\n                           (PK/IDIVF) * (1 + cos(PN*phi - PHASE))\n\n                 PK         The barrier height divided by a factor of 2.\n\n                 PHASE      The phase shift angle in the torsional function.\n\n                            The unit is degrees.\n\n                 PN         The periodicity of the torsional barrier.\n                            NOTE: If PN .lt. 0.0 then the torsional potential\n                                  is assumed to have more than one term, and the\n                                  values of the rest of the terms are read from the\n                                  next cards until a positive PN is encountered.  The\n                                  negative value of pn is used only for identifying\n                                  the existence of the next term and only the\n                                  absolute value of PN is kept.\n                \"\"\"\n                IDIVF = float(other.pop())\n                PK = float(other.pop())\n                PHASE = float(other.pop())\n                PN = float(other.pop())\n                section.append([atom_types, IDIVF, PK, PHASE, PN])\n            elif name == 'IMPROPER':\n                # e.g.\n                # cc-o -c -o          1.1          180.0         2.0          Using general improper torsional angle  X- o- c- o, penalty score=  3.0)\n                # ...  IDIVF , PK , PHASE , PN\n                atom_types = nextl[:11]\n                other = nextl[11:].split()[::-1]\n                # fixme - what is going on here? why is not generated this number?\n                # IDIVF = float(other.pop())\n                PK = float(other.pop())\n                PHASE = float(other.pop())\n                PN = float(other.pop())\n                if PN < 0:\n                    raise Exception('Unimplemented - ordering using with negative 0')\n                section.append([atom_types, PK, PHASE, PN])\n            else:\n                section.append(nextl.split())\n        return {name: section}\n\n    def load_frcmod(filepath):\n        # remark line\n        rlines = open(filepath).readlines()[::-1]\n        assert 'Remark' in rlines.pop()\n\n        parsed = OrderedDict()\n        for section_name in ['MASS', 'BOND', 'ANGLE', 'DIHE', 'IMPROPER', 'NONBON']:\n            parsed.update(get_section(section_name, rlines))\n\n        return parsed\n\n    def join_frcmod(left_frc, right_frc):\n        joined = OrderedDict()\n        for left, right in zip(left_frc.items(), right_frc.items()):\n            lname, litems = left\n            rname, ritems = right\n            assert lname == rname\n\n            joined[lname] = copy.copy(litems)\n\n            if lname == 'MASS':\n                if len(litems) > 0 or len(ritems) > 0:\n                    raise Exception('Unimplemented')\n            elif lname == 'BOND':\n                for ritem in ritems:\n                    if len(litems) > 0 or len(ritems) > 0:\n                        if ritem not in joined[lname]:\n                            raise Exception('Unimplemented')\n            # ANGLE, e.g.\n            # c -cc-na   86.700     123.270   same as c2-cc-na, penalty score=  2.6\n            elif lname == 'ANGLE':\n                for ritem in ritems:\n                    # if the item is not in the litems, add it there\n                    # extra the first three terms to determine if it is present\n                    # fixme - note we are ignoring the \"same as\" note\n                    if ritem not in joined[lname]:\n                        joined[lname].append(ritem)\n            elif lname == 'DIHE':\n                for ritem in ritems:\n                    if ritem not in joined[lname]:\n                        joined[lname].append(ritem)\n            elif lname == 'IMPROPER':\n                for ritem in ritems:\n                    if ritem not in joined[lname]:\n                        joined[lname].append(ritem)\n            elif lname == 'NONBON':\n                # if they're empty\n                if not litems and not ritems:\n                    continue\n\n                raise Exception('Unimplemented')\n            else:\n                raise Exception('Unimplemented')\n        return joined\n\n    def write_frcmod(frcmod, filename):\n        with open(filename, 'w') as FOUT:\n            FOUT.write('GENERATED .frcmod by joining two .frcmod files' + os.linesep)\n            for sname, items in frcmod.items():\n                FOUT.write(f'{sname}' + os.linesep)\n                for item in items:\n                    atom_types = item[0]\n                    FOUT.write(atom_types)\n                    numbers = ' \\t'.join([str(n) for n in item[1:]])\n                    FOUT.write(' \\t' + numbers)\n                    FOUT.write(os.linesep)\n                # the ending line\n                FOUT.write(os.linesep)\n\n    left_frc = load_frcmod(f1)\n    right_frc = load_frcmod(f2)\n    joined_frc = join_frcmod(left_frc, right_frc)\n    write_frcmod(joined_frc, output_filepath)\n
    "},{"location":"reference/generator/#ties.generator._correct_fep_tempfactor_single_top","title":"_correct_fep_tempfactor_single_top","text":"
    _correct_fep_tempfactor_single_top(fep_summary, source_pdb_filename, new_pdb_filename)\n

    Single topology version of function correct_fep_tempfactor.

    The left ligand has to be called INI And right FIN

    Source code in ties/generator.py
    def _correct_fep_tempfactor_single_top(fep_summary, source_pdb_filename, new_pdb_filename):\n    \"\"\"\n    Single topology version of function correct_fep_tempfactor.\n\n    The left ligand has to be called INI\n    And right FIN\n    \"\"\"\n    source_sys = parmed.load_file(source_pdb_filename, structure=True)\n    if {'INI', 'FIN'} != {a.residue.name for a in source_sys.atoms}:\n        raise Exception('Missing the resname \"mer\" in the pdb file prepared for fep')\n\n    # dual-topology info\n    # matched atoms are denoted -2 and 2 (morphing into each other)\n    matched_disappearing = list(fep_summary['single_top_matched'].keys())\n    matched_appearing = list( fep_summary['single_top_matched'].values())\n    # disappearing is denoted by -1\n    disappearing_atoms = fep_summary['single_top_disappearing']\n    # appearing is denoted by 1\n    appearing_atoms = fep_summary['single_top_appearing']\n\n    # update the Temp column\n    for atom in source_sys.atoms:\n        # ignore water and ions and non-ligand resname\n        # we only modify the protein, so ignore the ligand resnames\n        # fixme .. why is it called mer, is it tleap?\n        if atom.residue.name not in ['INI', 'FIN']:\n            continue\n\n        # if the atom was \"matched\", meaning present in both ligands (left and right)\n        # then ignore\n        # note: we only use the left ligand\n        if atom.name.upper() in matched_disappearing:\n            atom.bfactor = -2\n        elif atom.name.upper() in matched_appearing:\n            atom.bfactor = 2\n        elif atom.name.upper() in disappearing_atoms:\n            atom.bfactor = -1\n        elif atom.name.upper() in appearing_atoms:\n            # appearing atoms should\n            atom.bfactor = 1\n        else:\n            raise Exception('This should never happen. It has to be one of the cases')\n\n    source_sys.save(new_pdb_filename, use_hetatoms=False)  # , file_format='PDB') - fixme?\n
    "},{"location":"reference/generator/#ties.generator.correct_fep_tempfactor","title":"correct_fep_tempfactor","text":"
    correct_fep_tempfactor(fep_summary, source_pdb_filename, new_pdb_filename, hybrid_topology=False)\n

    fixme - this function does not need to use the file? we have the json information available here.

    Sets the temperature column in the PDB file So that the number reflects the alchemical information Requires by NAMD in order to know which atoms appear (1) and which disappear (-1).

    Source code in ties/generator.py
    def correct_fep_tempfactor(fep_summary, source_pdb_filename, new_pdb_filename, hybrid_topology=False):\n    \"\"\"\n    fixme - this function does not need to use the file?\n    we have the json information available here.\n\n    Sets the temperature column in the PDB file\n    So that the number reflects the alchemical information\n    Requires by NAMD in order to know which atoms\n    appear (1) and which disappear (-1).\n    \"\"\"\n    if hybrid_topology:\n        # delegate correcting fep column in the pdb file\n        return _correct_fep_tempfactor_single_top(fep_summary, source_pdb_filename, new_pdb_filename)\n\n    pmdpdb = parmed.load_file(str(source_pdb_filename), structure=True)\n    if 'HYB' not in {a.residue.name for a in pmdpdb.atoms}:\n        raise Exception('Missing the resname \"HYB\" in the pdb file prepared for fep')\n\n    # dual-topology info\n    matched = list(fep_summary['superimposition']['matched'].keys())\n    appearing_atoms = fep_summary['superimposition']['appearing']\n    disappearing_atoms = fep_summary['superimposition']['disappearing']\n\n    # update the Temp column\n    for atom in pmdpdb.atoms:\n        # ignore water and ions and non-ligand resname\n        # we only modify the protein, so ignore the ligand resnames\n        # fixme .. why is it called mer, is it tleap?\n        if atom.residue.name != 'HYB':\n            continue\n\n        # if the atom was \"matched\", meaning present in both ligands (left and right)\n        # then ignore\n        # note: we only use the left ligand\n        if atom.name in matched:\n            continue\n        elif atom.name in appearing_atoms:\n            # appearing atoms should\n            atom.bfactor = 1\n        elif atom.name in disappearing_atoms:\n            atom.bfactor = -1\n        else:\n            raise Exception('This should never happen. It has to be one of the cases')\n\n    pmdpdb.save(str(new_pdb_filename), use_hetatoms=False, overwrite=True)  # , file_format='PDB') - fixme?\n
    "},{"location":"reference/generator/#ties.generator.get_PBC_coords","title":"get_PBC_coords","text":"
    get_PBC_coords(pdb_file)\n

    Return [x, y, z]

    Source code in ties/generator.py
    def get_PBC_coords(pdb_file):\n    \"\"\"\n    Return [x, y, z]\n    \"\"\"\n    raise Exception('This should not be called PBC coords. Revisit')\n    # u = load(pdb_file)\n    x = np.abs(max(u.atoms.positions[:, 0]) - min(u.atoms.positions[:, 0]))\n    y = np.abs(max(u.atoms.positions[:, 1]) - min(u.atoms.positions[:, 1]))\n    z = np.abs(max(u.atoms.positions[:, 2]) - min(u.atoms.positions[:, 2]))\n    return (x, y, z)\n
    "},{"location":"reference/generator/#ties.generator.extract_PBC_oct_from_tleap_log","title":"extract_PBC_oct_from_tleap_log","text":"
    extract_PBC_oct_from_tleap_log(leap_log)\n

    http://ambermd.org/namd/namd_amber.html Return the 9 numbers for the truncated octahedron unit cell in namd cellBasisVector1 d 0.0 0.0 cellBasisVector2 (-1/3)d (2/3)sqrt(2)d 0.0 cellBasisVector3 (-1/3)d (-1/3)sqrt(2)d (-1/3)sqrt(6)*d

    Source code in ties/generator.py
    def extract_PBC_oct_from_tleap_log(leap_log):\n    \"\"\"\n    http://ambermd.org/namd/namd_amber.html\n    Return the 9 numbers for the truncated octahedron unit cell in namd\n    cellBasisVector1  d         0.0            0.0\n    cellBasisVector2  (-1/3)*d (2/3)sqrt(2)*d  0.0\n    cellBasisVector3  (-1/3)*d (-1/3)sqrt(2)*d (-1/3)sqrt(6)*d\n    \"\"\"\n    leapl_log_lines = open(leap_log).readlines()\n    line_to_extract = \"Total bounding box for atom centers:\"\n    line_of_interest = list(filter(lambda l: line_to_extract in l, leapl_log_lines))\n    d1, d2, d3 = line_of_interest[-1].split(line_to_extract)[1].split()\n    d1, d2, d3 = float(d1), float(d2), float(d3)\n    assert d1 == d2 == d3\n    # scale the d since after minimisation the system turns out to be much smaller?\n    d = d1 * 0.8\n    return {\n        'cbv1': d, 'cbv2': 0, 'cbv3': 0,\n        'cbv4': (1/3.0)*d, 'cbv5': (2/3.0)*np.sqrt(2)*d, 'cbv6': 0,\n        'cbv7': (-1/3.0)*d, 'cbv8': (1/3.0)*np.sqrt(2)*d, 'cbv9': (1/3)*np.sqrt(6)*d,\n    }\n
    "},{"location":"reference/generator/#ties.generator.prepare_antechamber_parmchk2","title":"prepare_antechamber_parmchk2","text":"
    prepare_antechamber_parmchk2(source_script, target_script, net_charge)\n

    Prepare the ambertools scripts. Particularly, create the scritp so that it has the net charge

    "},{"location":"reference/generator/#ties.generator.prepare_antechamber_parmchk2--fixme-run-antechamber-directly-with-the-right-settings-from-here","title":"fixme - run antechamber directly with the right settings from here?","text":""},{"location":"reference/generator/#ties.generator.prepare_antechamber_parmchk2--fixme-check-if-antechamber-has-a-python-interface","title":"fixme - check if antechamber has a python interface?","text":"Source code in ties/generator.py
    def prepare_antechamber_parmchk2(source_script, target_script, net_charge):\n    \"\"\"\n    Prepare the ambertools scripts.\n    Particularly, create the scritp so that it has the net charge\n    # fixme - run antechamber directly with the right settings from here?\n    # fixme - check if antechamber has a python interface?\n    \"\"\"\n    net_charge_set = open(source_script).read().format(net_charge=net_charge)\n    open(target_script, 'w').write(net_charge_set)\n
    "},{"location":"reference/generator/#ties.generator.get_protein_net_charge","title":"get_protein_net_charge","text":"
    get_protein_net_charge(working_dir, protein_file, ambertools_tleap, leap_input_file, prot_ff)\n

    Use automatic ambertools solvation of a single component to determine what is the next charge of the system. This should be replaced with pka/propka or something akin. Note that this is unsuitable for the hybrid ligand: ambertools does not understand a hybrid ligand and might assign the wront net charge.

    Source code in ties/generator.py
    def get_protein_net_charge(working_dir, protein_file, ambertools_tleap, leap_input_file, prot_ff):\n    \"\"\"\n    Use automatic ambertools solvation of a single component to determine what is the next charge of the system.\n    This should be replaced with pka/propka or something akin.\n    Note that this is unsuitable for the hybrid ligand: ambertools does not understand a hybrid ligand\n    and might assign the wront net charge.\n    \"\"\"\n    cwd = working_dir / 'prep' / 'prep_protein_to_find_net_charge'\n    if not cwd.is_dir():\n        cwd.mkdir()\n\n    # copy the protein\n    shutil.copy(working_dir / protein_file, cwd)\n\n    # use ambertools to solvate the protein: set ion numbers to 0 so that they are determined automatically\n    # fixme - consider moving out of the complex\n    leap_in_conf = open(leap_input_file).read()\n    ligand_ff = 'leaprc.gaff' # ignored but must be provided\n    open(cwd / 'solv_prot.in', 'w').write(leap_in_conf.format(protein_ff=prot_ff, ligand_ff=ligand_ff,\n                                                                          protein_file=protein_file))\n\n    log_filename = cwd / \"ties_tleap.log\"\n    with open(log_filename, 'w') as LOG:\n        try:\n            subprocess.run([ambertools_tleap, '-s', '-f', 'solv_prot.in'],\n                           cwd = cwd,\n                           stdout=LOG, stderr=LOG,\n                           check=True, text=True,\n                           timeout=60 * 2  # 2 minutes\n                        )\n        except subprocess.CalledProcessError as E:\n            print('ERROR: tleap could generate a simple topology for the protein to check the number of ions. ')\n            print(f'ERROR: The output was saved in the directory: {cwd}')\n            print(f'ERROR: can be found in the file: {log_filename}')\n            raise E\n\n\n    # read the file to see how many ions were added\n    newsys = parmed.load_file(str(cwd / 'prot_solv.pdb'), structure=True)\n    names = [a.name for a in newsys.atoms]\n    cl = names.count('Cl-')\n    na = names.count('Na+')  \n\n    if cl > na:\n        return cl-na\n    elif cl < na:\n        return -(na-cl)\n\n    return 0\n
    "},{"location":"reference/generator/#ties.generator.prepareFile","title":"prepareFile","text":"
    prepareFile(src, dst, symbolic=True)\n

    Either copies or sets up a relative link between the files. This allows for a quick switch in how the directory structure is organised. Using relative links means that the entire TIES ligand or TIES complex has to be moved together. However, one might want to be able to send a single replica anywhere and execute it independantly (suitable for BOINC).

    @type: 'sym' or 'copy'

    Source code in ties/generator.py
    def prepareFile(src, dst, symbolic=True):\n    \"\"\"\n    Either copies or sets up a relative link between the files.\n    This allows for a quick switch in how the directory structure is organised.\n    Using relative links means that the entire TIES ligand or TIES complex\n    has to be moved together.\n    However, one might want to be able to send a single replica anywhere and\n    execute it independantly (suitable for BOINC).\n\n    @type: 'sym' or 'copy'\n    \"\"\"\n    if symbolic:\n        # note that deleting all the files is intrusive, todo\n        if os.path.isfile(dst):\n            os.remove(dst)\n        os.symlink(src, dst)\n    else:\n        if os.path.isfile(dst):\n            os.remove(dst)\n        shutil.copy(src, dst)\n
    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs","title":"set_coor_from_ref_by_named_pairs","text":"
    set_coor_from_ref_by_named_pairs(mol2_filename, coor_ref_filename, output_filename, left_right_pairs_filename)\n

    Set coordinates but use atom names provided by the user.

    Example of the left_right_pairs_filename content:

    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--flip-the-first-ring","title":"flip the first ring","text":""},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--move-the-first-c-and-its-h","title":"move the first c and its h","text":"

    C32 C18 H34 C19

    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--second-pair","title":"second pair","text":"

    C33 C17

    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--the-actual-matching-pair","title":"the actual matching pair","text":"

    C31 C16 H28 H11

    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--the-second-matching-pair","title":"the second matching pair","text":"

    C30 C15 H29 H12

    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--_1","title":" generator","text":"

    C35 C14

    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--flip-the-other-ring-with-itself","title":"flip the other ring with itself","text":"

    C39 C36 C36 C39 H33 H30 H30 H33 C37 C38 C38 C37 H31 H32 H32 H31

    Source code in ties/generator.py
    def set_coor_from_ref_by_named_pairs(mol2_filename, coor_ref_filename, output_filename, left_right_pairs_filename):\n    \"\"\"\n    Set coordinates but use atom names provided by the user.\n\n    Example of the left_right_pairs_filename content:\n    # flip the first ring\n    # move the first c and its h\n    C32 C18\n    H34 C19\n    # second pair\n    C33 C17\n    # the actual matching pair\n    C31 C16\n    H28 H11\n    # the second matching pair\n    C30 C15\n    H29 H12\n    #\n    C35 C14\n    # flip the other ring with itself\n    C39 C36\n    C36 C39\n    H33 H30\n    H30 H33\n    C37 C38\n    C38 C37\n    H31 H32\n    H32 H31\n    \"\"\"\n    # fixme - check if the names are unique\n\n    # parse \"left_right_pairs_filename\n    # format per line: leftatom_name right_atom_name\n    lines = open(left_right_pairs_filename).read().split(os.linesep)\n    left_right_pairs = (l.split() for l in lines if not l.strip().startswith('#'))\n\n    # load the ref coordinates\n    ref_mol2 = load_mol2_wrapper(coor_ref_filename)\n    # load the .mol2 files with ParmEd and correct the charges\n    static_mol2 = load_mol2_wrapper(mol2_filename)\n    # this is being modified\n    mod_mol2 = load_mol2_wrapper(mol2_filename)\n\n\n    for pair in left_right_pairs:\n        print('find pair', pair)\n        new_pos = False\n        for mol2_atom in static_mol2.atoms:\n            # check if we are assigning from another molecule\n            for ref_atom in ref_mol2.atoms:\n                if mol2_atom.name.upper() == pair[0] and ref_atom.name.upper() == pair[1]:\n                    new_pos = ref_atom.position\n            # check if we are trying to assing coords from the same molecule\n            for another_atom in static_mol2.atoms:\n                if mol2_atom.name.upper() == pair[0] and another_atom.name.upper() == pair[1]:\n                    new_pos = another_atom.position\n\n        if new_pos is False:\n            raise Exception(\"Could not find this pair: \" + str(pair))\n\n        # assign the position to the right atom\n        # find pair[0]\n        found = False\n        for atom in mod_mol2.atoms:\n            if atom.name.upper() == pair[0]:\n                atom.position = new_pos\n                found = True\n                break\n        assert found\n\n\n    # update the mol2 file\n    mod_mol2.atoms.write(output_filename)\n
    "},{"location":"reference/generator/#ties.generator.update_PBC_in_namd_input","title":"update_PBC_in_namd_input","text":"
    update_PBC_in_namd_input(namd_filename, new_pbc_box, structure_filename, constraint_lines='')\n

    fixme - rename this file since it generates the .eq files These are the lines we modify: cellBasisVector1 {cell_x} 0.000 0.000 cellBasisVector2 0.000 {cell_y} 0.000 cellBasisVector3 0.000 0.000 {cell_z}

    With x/y/z replacing the 3 values

    Source code in ties/generator.py
    def update_PBC_in_namd_input(namd_filename, new_pbc_box, structure_filename, constraint_lines=''):\n    \"\"\"\n    fixme - rename this file since it generates the .eq files\n    These are the lines we modify:\n    cellBasisVector1\t{cell_x}  0.000  0.000\n    cellBasisVector2\t 0.000  {cell_y}  0.000\n    cellBasisVector3\t 0.000  0.000 {cell_z}\n\n    With x/y/z replacing the 3 values\n    \"\"\"\n    assert len(new_pbc_box) == 3\n\n    reformatted_namd_in = open(namd_filename).read().format(\n        cell_x=new_pbc_box[0], cell_y=new_pbc_box[1], cell_z=new_pbc_box[2],\n\n        constraints=constraint_lines, output='test_output', structure=structure_filename)\n\n    # write to the file\n    open(namd_filename, 'w').write(reformatted_namd_in)\n
    "},{"location":"reference/generator/#ties.generator.create_constraint_files","title":"create_constraint_files","text":"
    create_constraint_files(original_pdb, output)\n

    :param original_pdb: :param output: :return:

    Source code in ties/generator.py
    def create_constraint_files(original_pdb, output):\n    '''\n\n    :param original_pdb:\n    :param output:\n    :return:\n    '''\n    sys = parmed.load_file(str(original_pdb), structure=True)\n    # for each atom, give the B column the right value\n    for atom in sys.atoms:\n        # ignore water\n        if atom.residue.name in ['WAT', 'Na+', 'TIP3W', 'TIP3', 'HOH', 'SPC', 'TIP4P']:\n            continue\n\n        # set each atom depending on whether it is a H or not\n        if atom.name.upper().startswith('H'):\n            atom.bfactor = 0\n        else:\n            # restrain the heavy atom\n            atom.bfactor = 4\n\n    sys.save(output, use_hetatoms=False, overwrite=True)\n
    "},{"location":"reference/generator/#ties.generator.init_namd_file_min","title":"init_namd_file_min","text":"
    init_namd_file_min(from_dir, to_dir, filename, structure_name, pbc_box, protein)\n

    :param from_dir: :param to_dir: :param filename: :param structure_name: :param pbc_box: :param protein: :return:

    Source code in ties/generator.py
    def init_namd_file_min(from_dir, to_dir, filename, structure_name, pbc_box, protein):\n    '''\n\n    :param from_dir:\n    :param to_dir:\n    :param filename:\n    :param structure_name:\n    :param pbc_box:\n    :param protein:\n    :return:\n    '''\n    if protein is not None:\n        cons = f\"\"\"\nconstraints  on\nconsexp  2\n# use the same file for the position reference and the B column\nconsref  ../build/{structure_name}.pdb ;#need all positions\nconskfile  ../build/cons.pdb\nconskcol  B\n        \"\"\"\n    else:\n        cons = 'constraints  off'\n\n    min_namd_initialised = open(os.path.join(from_dir, filename)).read() \\\n        .format(structure_name=structure_name, constraints=cons, **pbc_box)\n    out_name = 'eq0.conf'\n    open(os.path.join(to_dir, out_name), 'w').write(min_namd_initialised)\n
    "},{"location":"reference/generator/#ties.generator.generate_namd_prod","title":"generate_namd_prod","text":"
    generate_namd_prod(namd_prod, dst_dir, structure_name)\n

    :param namd_prod: :param dst_dir: :param structure_name: :return:

    Source code in ties/generator.py
    def generate_namd_prod(namd_prod, dst_dir, structure_name):\n    '''\n\n    :param namd_prod:\n    :param dst_dir:\n    :param structure_name:\n    :return:\n    '''\n    input_data = open(namd_prod).read()\n    reformatted_namd_in = input_data.format(output='sim1', structure_name=structure_name)\n    open(dst_dir, 'w').write(reformatted_namd_in)\n
    "},{"location":"reference/generator/#ties.generator.generate_namd_eq","title":"generate_namd_eq","text":"
    generate_namd_eq(namd_eq, dst_dir, structure_name, engine, protein)\n

    :param namd_eq: :param dst_dir: :param structure_name: :param engine: :param protein: :return:

    Source code in ties/generator.py
    def generate_namd_eq(namd_eq, dst_dir, structure_name, engine, protein):\n    '''\n\n    :param namd_eq:\n    :param dst_dir:\n    :param structure_name:\n    :param engine:\n    :param protein:\n    :return:\n    '''\n    input_data = open(namd_eq).read()\n    for i in range(1,3):\n\n        if i == 1:\n            run = \"\"\"\nconstraintScaling 1\nrun 10000\n            \"\"\"\n            pressure = ''\n        else:\n            run = \"\"\"\n# protocol - minimization\nset factor 1\nset nall 10\nset n 1\n\nwhile {$n <= $nall} {\n   constraintScaling $factor\n   run 40000\n   set n [expr $n + 1]\n   set factor [expr $factor * 0.5]\n}\n\nconstraintScaling 0\nrun 600000\n            \"\"\"\n            if engine.lower() == 'namd' or engine.lower() == 'namd2':\n                pressure = \"\"\"\nuseGroupPressure      yes ;# needed for 2fs steps\nuseFlexibleCell       no  ;# no for water box, yes for membrane\nuseConstantArea       no  ;# no for water box, yes for membrane\nBerendsenPressure                       on\nBerendsenPressureTarget                 1.0\nBerendsenPressureCompressibility        4.57e-5\nBerendsenPressureRelaxationTime         100\nBerendsenPressureFreq                   2\n                \"\"\"\n            else:\n                pressure = \"\"\"\nuseGroupPressure      yes ;# needed for 2fs steps\nuseFlexibleCell       no  ;# no for water box, yes for membrane\nuseConstantArea       no  ;# no for water box, yes for membrane\nlangevinPiston          on             # Nose-Hoover Langevin piston pressure control\nlangevinPistonTarget  1.01325          # target pressure in bar 1atm = 1.01325bar\nlangevinPistonPeriod  50.0             # oscillation period in fs. correspond to pgamma T=50fs=0.05ps\nlangevinPistonTemp    300              # f=1/T=20.0(pgamma)\nlangevinPistonDecay   25.0             # oscillation decay time. smaller value correspons to larger random\n                                       # forces and increased coupling to the Langevin temp bath.\n                                       # Equall or smaller than piston period\n                \"\"\"\n\n        if protein is not None:\n            cons = f\"\"\"\n        constraints  on\n        consexp  2\n        # use the same file for the position reference and the B column\n        consref  ../build/{structure_name}.pdb ;#need all positions\n        conskfile  ../build/cons.pdb\n        conskcol  B\n                \"\"\"\n        else:\n            cons = 'constraints  off'\n\n        prev_output = 'eq{}'.format(i-1)\n\n        reformatted_namd_in = input_data.format(\n            constraints=cons, output='eq%d' % (i),\n            prev_output=prev_output, structure_name=structure_name, pressure=pressure, run=run)\n\n        next_eq_step_filename = dst_dir / (\"eq%d.conf\" % (i))\n        open(next_eq_step_filename, 'w').write(reformatted_namd_in)\n
    "},{"location":"reference/generator/#ties.generator.redistribute_charges","title":"redistribute_charges","text":"
    redistribute_charges(mda)\n

    Calculate the original charges in the matched component.

    Source code in ties/generator.py
    def redistribute_charges(mda):\n    \"\"\"\n    Calculate the original charges in the matched component.\n    \"\"\"\n\n\n    return\n
    "},{"location":"reference/helpers/","title":" helpers","text":""},{"location":"reference/helpers/#ties.helpers","title":"helpers","text":"

    A list of functions with a clear purpose that does not belong specifically to any of the existing units.

    Classes:

    • ArgparseChecker \u2013

    Functions:

    • get_new_atom_names \u2013

      todo - add unit tests

    • get_atom_names_counter \u2013

      name_counter: a dictionary with atom as the key such as 'N', 'C', etc,

    • parse_frcmod_sections \u2013

      Copied from the previous TIES. It's simpler and this approach must be fine then.

    "},{"location":"reference/helpers/#ties.helpers.ArgparseChecker","title":"ArgparseChecker","text":"

    Methods:

    • str2bool \u2013

      ArgumentParser tool to figure out the bool value

    • logging_lvl \u2013

      ArgumentParser tool to figure out the bool value

    "},{"location":"reference/helpers/#ties.helpers.ArgparseChecker.str2bool","title":"str2bool staticmethod","text":"
    str2bool(v)\n

    ArgumentParser tool to figure out the bool value

    Source code in ties/helpers.py
    @staticmethod\ndef str2bool(v):\n    \"ArgumentParser tool to figure out the bool value\"\n    if isinstance(v, bool):\n        return v\n    if v.lower() in ('yes', 'true', 't', 'y', '1'):\n        return True\n    elif v.lower() in ('no', 'false', 'f', 'n', '0'):\n        return False\n    else:\n        raise argparse.ArgumentTypeError('Boolean value expected.')\n
    "},{"location":"reference/helpers/#ties.helpers.ArgparseChecker.logging_lvl","title":"logging_lvl staticmethod","text":"
    logging_lvl(v)\n

    ArgumentParser tool to figure out the bool value

    Source code in ties/helpers.py
    @staticmethod\ndef logging_lvl(v):\n    \"ArgumentParser tool to figure out the bool value\"\n    logging_levels = {\n        'NOTSET': logging.NOTSET,\n          'DEBUG': logging.DEBUG,\n          'INFO': logging.INFO,\n          'WARNING': logging.WARNING,\n          'ERROR': logging.ERROR,\n          'CRITICAL': logging.CRITICAL,\n          # extras\n           \"ALL\": logging.INFO,\n           \"FALSE\": logging.ERROR\n                      }\n\n    if isinstance(v, bool) and v is True:\n        return logging.WARNING\n    elif isinstance(v, bool) and v is False:\n        # effectively we disable logging until an error happens\n        return logging.ERROR\n    elif v.upper() in logging_levels:\n        return logging_levels[v.upper()]\n    else:\n        raise argparse.ArgumentTypeError('Meaningful logging level expected.')\n
    "},{"location":"reference/helpers/#ties.helpers.get_new_atom_names","title":"get_new_atom_names","text":"
    get_new_atom_names(atoms, name_counter=None)\n

    todo - add unit tests

    @parameter/returns name_counter: a dictionary with atom as the key such as 'N', 'C', etc, the counter keeps track of the last used counter for each name. Empty means that the counting will start from 1. input atoms: mdanalysis atoms

    Source code in ties/helpers.py
    def get_new_atom_names(atoms, name_counter=None):\n    \"\"\"\n    todo - add unit tests\n\n    @parameter/returns name_counter: a dictionary with atom as the key such as 'N', 'C', etc,\n    the counter keeps track of the last used counter for each name.\n    Empty means that the counting will start from 1.\n    input atoms: mdanalysis atoms\n    \"\"\"\n    if name_counter is None:\n        name_counter = {}\n\n    # {new_uniqe_name: old_atom_name}\n    reverse_renaming_map = {}\n\n    for atom in atoms:\n        # count the letters before any digit\n        letter_count = 0\n        for letter in atom.name:\n            if not letter.isalpha():\n                break\n\n            letter_count += 1\n\n        # use max 3 letters from the atom name\n        letter_count = min(letter_count, 3)\n\n        letters = atom.name[:letter_count]\n\n        # how many atoms do we have with these letters? ie C1, C2, C3 -> 3\n        last_used_counter = name_counter.get(letters, 0) + 1\n\n        # rename\n        new_name = letters + str(last_used_counter)\n\n        # if the name is longer than 4 character,\n        # shorten the number of letters\n        if len(new_name) > 4:\n            # the name is too long, use only the first character\n            new_name = letters[:4-len(str(last_used_counter))] + str(last_used_counter)\n\n            # we assume that there is fewer than 1000 atoms with that name\n            assert len(str(last_used_counter)) < 1000\n\n        reverse_renaming_map[new_name] = atom.name\n\n        atom.name = new_name\n\n        # update the counter\n        name_counter[letters] = last_used_counter\n\n    return name_counter, reverse_renaming_map\n
    "},{"location":"reference/helpers/#ties.helpers.get_atom_names_counter","title":"get_atom_names_counter","text":"
    get_atom_names_counter(atoms)\n

    name_counter: a dictionary with atom as the key such as 'N', 'C', etc, the counter keeps track of the last used counter for each name. Ie if there are C1, C2, C3, this will return {'C':3} as the last counter.

    Source code in ties/helpers.py
    def get_atom_names_counter(atoms):\n    \"\"\"\n    name_counter: a dictionary with atom as the key such as 'N', 'C', etc,\n    the counter keeps track of the last used counter for each name.\n    Ie if there are C1, C2, C3, this will return {'C':3} as the last counter.\n    \"\"\"\n    name_counter = {}\n\n    for atom in atoms:\n        # get the first letters that is not a character\n        afterLetters = [i for i, l in enumerate(atom.name) if l.isalpha()][-1] + 1\n\n        atom_name = atom.name[:afterLetters]\n        atom_number = int(atom.name[afterLetters:])\n\n        # we are starting the counter from 0 as we always add 1 later on\n        last_used_counter = name_counter.get(atom_name, 0)\n\n        # update the counter\n        name_counter[atom_name] = max(last_used_counter + 1, atom_number)\n\n    return name_counter\n
    "},{"location":"reference/helpers/#ties.helpers.parse_frcmod_sections","title":"parse_frcmod_sections","text":"
    parse_frcmod_sections(filename)\n

    Copied from the previous TIES. It's simpler and this approach must be fine then.

    Source code in ties/helpers.py
    def parse_frcmod_sections(filename):\n    \"\"\"\n    Copied from the previous TIES. It's simpler and this approach must be fine then.\n    \"\"\"\n    frcmod_info = {}\n    section = 'REMARK'\n\n    with open(filename) as F:\n        for line in F:\n            start_line = line[0:9].strip()\n\n            if start_line in ['MASS', 'BOND', 'IMPROPER',\n                              'NONBON', 'ANGLE', 'DIHE']:\n                section = start_line\n                frcmod_info[section] = []\n            elif line.strip() and section != 'REMARK':\n                frcmod_info[section].append(line)\n\n    return frcmod_info\n
    "},{"location":"reference/ligand/","title":" ligand","text":""},{"location":"reference/ligand/#ties.ligand","title":"ligand","text":"

    Classes:

    • Ligand \u2013

      The ligand helper class. Helps to load and manage the different copies of the ligand file.

    "},{"location":"reference/ligand/#ties.ligand.Ligand","title":"Ligand","text":"
    Ligand(ligand, config=None, save=True)\n

    The ligand helper class. Helps to load and manage the different copies of the ligand file. Specifically, it tracks the different copies of the original input files as it is transformed (e.g. charge assignment).

    :param ligand: ligand filepath :type ligand: string :param config: Optional configuration from which the relevant ligand settings can be used :type config: :class:Config :param save: write a file with unique atom names for further inspection :type save: bool

    Methods:

    • convert_acprep_to_mol2 \u2013

      If the file is not a prep/ac file, this function does not do anything.

    • are_atom_names_correct \u2013

      Checks if atom names:

    • correct_atom_names \u2013

      Ensure that each atom name:

    • antechamber_prepare_mol2 \u2013

      Converts the ligand into a .mol2 format.

    • removeDU_atoms \u2013

      Ambertools antechamber creates sometimes DU dummy atoms.

    • generate_frcmod \u2013

      params

    • overwrite_coordinates_with \u2013

      Load coordinates from another file and overwrite the coordinates in the current file.

    Attributes:

    • renaming_map \u2013

      Otherwise, key: newName, value: oldName.

    Source code in ties/ligand.py
    def __init__(self, ligand, config=None, save=True):\n    \"\"\"Constructor method\n    \"\"\"\n\n    self.save = save\n    # save workplace root\n    self.config = Config() if config is None else config\n    self.config.ligand_files = ligand\n\n    self.original_input = Path(ligand).absolute()\n\n    # internal name without an extension\n    self.internal_name = self.original_input.stem\n\n    # ligand names have to be unique\n    if self.internal_name in Ligand._USED_FILENAMES and self.config.uses_cmd:\n        raise ValueError(f'ERROR: the ligand filename {self.internal_name} is not unique in the list of ligands. ')\n    else:\n        Ligand._USED_FILENAMES.add(self.internal_name)\n\n    # last used representative Path file\n    self.current = self.original_input\n\n    # internal index\n    # TODO - move to config\n    self.index = Ligand.LIG_COUNTER\n    Ligand.LIG_COUNTER += 1\n\n    self._renaming_map = None\n    self.ligand_with_uniq_atom_names = None\n\n    # If .ac format (ambertools, similar to .pdb), convert it to .mol2 using antechamber\n    self.convert_acprep_to_mol2()\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.renaming_map","title":"renaming_map property writable","text":"
    renaming_map\n

    Otherwise, key: newName, value: oldName.

    If None, means no renaming took place.

    "},{"location":"reference/ligand/#ties.ligand.Ligand.convert_acprep_to_mol2","title":"convert_acprep_to_mol2","text":"
    convert_acprep_to_mol2()\n

    If the file is not a prep/ac file, this function does not do anything. Antechamber is called to convert the .prepi/.prep/.ac file into a .mol2 file.

    Returns: the name of the original file, or of it was .prepi, a new filename with .mol2

    Source code in ties/ligand.py
    def convert_acprep_to_mol2(self):\n    \"\"\"\n    If the file is not a prep/ac file, this function does not do anything.\n    Antechamber is called to convert the .prepi/.prep/.ac file into a .mol2 file.\n\n    Returns: the name of the original file, or of it was .prepi, a new filename with .mol2\n    \"\"\"\n\n    if self.current.suffix.lower() not in ('.ac', '.prep'):\n        return\n\n    filetype = {'.ac': 'ac', '.prep': 'prepi'}[self.current.suffix.lower()]\n\n    cwd = self.config.lig_acprep_dir / self.internal_name\n    if not cwd.is_dir():\n        cwd.mkdir(parents=True, exist_ok=True)\n\n    # prepare the .mol2 files with antechamber (ambertools), assign BCC charges if necessary\n    logger.debug(f'Antechamber: converting {filetype} to mol2')\n    new_current = cwd / (self.internal_name + '.mol2')\n\n    log_filename = cwd / \"antechamber_conversion.log\"\n    with open(log_filename, 'w') as LOG:\n        try:\n            subprocess.run([self.config.ambertools_antechamber,\n                            '-i', self.current, '-fi', filetype,\n                            '-o', new_current, '-fo', 'mol2',\n                            '-dr', self.config.antechamber_dr],\n                           stdout=LOG, stderr=LOG,\n                           check=True, text=True,\n                           cwd=cwd, timeout=30)\n        except subprocess.CalledProcessError as E:\n            raise Exception('An error occurred during the antechamber conversion from .ac to .mol2 data type. '\n                            f'The output was saved in the directory: {cwd}'\n                            f'Please see the log file for the exact error information: {log_filename}') from E\n\n    # update\n    self.original_ac = self.current\n    self.current = new_current\n    logger.debug(f'Converted .ac file to .mol2. The location of the new file: {self.current}')\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.are_atom_names_correct","title":"are_atom_names_correct","text":"
    are_atom_names_correct()\n
    Checks if atom names
    • are unique
    • have a correct format \"LettersNumbers\" e.g. C17
    Source code in ties/ligand.py
    def are_atom_names_correct(self):\n    \"\"\"\n    Checks if atom names:\n     - are unique\n     - have a correct format \"LettersNumbers\" e.g. C17\n    \"\"\"\n    ligand = parmed.load_file(str(self.current), structure=True)\n    atom_names = [a.name for a in ligand.atoms]\n\n    are_uniqe = len(set(atom_names)) == len(atom_names)\n\n    return are_uniqe and self._do_atom_names_have_correct_format(atom_names)\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand._do_atom_names_have_correct_format","title":"_do_atom_names_have_correct_format staticmethod","text":"
    _do_atom_names_have_correct_format(names)\n

    Check if the atom name is followed by a number, e.g. \"C15\" Note that the full atom name cannot be more than 4 characters. This is because the PDB format does not allow for more characters which can lead to inconsistencies.

    :param names: a list of atom names :type names: list[str] :return True if they all follow the correct format.

    Source code in ties/ligand.py
    @staticmethod\ndef _do_atom_names_have_correct_format(names):\n    \"\"\"\n    Check if the atom name is followed by a number, e.g. \"C15\"\n    Note that the full atom name cannot be more than 4 characters.\n    This is because the PDB format does not allow for more\n    characters which can lead to inconsistencies.\n\n    :param names: a list of atom names\n    :type names: list[str]\n    :return True if they all follow the correct format.\n    \"\"\"\n    for name in names:\n        # cannot exceed 4 characters\n        if len(name) > 4:\n            return False\n\n        # count letters before any digit\n        letter_count = 0\n        for letter in name:\n            if not letter.isalpha():\n                break\n\n            letter_count += 1\n\n        # at least one character\n        if letter_count == 0:\n            return False\n\n        # extrac the number suffix\n        atom_number = name[letter_count:]\n        try:\n            int(atom_number)\n        except:\n            return False\n\n    return True\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.correct_atom_names","title":"correct_atom_names","text":"
    correct_atom_names()\n
    Ensure that each atom name
    • is unique
    • has letter followed by digits
    • has max 4 characters

    E.g. C17, NX23

    :param self.save: if the path is provided, the updated file will be saved with the unique names and a handle to the new file (ParmEd) will be returned.

    Source code in ties/ligand.py
    def correct_atom_names(self):\n    \"\"\"\n    Ensure that each atom name:\n     - is unique\n     - has letter followed by digits\n     - has max 4 characters\n    E.g. C17, NX23\n\n    :param self.save: if the path is provided, the updated file\n        will be saved with the unique names and a handle to the new file (ParmEd) will be returned.\n    \"\"\"\n    if self.are_atom_names_correct():\n        return\n\n    logger.debug(f'Ligand {self.internal_name} will have its atom names renamed. ')\n\n    ligand = parmed.load_file(str(self.current), structure=True)\n\n    logger.debug(f'Atom names in the molecule ({self.original_input}/{self.internal_name}) are either not unique '\n          f'or do not follow NameDigit format (e.g. C15). Renaming')\n    _, renaming_map = ties.helpers.get_new_atom_names(ligand.atoms)\n    self._renaming_map = renaming_map\n    logger.debug(f'Rename map: {renaming_map}')\n\n    # save the output here\n    os.makedirs(self.config.lig_unique_atom_names_dir, exist_ok=True)\n\n    ligand_with_uniq_atom_names = self.config.lig_unique_atom_names_dir / (self.internal_name + self.current.suffix)\n    if self.save:\n        ligand.save(str(ligand_with_uniq_atom_names))\n\n    self.ligand_with_uniq_atom_names = ligand_with_uniq_atom_names\n    self.parmed = ligand\n    # this object is now represented by the updated ligand\n    self.current = ligand_with_uniq_atom_names\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.antechamber_prepare_mol2","title":"antechamber_prepare_mol2","text":"
    antechamber_prepare_mol2(**kwargs)\n

    Converts the ligand into a .mol2 format.

    BCC charges are generated if missing or requested. It calls antechamber (the charge type -c is not used if user prefers to use their charges). Any DU atoms created in the antechamber call are removed.

    :param atom_type: Atom type bla bla :type atom_type: :param net_charge: :type net_charge: int

    Source code in ties/ligand.py
    def antechamber_prepare_mol2(self, **kwargs):\n    \"\"\"\n    Converts the ligand into a .mol2 format.\n\n    BCC charges are generated if missing or requested.\n    It calls antechamber (the charge type -c is not used if user prefers to use their charges).\n    Any DU atoms created in the antechamber call are removed.\n\n    :param atom_type: Atom type bla bla\n    :type atom_type:\n    :param net_charge:\n    :type net_charge: int\n    \"\"\"\n    self.config.set_configs(**kwargs)\n\n    if self.config.ligands_contain_q or not self.config.antechamber_charge_type:\n        logger.info(f'Antechamber: User-provided atom charges will be reused ({self.current.name})')\n\n    mol2_cwd = self.config.lig_dir / self.internal_name\n\n    # prepare the directory\n    mol2_cwd.mkdir(parents=True, exist_ok=True)\n    mol2_target = mol2_cwd / f'{self.internal_name}.mol2'\n\n    # do not redo if the target file exists\n    if not (mol2_target).is_file():\n        log_filename = mol2_cwd / \"antechamber.log\"\n        with open(log_filename, 'w') as LOG:\n            try:\n                cmd = [self.config.ambertools_antechamber,\n                       '-i', self.current,\n                       '-fi', self.current.suffix[1:],\n                       '-o', mol2_target,\n                       '-fo', 'mol2',\n                       '-at', self.config.ligand_ff_name,\n                       '-nc', str(self.config.ligand_net_charge),\n                       '-dr', str(self.config.antechamber_dr)\n                       ] +  self.config.antechamber_charge_type\n                subprocess.run(cmd,\n                               cwd=mol2_cwd,\n                               stdout=LOG, stderr=LOG,\n                               check=True, text=True,\n                               timeout=60 * 30  # 30 minutes\n                               )\n            except subprocess.CalledProcessError as ProcessError:\n                raise Exception(f'Could not convert the ligand into .mol2 file with antechamber. '\n                                f'See the log and its directory: {log_filename} . '\n                                f'Command used: {\" \".join(map(str, cmd))}') from ProcessError\n        logger.debug(f'Converted {self.original_input} into .mol2, Log: {log_filename}')\n    else:\n        logger.info(f'File {mol2_target} already exists. Skipping. ')\n\n    self.antechamber_mol2 = mol2_target\n    self.current = mol2_target\n\n    # remove any DUMMY DU atoms in the .mol2 atoms\n    self.removeDU_atoms()\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.removeDU_atoms","title":"removeDU_atoms","text":"
    removeDU_atoms()\n

    Ambertools antechamber creates sometimes DU dummy atoms. These are not created when BCC charges are computed from scratch. They are only created if you reuse existing charges. They appear to be a side effect. We remove the dummy atoms therefore.

    Source code in ties/ligand.py
    def removeDU_atoms(self):\n    \"\"\"\n    Ambertools antechamber creates sometimes DU dummy atoms.\n    These are not created when BCC charges are computed from scratch.\n    They are only created if you reuse existing charges.\n    They appear to be a side effect. We remove the dummy atoms therefore.\n    \"\"\"\n    mol2 = parmed.load_file(str(self.current), structure=True)\n    # check if there are any DU atoms\n    has_DU = any(a.type == 'DU' for a in mol2.atoms)\n    if not has_DU:\n        return\n\n    # make a backup copy before (to simplify naming)\n    shutil.move(self.current, self.current.parent / ('lig.beforeRemovingDU' + self.current.suffix))\n\n    # remove DU type atoms and save the file\n    for atom in mol2.atoms:\n        if atom.name != 'DU':\n            continue\n\n        atom.residue.delete_atom(atom)\n    # save the updated molecule\n    mol2.save(str(self.current))\n    logger.debug('Removed dummy atoms with type \"DU\"')\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.generate_frcmod","title":"generate_frcmod","text":"
    generate_frcmod(**kwargs)\n

    params - parmchk2 - atom_type

    Source code in ties/ligand.py
    def generate_frcmod(self, **kwargs):\n    \"\"\"\n        params\n         - parmchk2\n         - atom_type\n    \"\"\"\n    self.config.set_configs(**kwargs)\n\n    logger.debug(f'INFO: frcmod for {self} was computed before. Not repeating.')\n    if hasattr(self, 'frcmod'):\n        return\n\n    # fixme - work on the file handles instaed of the constant stitching\n    logger.debug(f'Parmchk2: generate the .frcmod for {self.internal_name}.mol2')\n\n    # prepare cwd\n    cwd = self.config.lig_frcmod_dir / self.internal_name\n    if not cwd.is_dir():\n        cwd.mkdir(parents=True, exist_ok=True)\n\n    target_frcmod = f'{self.internal_name}.frcmod'\n    log_filename = cwd / \"parmchk2.log\"\n    with open(log_filename, 'w') as LOG:\n        try:\n            subprocess.run([self.config.ambertools_parmchk2,\n                            '-i', self.current,\n                            '-o', target_frcmod,\n                            '-f', 'mol2',\n                            '-s', self.config.ligand_ff_name],\n                           stdout=LOG, stderr=LOG,\n                           check= True, text=True,\n                           cwd= cwd, timeout=20,  # 20 seconds\n                            )\n        except subprocess.CalledProcessError as E:\n            raise Exception(f\"GAFF Error: Could not generate FRCMOD for file: {self.current} . \"\n                            f'See more here: {log_filename}') from E\n\n    logger.debug(f'Parmchk2: created frcmod: {target_frcmod}')\n    self.frcmod = cwd / target_frcmod\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.overwrite_coordinates_with","title":"overwrite_coordinates_with","text":"
    overwrite_coordinates_with(file, output_file)\n

    Load coordinates from another file and overwrite the coordinates in the current file.

    Source code in ties/ligand.py
    def overwrite_coordinates_with(self, file, output_file):\n    \"\"\"\n    Load coordinates from another file and overwrite the coordinates in the current file.\n    \"\"\"\n\n    # load the current atoms with ParmEd\n    template = parmed.load_file(str(self.current), structure=True)\n\n    # load the file with the coordinates we want to use\n    coords = parmed.load_file(str(file), structure=True)\n\n    # fixme: use the atom names\n    by_atom_name = True\n    by_index = False\n    by_general_atom_type = False\n\n    # mol2_filename will be overwritten!\n    logger.info(f'Writing to {self.current} the coordinates from {file}. ')\n\n    coords_sum = np.sum(coords.atoms.positions)\n\n    if by_atom_name and by_index:\n        raise ValueError('Cannot have both. They are exclusive')\n    elif not by_atom_name and not by_index:\n        raise ValueError('Either option has to be selected.')\n\n    if by_general_atom_type:\n        for mol2_atom in template.atoms:\n            found_match = False\n            for ref_atom in coords.atoms:\n                if element_from_type[mol2_atom.type.upper()] == element_from_type[ref_atom.type.upper()]:\n                    found_match = True\n                    mol2_atom.position = ref_atom.position\n                    break\n            assert found_match, \"Could not find the following atom in the original file: \" + mol2_atom.name\n    if by_atom_name:\n        for mol2_atom in template.atoms:\n            found_match = False\n            for ref_atom in coords.atoms:\n                if mol2_atom.name.upper() == ref_atom.name.upper():\n                    found_match = True\n                    mol2_atom.position = ref_atom.position\n                    break\n            assert found_match, \"Could not find the following atom name across the two files: \" + mol2_atom.name\n    elif by_index:\n        for mol2_atom, ref_atom in zip(template.atoms, coords.atoms):\n            atype = element_from_type[mol2_atom.type.upper()]\n            reftype = element_from_type[ref_atom.type.upper()]\n            if atype != reftype:\n                raise Exception(\n                    f\"The found general type {atype} does not equal to the reference type {reftype} \")\n\n            mol2_atom.position = ref_atom.position\n\n    if np.testing.assert_almost_equal(coords_sum, np.sum(mda_template.atoms.positions), decimal=2):\n        logger.debug('Different positions sums:', coords_sum, np.sum(mda_template.atoms.positions))\n        raise Exception('Copying of the coordinates did not work correctly')\n\n    # save the output file\n    mda_template.atoms.write(output_file)\n
    "},{"location":"reference/ligandmap/","title":" ligandmap","text":""},{"location":"reference/ligandmap/#ties.ligandmap","title":"ligandmap","text":"

    Classes:

    • LigandMap \u2013

      Work on a list of morphs and use their information to generate a each to each map.

    "},{"location":"reference/ligandmap/#ties.ligandmap.LigandMap","title":"LigandMap","text":"
    LigandMap(ligands, morphs)\n

    Work on a list of morphs and use their information to generate a each to each map. This class then uses the information for * clustering, * path finding (traveling salesman, minimum spanning tree) * visualisation, etc.

    Methods:

    • generate_map \u2013

      Use the underlying morphs to extract the each to each cases.

    Source code in ties/ligandmap.py
    def __init__(self, ligands, morphs):\n    self.morphs = morphs\n    self.ligands = ligands\n    # similarity map\n    self.map = None\n    self.map_weights = None\n    self.graph = None\n
    "},{"location":"reference/ligandmap/#ties.ligandmap.LigandMap.generate_map","title":"generate_map","text":"
    generate_map()\n

    Use the underlying morphs to extract the each to each cases.

    Source code in ties/ligandmap.py
    def generate_map(self):\n    \"\"\"\n    Use the underlying morphs to extract the each to each cases.\n    \"\"\"\n    # a simple 2D map of the ligands\n    self.map = [list(range(len(self.ligands))) for l1 in range(len(self.ligands))]\n\n    # weights based on the size of the superimposed topology\n    self.map_weights = numpy.zeros([len(self.ligands), len(self.ligands)])\n    for morph in self.morphs:\n        self.map[morph.ligA.index][morph.ligZ.index] = morph\n        self.map[morph.ligZ.index][morph.ligA.index] = morph\n\n        matched_left, matched_right, disappearing_atoms, appearing_atoms = morph.overlap_fractions()\n        # use the average number of matched fractions in both ligands\n        weight = 1 - (matched_left + matched_right) / 2.0\n        self.map_weights[morph.ligA.index][morph.ligZ.index] = weight\n        self.map_weights[morph.ligZ.index][morph.ligA.index] = weight\n\n        # update also the morph\n        morph.set_distance(weight)\n
    "},{"location":"reference/md/","title":" md","text":""},{"location":"reference/md/#ties.md","title":"md","text":"

    Classes:

    • MD \u2013

      Class a wrapper around TIES_MD API that exposes a simplified interface.

    "},{"location":"reference/md/#ties.md.MD","title":"MD","text":"
    MD(sim_dir, sim_name='complex', fast=False)\n

    Class a wrapper around TIES_MD API that exposes a simplified interface.

    :param sim_dir: str, points to where the simulation is running i.e where the TIES.cfg file is. :param sim_name: str, the prefix to the input param and topo file i.e complex for complex.pdb/prmtop. :param fast: boolean, if True the setting is TIES.cfg will be overwritten with minimal TIES protocol.

    Methods:

    • run \u2013

      Wrapper for TIES_MD.TIES.run()

    • analysis \u2013

      Wrapper for TIES_MD.ties_analysis()

    Source code in ties/md.py
    def __init__(self, sim_dir, sim_name='complex', fast=False):\n    cwd = os.getcwd()\n    self.sim_dir = os.path.join(cwd, sim_dir)\n    self.analysis_dir = os.path.join(self.sim_dir, '..', '..', '..')\n    #This is the main TIES MD object we call it options as the user interacts with this object to change options\n    self.options = TIES(cwd=self.sim_dir, exp_name=sim_name)\n\n    if fast:\n        #modify md to cut as many corners as possible i.e. reps, windows, sim length\n        self.options.total_reps = 3\n        self.options.global_lambdas = [0.00, 0.05, 0.1, 0.3, 0.5, 0.7, 0.9, 0.95, 1.00]\n        self.options.sampling_per_window = 2*unit.nanoseconds()\n\n    self.options.setup()\n
    "},{"location":"reference/md/#ties.md.MD.run","title":"run","text":"
    run()\n

    Wrapper for TIES_MD.TIES.run()

    :return: None

    Source code in ties/md.py
    def run(self):\n    '''\n    Wrapper for TIES_MD.TIES.run()\n\n    :return: None\n    '''\n    self.options.run()\n
    "},{"location":"reference/md/#ties.md.MD.analysis","title":"analysis","text":"
    analysis(legs, analysis_cfg='./analysis.cfg')\n

    Wrapper for TIES_MD.ties_analysis()

    :param legs: list of strings, these are the thermodynamic legs of the simulation i.e. ['lig', 'com']. :param analysis_cfg: str, for what the analysis config file is called.

    :return: None

    Source code in ties/md.py
    def analysis(self, legs, analysis_cfg='./analysis.cfg'):\n    '''\n    Wrapper for TIES_MD.ties_analysis()\n\n    :param legs: list of strings, these are the thermodynamic legs of the simulation i.e. ['lig', 'com'].\n    :param analysis_cfg: str, for what the analysis config file is called.\n\n    :return: None\n    '''\n    os.chdir(self.analysis_dir)\n    if not os.path.exists('exp.dat'):\n        ties_analysis.make_exp(verbose=False)\n\n    #read the experimental data\n    with open('exp.dat') as f:\n        data = f.read()\n    exp_js = json.loads(data)\n\n    ana_cfg = ties_analysis.Config(analysis_cfg)\n    ana_cfg.simulation_legs = legs\n    ana_cfg.exp_data = exp_js\n    ana = ties_analysis.Analysis(ana_cfg)\n    ana.run()\n
    "},{"location":"reference/namd_generator/","title":" namd_generator","text":""},{"location":"reference/namd_generator/#ties.namd_generator","title":"namd_generator","text":"

    Load two ligands, run the topology superimposer, and then using the results, generate the NAMD input files.

    frcmod file format: http://ambermd.org/FileFormats.php#frcmod

    "},{"location":"reference/pair/","title":" pair","text":""},{"location":"reference/pair/#ties.pair","title":"pair","text":"

    Classes:

    • Pair \u2013

      Facilitates the creation of morphs.

    "},{"location":"reference/pair/#ties.pair.Pair","title":"Pair","text":"
    Pair(ligA, ligZ, config=None, **kwargs)\n

    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)\n

    Methods:

    • superimpose \u2013

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

    • set_suptop \u2013

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

    • make_atom_names_unique \u2013

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

    • check_json_file \u2013

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

    • merge_frcmod_files \u2013

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

    • overlap_fractions \u2013

      Calculate the size of the common area.

    Source code in ties/pair.py
    def __init__(self, ligA, ligZ, config=None, **kwargs):\n    \"\"\"\n    Please use the Config class for the documentation of the possible kwargs.\n    Each kwarg is passed to the config class.\n\n    fixme - list all relevant kwargs here\n\n        param ligand_net_charge: integer, net charge of each ligand (has to be the same)\n    \"\"\"\n\n    # create a new config if it is not provided\n    self.config = ties.config.Config() if config is None else config\n\n    # channel all config variables to the config class\n    self.config.set_configs(**kwargs)\n\n    # tell Config about the ligands if necessary\n    if self.config.ligands is None:\n        self.config.ligands = [ligA, ligZ]\n\n    # create ligands if they're just paths\n    if isinstance(ligA, ties.ligand.Ligand):\n        self.ligA = ligA\n    else:\n        self.ligA = ties.ligand.Ligand(ligA, self.config)\n\n    if isinstance(ligZ, ties.ligand.Ligand):\n        self.ligZ = ligZ\n    else:\n        self.ligZ = ties.ligand.Ligand(ligZ, self.config)\n\n    # initialise the handles to the molecules that morph\n    self.current_ligA = self.ligA.current\n    self.current_ligZ = self.ligZ.current\n\n    self.internal_name = f'{self.ligA.internal_name}_{self.ligZ.internal_name}'\n    self.mol2 = None\n    self.pdb = None\n    self.summary = None\n    self.suptop = None\n    self.mda_l1 = None\n    self.mda_l2 = None\n    self.distance = None\n
    "},{"location":"reference/pair/#ties.pair.Pair.superimpose","title":"superimpose","text":"
    superimpose(**kwargs)\n

    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
    def superimpose(self, **kwargs):\n    \"\"\"\n    Please see :class:`Config` class for the documentation of kwargs. The passed kwargs overwrite the config\n    object passed in the constructor.\n\n    fixme - list all relevant kwargs here\n\n    :param use_element_in_superimposition: bool whether the superimposition should rely on the element initially,\n        before refining the results with a more specific check of the atom type.\n    :param manually_matched_atom_pairs:\n    :param manually_mismatched_pairs:\n    :param redistribute_q_over_unmatched:\n    \"\"\"\n    self.config.set_configs(**kwargs)\n\n    # use ParmEd to load the files\n    # fixme - move this to the Morph class instead of this place,\n    # fixme - should not squash all messsages. For example, wrong type file should not be squashed\n    leftlig_atoms, leftlig_bonds, rightlig_atoms, rightlig_bonds, parmed_ligA, parmed_ligZ = \\\n        get_atoms_bonds_from_mol2(self.current_ligA, self.current_ligZ,\n                                  use_general_type=self.config.use_element_in_superimposition)\n    # fixme - manual match should be improved here and allow for a sensible format.\n\n    # in case the atoms were renamed, pass the names via the map renaming map\n    # TODO\n    # ligZ_old_new_atomname_map\n    new_mismatch_names = []\n    for a, z in self.config.manually_mismatched_pairs:\n        new_names = (self.ligA.rev_renaming_map[a], self.ligZ.rev_renaming_map[z])\n        logger.debug(f'Selecting mismatching atoms. The mismatch {(a, z)}) was renamed to {new_names}')\n        new_mismatch_names.append(new_names)\n\n    # assign\n    # fixme - Ideally I would reuse the ParmEd data for this,\n    # ParmEd can use bonds if they are present - fixme\n    # map atom IDs to their objects\n    ligand1_nodes = {}\n    for atomNode in leftlig_atoms:\n        ligand1_nodes[atomNode.id] = atomNode\n    # link them together\n    for nfrom, nto, btype in leftlig_bonds:\n        ligand1_nodes[nfrom].bind_to(ligand1_nodes[nto], btype)\n\n    ligand2_nodes = {}\n    for atomNode in rightlig_atoms:\n        ligand2_nodes[atomNode.id] = atomNode\n    for nfrom, nto, btype in rightlig_bonds:\n        ligand2_nodes[nfrom].bind_to(ligand2_nodes[nto], btype)\n\n    # fixme - this should be moved out of here,\n    #  ideally there would be a function in the main interface for this\n    manual_match = [] if self.config.manually_matched_atom_pairs is None else self.config.manually_matched_atom_pairs\n    starting_node_pairs = []\n    for l_aname, r_aname in manual_match:\n        # find the starting node pairs, ie the manually matched pair(s)\n        found_left_node = None\n        for id, ln in ligand1_nodes.items():\n            if l_aname == ln.name:\n                found_left_node = ln\n        if found_left_node is None:\n            raise ValueError(f'Manual Matching: could not find an atom name: \"{l_aname}\" in the left molecule')\n\n        found_right_node = None\n        for id, ln in ligand2_nodes.items():\n            if r_aname == ln.name:\n                found_right_node = ln\n        if found_right_node is None:\n            raise ValueError(f'Manual Matching: could not find an atom name: \"{r_aname}\" in the right molecule')\n\n        starting_node_pairs.append([found_left_node, found_right_node])\n\n    if starting_node_pairs:\n        logger.debug(f'Starting nodes will be used: {starting_node_pairs}')\n\n    # fixme - simplify to only take the ParmEd as input\n    suptop = superimpose_topologies(ligand1_nodes.values(), ligand2_nodes.values(),\n                                     disjoint_components=self.config.allow_disjoint_components,\n                                     net_charge_filter=True,\n                                     pair_charge_atol=self.config.atom_pair_q_atol,\n                                     net_charge_threshold=self.config.net_charge_threshold,\n                                     redistribute_charges_over_unmatched=self.config.redistribute_q_over_unmatched,\n                                     ignore_charges_completely=self.config.ignore_charges_completely,\n                                     ignore_bond_types=True,\n                                     ignore_coords=False,\n                                     align_molecules=self.config.align_molecules_using_mcs,\n                                     use_general_type=self.config.use_element_in_superimposition,\n                                     # fixme - not the same ... use_element_in_superimposition,\n                                     use_only_element=False,\n                                     check_atom_names_unique=True,  # fixme - remove?\n                                     starting_pairs_heuristics=self.config.starting_pairs_heuristics,  # fixme - add to config\n                                     force_mismatch=new_mismatch_names,\n                                     starting_node_pairs=starting_node_pairs,\n                                     parmed_ligA=parmed_ligA, parmed_ligZ=parmed_ligZ,\n                                     starting_pair_seed=self.config.superimposition_starting_pair,\n                                     config=self.config)\n\n    self.set_suptop(suptop, parmed_ligA, parmed_ligZ)\n    # attach the used config to the suptop\n\n    if suptop is not None:\n        suptop.config = self.config\n        # attach the morph to the suptop\n        suptop.morph = self\n\n    return suptop\n
    "},{"location":"reference/pair/#ties.pair.Pair.set_suptop","title":"set_suptop","text":"
    set_suptop(suptop, parmed_ligA, parmed_ligZ)\n

    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
    def set_suptop(self, suptop, parmed_ligA, parmed_ligZ):\n    \"\"\"\n    Attach a SuperimposedTopology object along with the ParmEd objects for the ligA and ligZ.\n\n    :param suptop: :class:`SuperimposedTopology`\n    :param parmed_ligA: An ParmEd for the ligA\n    :param parmed_ligZ: An ParmEd for the ligZ\n    \"\"\"\n    self.suptop = suptop\n    self.parmed_ligA = parmed_ligA\n    self.parmed_ligZ = parmed_ligZ\n
    "},{"location":"reference/pair/#ties.pair.Pair.make_atom_names_unique","title":"make_atom_names_unique","text":"
    make_atom_names_unique(out_ligA_filename=None, out_ligZ_filename=None, save=True)\n

    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
    def make_atom_names_unique(self, out_ligA_filename=None, out_ligZ_filename=None, save=True):\n    \"\"\"\n    Ensure that each that atoms across the two ligands have unique names.\n\n    While renaming atoms, start with the element (C, N, ..) followed by\n     the count so far (e.g. C1, C2, N1).\n\n    Resnames are set to \"INI\" and \"FIN\", this is useful for the hybrid dual topology.\n\n    :param out_ligA_filename: The new filenames for the ligands with renamed atoms. If None, the default\n        naming convention is used.\n    :type out_ligA_filename: string or bool\n    :param out_ligZ_filename: The new filenames for the ligands with renamed atoms. If None, the default\n        naming convention is used.\n    :type out_ligZ_filename: string or bool\n    :param save: Whether to save to the disk the ligands after renaming the atoms\n    :type save: bool\n    \"\"\"\n\n    # The A ligand is a template for the renaming\n    self.ligA.correct_atom_names()\n\n    # load both ligands\n    left = parmed.load_file(str(self.ligA.current), structure=True)\n    right = parmed.load_file(str(self.ligZ.current), structure=True)\n\n    common_atom_names = {a.name for a in right.atoms}.intersection({a.name for a in left.atoms})\n    atom_names_overlap = len(common_atom_names) > 0\n\n    if atom_names_overlap or not self.ligZ.are_atom_names_correct():\n        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. ')\n        if atom_names_overlap:\n            logger.debug(f'Common atom names: {common_atom_names}')\n        name_counter_L_nodes = ties.helpers.get_atom_names_counter(left.atoms)\n        _, renaming_map = ties.helpers.get_new_atom_names(right.atoms, name_counter=name_counter_L_nodes)\n        self.ligZ.renaming_map = renaming_map\n\n    # rename the residue names to INI and FIN\n    for atom in left.atoms:\n        atom.residue = 'INI'\n    for atom in right.atoms:\n        atom.residue = 'FIN'\n\n    # fixme - instead of using the save parameter, have a method pair.save(filename1, filename2) and\n    #  call it when necessary.\n    # prepare the destination directory\n    if not save:\n        return\n\n    if out_ligA_filename is None:\n        cwd = self.config.pair_unique_atom_names_dir / f'{self.ligA.internal_name}_{self.ligZ.internal_name}'\n        cwd.mkdir(parents=True, exist_ok=True)\n\n        self.current_ligA = cwd / (self.ligA.internal_name + '.mol2')\n        self.current_ligZ = cwd / (self.ligZ.internal_name + '.mol2')\n    else:\n        self.current_ligA = out_ligA_filename\n        self.current_ligZ = out_ligZ_filename\n\n    # save the updated atom names\n    left.save(str(self.current_ligA))\n    right.save(str(self.current_ligZ))\n
    "},{"location":"reference/pair/#ties.pair.Pair.check_json_file","title":"check_json_file","text":"
    check_json_file()\n

    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
    def check_json_file(self):\n    \"\"\"\n    Performance optimisation in case TIES is rerun again. Return the first matched atoms which\n    can be used as a seed for the superimposition.\n\n    :return: If the superimposition was computed before, and the .json file is available,\n        gets one of the matched atoms.\n    :rtype: [(ligA_atom, ligZ_atom)]\n    \"\"\"\n    matching_json = self.config.workdir / f'fep_{self.ligA.internal_name}_{self.ligZ.internal_name}.json'\n    if not matching_json.is_file():\n        return None\n\n    return [list(json.load(matching_json.open())['matched'].items())[0]]\n
    "},{"location":"reference/pair/#ties.pair.Pair.merge_frcmod_files","title":"merge_frcmod_files","text":"
    merge_frcmod_files(ligcom=None)\n

    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
    def merge_frcmod_files(self, ligcom=None):\n    \"\"\"\n    Merges the .frcmod files generated for each ligand separately, simply by adding them together.\n\n    The duplication has no effect on the final generated topology parm7 top file.\n\n    We are also testing the .frcmod here with the user's force field in order to check if\n    the merge works correctly.\n\n    :param ligcom: Either \"lig\" if only ligands are present, or \"com\" if the complex is present.\n        Helps with the directory structure.\n    :type ligcom: string \"lig\" or \"com\"\n    \"\"\"\n    ambertools_tleap = self.config.ambertools_tleap\n    ambertools_script_dir = self.config.ambertools_script_dir\n    if self.config.protein is None:\n        protein_ff = None\n    else:\n        protein_ff = self.config.protein_ff\n\n    ligand_ff = self.config.ligand_ff\n\n    frcmod_info1 = ties.helpers.parse_frcmod_sections(self.ligA.frcmod)\n    frcmod_info2 = ties.helpers.parse_frcmod_sections(self.ligZ.frcmod)\n\n    cwd = self.config.workdir\n\n    # fixme: use the provided cwd here, otherwise this will not work if the wrong cwd is used\n    # have some conf module instead of this\n    if ligcom:\n        morph_frcmod = cwd / f'ties-{self.ligA.internal_name}-{self.ligZ.internal_name}' / ligcom / 'build' / 'hybrid.frcmod'\n    else:\n        # fixme - clean up\n        morph_frcmod = cwd / f'ties-{self.ligA.internal_name}-{self.ligZ.internal_name}' / 'build' / 'hybrid.frcmod'\n    morph_frcmod.parent.mkdir(parents=True, exist_ok=True)\n    with open(morph_frcmod, 'w') as FOUT:\n        FOUT.write('merged frcmod\\n')\n\n        for section in ['MASS', 'BOND', 'ANGLE',\n                        'DIHE', 'IMPROPER', 'NONBON']:\n            section_lines = frcmod_info1[section] + frcmod_info2[section]\n            FOUT.write('{0:s}\\n'.format(section))\n            for line in section_lines:\n                FOUT.write('{0:s}'.format(line))\n            FOUT.write('\\n')\n\n        FOUT.write('\\n\\n')\n\n    # this is our current frcmod file\n    self.frcmod = morph_frcmod\n\n    # as part of the .frcmod writing\n    # insert dummy angles/dihedrals if a morph .frcmod requires\n    # new terms between the appearing/disappearing atoms\n    # this is a trick to make sure tleap has everything it needs to generate the .top file\n    correction_introduced = self._check_hybrid_frcmod(ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff)\n    if correction_introduced:\n        # move the .frcmod which turned out to be insufficient according to the test\n        shutil.move(morph_frcmod, str(self.frcmod) + '.uncorrected' )\n        # now copy in place the corrected version\n        shutil.copy(self.frcmod, morph_frcmod)\n
    "},{"location":"reference/pair/#ties.pair.Pair._check_hybrid_frcmod","title":"_check_hybrid_frcmod","text":"
    _check_hybrid_frcmod(ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff)\n

    Check that the output library can be used to create a valid amber topology. Add missing terms with no force to pass the topology creation. Returns the corrected .frcmod content, otherwise throws an exception.

    Source code in ties/pair.py
    def _check_hybrid_frcmod(self, ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff):\n    \"\"\"\n    Check that the output library can be used to create a valid amber topology.\n    Add missing terms with no force to pass the topology creation.\n    Returns the corrected .frcmod content, otherwise throws an exception.\n    \"\"\"\n    # prepare the working directory\n    cwd = self.config.pair_morphfrmocs_tests_dir / self.internal_name\n    if not cwd.is_dir():\n        cwd.mkdir(parents=True, exist_ok=True)\n\n    if protein_ff is None:\n        protein_ff = '# no protein ff needed'\n    else:\n        protein_ff = 'source ' + protein_ff\n\n    # prepare the superimposed .mol2 file if needed\n    if not hasattr(self.suptop, 'mol2'):\n        self.suptop.write_mol2()\n\n    # prepare tleap input\n    leap_in_test = 'leap_test_morph.in'\n    leap_in_conf = open(ambertools_script_dir / leap_in_test).read()\n    open(cwd / leap_in_test, 'w').write(leap_in_conf.format(\n                            mol2=os.path.relpath(self.suptop.mol2, cwd),\n                            frcmod=os.path.relpath(self.frcmod, cwd),\n                            protein_ff=protein_ff, ligand_ff=ligand_ff))\n\n    # attempt generating the .top\n    logger.debug('Create amber7 topology .top')\n    try:\n        tleap_process = subprocess.run([ambertools_tleap, '-s', '-f', leap_in_test],\n                                       cwd=cwd, text=True, timeout=20,\n                                       capture_output=True, check=True)\n    except subprocess.CalledProcessError as err:\n        raise Exception(\n            f'ERROR: Testing the topology with tleap broke. Return code: {err.returncode} '\n            f'ERROR: Ambertools output: {err.stdout}') from err\n\n    # save stdout and stderr\n    open(cwd / 'tleap_scan_check.log', 'w').write(tleap_process.stdout + tleap_process.stderr)\n\n    if 'Errors = 0' in tleap_process.stdout:\n        logger.debug('Test hybrid .frcmod: OK, no dummy angle/dihedrals inserted.')\n        return False\n\n    # extract the missing angles/dihedrals\n    missing_bonds = set()\n    missing_angles = []\n    missing_dihedrals = []\n    for line in tleap_process.stdout.splitlines():\n        if \"Could not find bond parameter for:\" in line:\n            bond = line.split(':')[-1].strip()\n            missing_bonds.add(bond)\n        elif \"Could not find angle parameter:\" in line or \\\n                \"Could not find angle parameter for atom types:\" in line:\n            cols = line.split(':')\n            angle = cols[-1].strip()\n            if angle not in missing_angles:\n                missing_angles.append(angle)\n        elif \"No torsion terms for\" in line:\n            cols = line.split()\n            torsion = cols[-1].strip()\n            if torsion not in missing_dihedrals:\n                missing_dihedrals.append(torsion)\n\n    modified_hybrid_frcmod = cwd / f'{self.internal_name}_corrected.frcmod'\n    if missing_angles or missing_dihedrals:\n        logger.debug('Adding dummy bonds+angles+dihedrals to frcmod to generate .top')\n        # read the original frcmod\n        frcmod_lines = open(self.frcmod).readlines()\n        # overwriting the .frcmod with dummy angles/dihedrals\n        with open(modified_hybrid_frcmod, 'w') as NEW_FRCMOD:\n            for line in frcmod_lines:\n                NEW_FRCMOD.write(line)\n                if 'BOND' in line:\n                    for bond  in missing_bonds:\n                        dummy_bond = f'{bond:<14}0  180  \\t\\t# Dummy bond\\n'\n                        NEW_FRCMOD.write(dummy_bond)\n                        logger.debug(f'Added dummy bond: \"{dummy_bond}\"')\n                if 'ANGLE' in line:\n                    for angle in missing_angles:\n                        dummy_angle = f'{angle:<14}0  120.010  \\t\\t# Dummy angle\\n'\n                        NEW_FRCMOD.write(dummy_angle)\n                        logger.debug(f'Added dummy angle: \"{dummy_angle}\"')\n                if 'DIHE' in line:\n                    for dihedral in missing_dihedrals:\n                        dummy_dihedral = f'{dihedral:<14}1  0.00  180.000  2.000   \\t\\t# Dummy dihedrals\\n'\n                        NEW_FRCMOD.write(dummy_dihedral)\n                        logger.debug(f'Added dummy dihedral: \"{dummy_dihedral}\"')\n\n        # update our tleap input test to use the corrected file\n        leap_in_test_corrected = cwd / 'leap_test_morph_corrected.in'\n        open(leap_in_test_corrected, 'w').write(leap_in_conf.format(\n                            mol2=os.path.relpath(self.suptop.mol2, cwd),\n                            frcmod=os.path.relpath(modified_hybrid_frcmod, cwd),\n                            protein_ff=protein_ff, ligand_ff=ligand_ff))\n\n        # verify that adding the dummy angles/dihedrals worked\n        tleap_process = subprocess.run([ambertools_tleap, '-s', '-f', leap_in_test_corrected],\n                                       cwd=cwd, text=True, timeout=60 * 10, capture_output=True, check=True)\n\n        if not \"Errors = 0\" in tleap_process.stdout:\n            raise Exception('ERROR: Could not generate the .top file after adding dummy angles/dihedrals')\n\n\n    logger.debug('Morph .frcmod after the insertion of dummy angle/dihedrals: OK')\n    # set this .frcmod as the correct one now,\n    self.frcmod_before_correction = self.frcmod\n    self.frcmod = modified_hybrid_frcmod\n    return True\n
    "},{"location":"reference/pair/#ties.pair.Pair.overlap_fractions","title":"overlap_fractions","text":"
    overlap_fractions()\n

    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
    def overlap_fractions(self):\n    \"\"\"\n    Calculate the size of the common area.\n\n    :return: Four decimals capturing: 1) the fraction of the common size with respect to the ligA topology,\n        2) the fraction of the common size with respect to the ligZ topology,\n        3) the percentage of the disappearing atoms in the disappearing molecule\n        4) the percentage of the appearing atoms  in the appearing molecule\n    :rtype: [float, float, float, float]\n    \"\"\"\n\n    if self.suptop is None:\n        return 0, 0, float('inf'), float('inf')\n    else:\n        mcs_size = len(self.suptop.matched_pairs)\n\n    matched_fraction_left = mcs_size / float(len(self.suptop.top1))\n    matched_fraction_right = mcs_size / float(len(self.suptop.top2))\n    disappearing_atoms_fraction = (len(self.suptop.top1) - mcs_size) \\\n                               / float(len(self.suptop.top1)) * 100\n    appearing_atoms_fraction = (len(self.suptop.top2) - mcs_size) \\\n                               / float(len(self.suptop.top2)) * 100\n\n    return matched_fraction_left, matched_fraction_right, disappearing_atoms_fraction, appearing_atoms_fraction\n
    "},{"location":"reference/protein/","title":" protein","text":""},{"location":"reference/protein/#ties.protein","title":"protein","text":"

    Classes:

    • Protein \u2013

      A helper tool for the protein file. It calculates the number of ions needed to neutralise it

    "},{"location":"reference/protein/#ties.protein.Protein","title":"Protein","text":"
    Protein(filename=None, config=None)\n

    A helper tool for the protein file. It calculates the number of ions needed to neutralise it (using ambertools for now).

    :param filename: filepath to the protein :type filename: string :param config: Optional configuration for the protein :type config: :class:Config

    Methods:

    • get_path \u2013

      Get a path to the protein.

    Source code in ties/protein.py
    def __init__(self, filename=None, config=None):\n    if filename is None and config is None:\n        raise Exception('Protein filename is not passed and the config file is missing. ')\n\n    self.config = Config() if config is None else config\n\n    if filename is None:\n        if config.protein is None:\n            raise Exception('Could not find the protein in the config object. ')\n        self.file = config.protein\n    elif filename is not None:\n        self.file = filename\n        # update the config\n        config.protein = filename\n\n    # fixme - check if the file exists at this point, throw an exception otherwise\n\n    # calculate the charges of the protein (using ambertools)\n    # fixme - turn this into a method? stage2: use propka or some other tool, not this workaround\n    self.protein_net_charge = ties.generator.get_protein_net_charge(config.workdir, config.protein.absolute(),\n                                                               config.ambertools_tleap, config.tleap_check_protein,\n                                                               config.protein_ff)\n\n    logger.info(f'Protein net charge: {self.protein_net_charge}')\n
    "},{"location":"reference/protein/#ties.protein.Protein.get_path","title":"get_path","text":"
    get_path()\n

    Get a path to the protein.

    :return: the protein filename :rtype: string

    Source code in ties/protein.py
    def get_path(self):\n    \"\"\"\n    Get a path to the protein.\n\n    :return: the protein filename\n    :rtype: string\n    \"\"\"\n    return self.file\n
    "},{"location":"reference/topology_superimposer/","title":" topology_superimposer","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer","title":"topology_superimposer","text":"

    The main module responsible for the superimposition.

    Classes:

    • Atom \u2013
    • AtomPair \u2013

      An atom pair for networkx.

    • SuperimposedTopology \u2013

      SuperimposedTopology contains in the minimal case two sets of nodes S1 and S2, which

    Functions:

    • get_largest \u2013

      return a list of largest solutions

    • long_merge \u2013

      Carry out a merge and apply all checks.

    • merge_compatible_suptops \u2013

      Imagine mapping of two carbons C1 and C2 to another pair of carbons C1' and C2'.

    • merge_compatible_suptops_faster \u2013

      :param pairing_suptop:

    • superimpose_topologies \u2013

      The main function that manages the entire process.

    • extract_best_suptop \u2013

      Assumes that any merging possible already took place.

    • is_mirror_of_one \u2013

      \"Mirror\" in the sense that it is an alternative topological way to traverse the molecule.

    • generate_nxg_from_list \u2013

      Helper function. Generates a graph from a list of atoms

    • get_starting_configurations \u2013
      Minimise the number of starting configurations to optimise the process speed.\n
    • get_atoms_bonds_from_mol2 \u2013

      Use Parmed to load the files.

    • assign_coords_from_pdb \u2013

      Match the atoms from the ParmEd object based on a .pdb file

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.Atom","title":"Atom","text":"
    Atom(name, atom_type, charge=0, use_general_type=False)\n

    Methods:

    • eq \u2013

      Check if the atoms are of the same type and have a charge within the given absolute tolerance.

    • united_eq \u2013

      Like .eq, but treat the atoms as united atoms.

    Attributes:

    • united_charge \u2013

      United atom charge: summed charges of this atom and the bonded hydrogens.

    Source code in ties/topology_superimposer.py
    def __init__(self, name, atom_type, charge=0, use_general_type=False):\n    self._original_name = None\n\n    self._id = None\n    self.name = name\n    self._original_name = name.upper()\n    self.type = atom_type\n\n    self._resname = None\n    self.charge = charge\n    self._original_charge = charge\n\n    self.resid = None\n    self.bonds:Bonds = Bonds()\n    self.use_general_type = use_general_type\n    self.hash_value = None\n\n    self._unique_counter = Atom.counter\n    Atom.counter += 1\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.Atom.united_charge","title":"united_charge property","text":"
    united_charge\n

    United atom charge: summed charges of this atom and the bonded hydrogens.

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.Atom.eq","title":"eq","text":"
    eq(atom, atol=0)\n

    Check if the atoms are of the same type and have a charge within the given absolute tolerance.

    Source code in ties/topology_superimposer.py
    def eq(self, atom, atol=0):\n    \"\"\"\n    Check if the atoms are of the same type and have a charge within the given absolute tolerance.\n    \"\"\"\n    if self.type == atom.type and np.isclose(self.charge, atom.charge, atol=atol):\n        return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.Atom.united_eq","title":"united_eq","text":"
    united_eq(atom, atol=0)\n

    Like .eq, but treat the atoms as united atoms. Check if the atoms have the same atom type, and if if their charges are within the absolute tolerance. If the atoms have hydrogens, add up the attached hydrogens and use a united atom representation.

    Source code in ties/topology_superimposer.py
    def united_eq(self, atom, atol=0):\n    \"\"\"\n    Like .eq, but treat the atoms as united atoms.\n    Check if the atoms have the same atom type, and\n    if if their charges are within the absolute tolerance.\n    If the atoms have hydrogens, add up the attached hydrogens and use a united atom representation.\n    \"\"\"\n    if self.type != atom.type:\n        return False\n\n    if not np.isclose(self.united_charge, atom.united_charge, atol=atol):\n        return False\n\n    return True\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.AtomPair","title":"AtomPair","text":"
    AtomPair(left_node, right_node)\n

    An atom pair for networkx.

    Source code in ties/topology_superimposer.py
    def __init__(self, left_node, right_node):\n    self.left_atom = left_node\n    self.right_atom = right_node\n    # generate the hash value for this match\n    self.hash_value = self._gen_hash()\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology","title":"SuperimposedTopology","text":"
    SuperimposedTopology(topology1=None, topology2=None, parmed_ligA=None, parmed_ligZ=None)\n

    SuperimposedTopology contains in the minimal case two sets of nodes S1 and S2, which are paired together and represent a strongly connected component.

    However, it can also represent the symmetrical versions that were superimposed.

    Methods:

    • mcs_score \u2013

      Raturn a ratio of the superimposed atoms to the number of all atoms.

    • write_metadata \u2013

      Writes a .json file with a summary of which atoms are classified as appearing, disappearing

    • write_pdb \u2013

      param filename: name or a filepath of the new file. If None, standard preconfigured pattern will be used.

    • write_mol2 \u2013

      param filename: str location where the .mol2 file should be saved.

    • get_single_topology_region \u2013

      Return: matched atoms (even if they were unmatched for any reason)

    • get_single_topology_app \u2013

      fixme - called app but gives both app and dis

    • ringring \u2013

      Rings can only be matched to rings.

    • is_or_was_matched \u2013

      A helper function. For whatever reasons atoms get discarded.

    • get_unmatched_atoms \u2013

      Find the atoms in both topologies which were unmatched and return them.

    • get_unique_atom_count \u2013

      Requires that the .assign_atoms_ids() was called.

    • align_ligands_using_mcs \u2013

      Align the two ligands using the MCS (Maximum Common Substructure).

    • rm_matched_pairs_with_different_bonds \u2013

      Scan the matched pairs. Assume you have three pairs

    • get_dual_topology_bonds \u2013

      Get the bonds between all the atoms.

    • largest_cc_survives \u2013

      CC - Connected Component.

    • assign_atoms_ids \u2013

      Assign an ID to each pair A1-B1. This means that if we request an atom ID

    • get_appearing_atoms \u2013
    • get_disappearing_atoms \u2013
    • remove_lonely_hydrogens \u2013

      You could also remove the hydrogens when you correct charges.

    • match_gaff2_nondirectional_bonds \u2013

      If needed, swap cc-cd with cd-cc.

    • get_net_charge \u2013

      Calculate the net charge difference across

    • get_matched_with_diff_q \u2013

      Returns a list of matched atom pairs that have a different q,

    • apply_net_charge_filter \u2013

      Averaging the charges across paired atoms introduced inequalities.

    • remove_attached_hydrogens \u2013

      The node_pair to which these hydrogens are attached was removed.

    • find_lowest_rmsd_mirror \u2013

      Walk through the different mirrors and out of all options select the one

    • is_subgraph_of_global_top \u2013

      Check if after superimposition, one graph is a subgraph of another

    • rmsd \u2013

      For each pair take the distance, and then get rmsd, so root(mean(square(deviation)))

    • link_pairs \u2013

      This helps take care of the bonds.

    • find_mirror_choices \u2013

      For each pair (A1, B1) find all the other options in the mirrors where (A1, B2)

    • add_alternative_mapping \u2013

      This means that there is another way to traverse and overlap the two molecules,

    • correct_for_coordinates \u2013

      Use the coordinates of the atoms, to figure out which symmetries are the correct ones.

    • enforce_no_partial_rings \u2013

      http://www.alchemistry.org/wiki/Constructing_a_Pathway_of_Intermediate_States

    • get_topology_similarity_score \u2013

      Having the superimposed A(Left) and B(Right), score the match.

    • unmatch_pairs_with_different_charges \u2013

      Removes the matched pairs where atom charges are more different

    • is_consistent_with \u2013

      Conditions:

    • get_circles \u2013

      Return circles found in the matched pairs.

    • get_original_circles \u2013

      Return the original circles present in the input topologies.

    • cycle_spans_multiple_cycles \u2013

      What is the circle is shared?

    • merge \u2013

      Absorb the other suptop by adding all the node pairs that are not present

    • validate_charges \u2013

      Check the original charges:

    • redistribute_charges \u2013

      After the match is made and the user commits to the superimposed topology,

    • contains_same_atoms_symmetric \u2013

      The atoms can be paired differently, but they are the same.

    • is_subgraph_of \u2013

      Checks if this superimposed topology is a subgraph of another superimposed topology.

    • subgraph_relationship \u2013

      Return

    • is_mirror_of \u2013

      this is a naive check

    • eq \u2013

      Check if the superimposed topology is \"the same\". This means that every pair has a corresponding pair in the

    • toJSON \u2013

      \"

    Source code in ties/topology_superimposer.py
    def __init__(self, topology1=None, topology2=None, parmed_ligA=None, parmed_ligZ=None):\n    self.set_parmeds(parmed_ligA, parmed_ligZ)\n\n    \"\"\"\n    @superimposed_nodes : a set of pairs of nodes that matched together\n    \"\"\"\n    matched_pairs = []\n\n    # TEST: with the list of matching nodes, check if each node was used only once,\n    # the number of unique nodes should be equivalent to 2*len(common_pairs)\n    all_matched_nodes = []\n    [all_matched_nodes.extend(list(pair)) for pair in matched_pairs]\n    assert len(matched_pairs) * 2 == len(all_matched_nodes)\n\n    # fixme don't allow for initiating with matching pairs, it's not used anyway\n\n    # todo convert to nx? some other graph theory package?\n    matched_pairs.sort(key=lambda pair: pair[0].name)\n    self.matched_pairs = matched_pairs\n    self.top1 = topology1\n    self.top2 = topology2\n    # create graph representation for both in networkx library, initially to track the number of cycles\n    # fixme\n\n    self.mirrors = []\n    self.alternative_mappings = []\n    # this is a set of all nodes rather than their pairs\n    self.nodes = set(all_matched_nodes)\n    self.nodes_added_log = []\n\n    self.internal_ids = None\n    self.unique_atom_count = 0\n    self.matched_pairs_bonds = {}\n\n    # options\n    # Ambertools ignores the bonds when creating the .prmtop from the hybrid.mol2 file,\n    # so for now we can ignore the bond types\n    self.ignore_bond_types = True\n\n    # removed because\n    # fixme - make this into a list\n    self._removed_pairs_with_charge_difference = []    # atom-atom charge decided by qtol\n    self._removed_because_disjointed_cc = []    # disjointed segment\n    self._removed_due_to_net_charge = []\n    self._removed_because_unmatched_rings = []\n    self._removed_because_diff_bonds = []  # the atoms pair uses a different bond\n\n    # save the cycles in the left and right molecules\n    if self.top1 is not None and self.top2 is not None:\n        self._init_nonoverlapping_cycles()\n\n    self.id = SuperimposedTopology.COUNTER\n    SuperimposedTopology.COUNTER += 1\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology--fixme-should-check-first-if-atomname-is-unique","title":"fixme - should check first if atomName is unique","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology--fixme-should-check-first-if-atomname-is-unique","title":"fixme - should check first if atomName is unique","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.mcs_score","title":"mcs_score","text":"
    mcs_score()\n

    Raturn a ratio of the superimposed atoms to the number of all atoms. Specifically, (superimposed_atoms_number * 2) / (atoms_number_ligandA + atoms_number_ligandB) :return:

    Source code in ties/topology_superimposer.py
    def mcs_score(self):\n    \"\"\"\n    Raturn a ratio of the superimposed atoms to the number of all atoms.\n    Specifically, (superimposed_atoms_number * 2) / (atoms_number_ligandA + atoms_number_ligandB)\n    :return:\n    \"\"\"\n    return (len(self.matched_pairs) * 2) / (len(self.top1) + len(self.top2))\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.write_metadata","title":"write_metadata","text":"
    write_metadata(filename=None)\n

    Writes a .json file with a summary of which atoms are classified as appearing, disappearing as well as all other metadata relevant to this superimposition/hybrid. TODO add information: - config class in general -- relative paths to ligand 1, ligand 2 (the latest copies, ie renamed etc) -- general settings used - pair? bonds? these can be restractured, so not necessary?

    param filename: a location where the metadata should be saved\n
    Source code in ties/topology_superimposer.py
    def write_metadata(self, filename=None):\n    \"\"\"\n    Writes a .json file with a summary of which atoms are classified as appearing, disappearing\n    as well as all other metadata relevant to this superimposition/hybrid.\n    TODO add information:\n     - config class in general\n     -- relative paths to ligand 1, ligand 2 (the latest copies, ie renamed etc)\n     -- general settings used\n     - pair? bonds? these can be restractured, so not necessary?\n\n        param filename: a location where the metadata should be saved\n    \"\"\"\n\n    # store at the root for now\n    # fixme - should either be created or generated API\n    if filename is None:\n        matching_json = self.config.workdir / f'meta_{self.morph.ligA.internal_name}_{self.morph.ligZ.internal_name}.json'\n    else:\n        matching_json = pathlib.Path(filename)\n\n    matching_json.parent.mkdir(parents=True, exist_ok=True)\n\n    json.dump(self.toJSON(), open(matching_json, 'w'))\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.write_pdb","title":"write_pdb","text":"
    write_pdb(filename=None)\n

    param filename: name or a filepath of the new file. If None, standard preconfigured pattern will be used.

    Source code in ties/topology_superimposer.py
    def write_pdb(self, filename=None):\n    \"\"\"\n        param filename: name or a filepath of the new file. If None, standard preconfigured pattern will be used.\n    \"\"\"\n    if filename is None:\n        morph_pdb_path = self.config.workdir / f'{self.morph.ligA.internal_name}_{self.morph.ligZ.internal_name}_morph.pdb'\n    else:\n        morph_pdb_path = filename\n\n    # def write_morph_top_pdb(filepath, mda_l1, mda_l2, suptop, hybrid_single_dual_top=False):\n    if self.config.use_hybrid_single_dual_top:\n        # the NAMD hybrid single dual topology\n        # rename the ligand on the left to INI\n        # and the ligand on the right to END\n\n        # make a copy of the suptop here to ensure that the modifications won't affect it\n        st = copy.copy(self)\n\n        # first, set all the matched pairs to -2 and 2 (single topology)\n        # regardless of how they were mismatched\n        raise NotImplementedError('Cannot yet write hybrid single dual topology .pdb file')\n\n        # then, set the different atoms to -1 and 1 (dual topology)\n\n        # save in a single PDB file\n        # Note that the atoms from left to right\n        # in the single topology region have to\n        # be separated by the same number\n        # fixme - make a check for that\n        return\n    # fixme - find another library that can handle writing to a PDB file, MDAnalysis\n    # save the ligand with all the appropriate atomic positions, write it using the pdb format\n    # pdb file format: http://www.wwpdb.org/documentation/file-format-content/format33/sect9.html#ATOM\n    # write a dual .pdb file\n    with open(morph_pdb_path, 'w') as FOUT:\n        for atom in self.parmed_ligA.atoms:\n            \"\"\"\n            There is only one forcefield which is shared across the two topologies. \n            Basically, we need to check whether the atom is in both topologies. \n            If that is the case, then the atom should have the same name, and therefore appear only once. \n            However, if there is a new atom, it should be specfically be outlined \n            that it is 1) new and 2) the right type\n            \"\"\"\n            # write all the atoms if they are matched, that's the common part\n            # note that ParmEd does not have the information on a residue ID\n            REMAINS = 0\n            if self.contains_left_atom(atom.idx):\n                line = f\"ATOM  {atom.idx:>5d} {atom.name:>4s} {atom.residue.name:>3s}  \" \\\n                       f\"{1:>4d}    \" \\\n                       f\"{atom.xx:>8.3f}{atom.xy:>8.3f}{atom.xz:>8.3f}\" \\\n                       f\"{1.0:>6.2f}{REMAINS:>6.2f}\" + (' ' * 11) + \\\n                       '  ' + '  ' + '\\n'\n                FOUT.write(line)\n            else:\n                # this atom was not found, this means it disappears, so we should update the\n                DISAPPEARING_ATOM = -1.0\n                line = f\"ATOM  {atom.idx:>5d} {atom.name:>4s} {atom.residue.name:>3s}  \" \\\n                       f\"{1:>4d}    \" \\\n                       f\"{atom.xx:>8.3f}{atom.xy:>8.3f}{atom.xz:>8.3f}\" \\\n                       f\"{1.0:>6.2f}{DISAPPEARING_ATOM:>6.2f}\" + \\\n                       (' ' * 11) + \\\n                       '  ' + '  ' + '\\n'\n                FOUT.write(line)\n        # add atoms from the right topology,\n        # which are going to be created\n        for atom in self.parmed_ligZ.atoms:\n            if not self.contains_right_atom(atom.idx):\n                APPEARING_ATOM = 1.0\n                line = f\"ATOM  {atom.idx:>5d} {atom.name:>4s} {atom.residue.name:>3s}  \" \\\n                       f\"{1:>4d}    \" \\\n                       f\"{atom.xx:>8.3f}{atom.xy:>8.3f}{atom.xz:>8.3f}\" \\\n                       f\"{1.0:>6.2f}{APPEARING_ATOM:>6.2f}\" + \\\n                       (' ' * 11) + \\\n                       '  ' + '  ' + '\\n'\n                FOUT.write(line)\n    self.pdb = morph_pdb_path\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.write_mol2","title":"write_mol2","text":"
    write_mol2(filename=None, use_left_charges=True, use_left_coords=True)\n

    param filename: str location where the .mol2 file should be saved.

    Source code in ties/topology_superimposer.py
    def write_mol2(self, filename=None, use_left_charges=True, use_left_coords=True):\n    \"\"\"\n        param filename: str location where the .mol2 file should be saved.\n    \"\"\"\n    if filename is None:\n        hybrid_mol2 = self.config.workdir / f'{self.morph.ligA.internal_name}_{self.morph.ligZ.internal_name}_morph.mol2'\n    else:\n        hybrid_mol2 = filename\n\n    # fixme - make this as a method of suptop as well\n    # recreate the mol2 file that is merged and contains the correct atoms from both\n    # mol2 format: http://chemyang.ccnu.edu.cn/ccb/server/AIMMS/mol2.pdf\n    # fixme - build this molecule using the MDAnalysis builder instead of the current approach\n    # however, MDAnalysis currently cannot convert pdb into mol2? ...\n    # where the formatting is done manually\n    with open(hybrid_mol2, 'w') as FOUT:\n        bonds = self.get_dual_topology_bonds()\n\n        FOUT.write('@<TRIPOS>MOLECULE ' + os.linesep)\n        # name of the molecule\n        FOUT.write('HYB ' + os.linesep)\n        # num_atoms [num_bonds [num_subst [num_feat [num_sets]]]]\n        # fixme this is tricky\n        FOUT.write(f'{self.get_unique_atom_count():d} '\n                   f'{len(bonds):d}' + os.linesep)\n        # mole type\n        FOUT.write('SMALL ' + os.linesep)\n        # charge_type\n        FOUT.write('NO_CHARGES ' + os.linesep)\n        FOUT.write(os.linesep)\n\n        # write the atoms\n        FOUT.write('@<TRIPOS>ATOM ' + os.linesep)\n        # atom_id atom_name x y z atom_type [subst_id [subst_name [charge [status_bit]]]]\n        # e.g.\n        #       1 O4           3.6010   -50.1310     7.2170 o          1 L39      -0.815300\n\n        # so from the two topologies all the atoms are needed and they need to have a different atom_id\n        # so we might need to name the atom_id for them, other details are however pretty much the same\n        # the importance of atom_name is difficult to estimate\n\n        # we are going to assign IDs in the superimposed topology in order to track which atoms have IDs\n        # and which don't\n\n        # fixme - for writing, modify things to achieve the desired output\n        # note - we are modifying in place our atoms\n        for left, right in self.matched_pairs:\n            logger.debug(\n                f'Aligned {left.original_name} id {left.id} with {right.original_name} id {right.id}')\n            if not use_left_charges:\n                left.charge = right.charge\n            if not use_left_coords:\n                left.position = right.position\n\n        subst_id = 1  # resid basically\n        # write all the atoms that were matched first with their IDs\n        # prepare all the atoms, note that we use primarily the left ligand naming\n        all_atoms = [left for left, right in self.matched_pairs] + self.get_unmatched_atoms()\n        unmatched_atoms = self.get_unmatched_atoms()\n        # reorder the list according to the ID\n        all_atoms.sort(key=lambda atom: self.get_generated_atom_id(atom))\n\n        resname = 'HYB'\n        for atom in all_atoms:\n            FOUT.write(f'{self.get_generated_atom_id(atom)} {atom.name} '\n                       f'{atom.position[0]:.4f} {atom.position[1]:.4f} {atom.position[2]:.4f} '\n                       f'{atom.type.lower()} {subst_id} {resname} {atom.charge:.6f} {os.linesep}')\n\n        FOUT.write(os.linesep)\n\n        # write bonds\n        FOUT.write('@<TRIPOS>BOND ' + os.linesep)\n\n        # we have to list every bond:\n        # 1) all the bonds between the paired atoms, so that part is easy\n        # 2) bonds which link the disappearing atoms, and their connection to the paired atoms\n        # 3) bonds which link the appearing atoms, and their connections to the paired atoms\n\n        bond_counter = 1\n        list(bonds)\n        for bond_from_id, bond_to_id, bond_type in sorted(list(bonds), key=lambda b: b[:2]):\n            # Bond Line Format:\n            # bond_id origin_atom_id target_atom_id bond_type [status_bits]\n            FOUT.write(f'{bond_counter} {bond_from_id} {bond_to_id} {bond_type}' + os.linesep)\n            bond_counter += 1\n\n    self.mol2 = hybrid_mol2\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._init_nonoverlapping_cycles","title":"_init_nonoverlapping_cycles","text":"
    _init_nonoverlapping_cycles()\n

    Compile the cycles separately for the left and right molecule. Then, across the cycles, remove the nodes that join rings (double rings).

    Source code in ties/topology_superimposer.py
    def _init_nonoverlapping_cycles(self):\n    \"\"\"\n    Compile the cycles separately for the left and right molecule.\n    Then, across the cycles, remove the nodes that join rings (double rings).\n    \"\"\"\n    l_cycles, r_cycles = self.get_original_circles()\n    # remove any nodes that are shared between two cycles\n    for c1, c2 in itertools.combinations(l_cycles, r=2):\n        common = c1.intersection(c2)\n        for atom in common:\n            c1.remove(atom)\n            c2.remove(atom)\n\n    # same for r_cycles\n    for c1, c2 in itertools.combinations(r_cycles, r=2):\n        common = c1.intersection(c2)\n        for atom in common:\n            c1.remove(atom)\n            c2.remove(atom)\n\n    self._nonoverlapping_l_cycles = l_cycles\n    self._nonoverlapping_r_cycles = r_cycles\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_single_topology_region","title":"get_single_topology_region","text":"
    get_single_topology_region()\n

    Return: matched atoms (even if they were unmatched for any reason)

    Source code in ties/topology_superimposer.py
    def get_single_topology_region(self):\n    \"\"\"\n    Return: matched atoms (even if they were unmatched for any reason)\n    \"\"\"\n    # strip the pairs of the exact information about the charge differences\n    removed_pairs_with_charge_difference = [(n1, n2) for (n1, n2), q_diff in\n                                            self._removed_pairs_with_charge_difference]\n\n    # fixme: this should not work with disjointed cc and others?\n    unpaired = self._removed_because_disjointed_cc + self._removed_due_to_net_charge + \\\n        removed_pairs_with_charge_difference\n\n    return self.matched_pairs + unpaired\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_single_topology_app","title":"get_single_topology_app","text":"
    get_single_topology_app()\n

    fixme - called app but gives both app and dis get the appearing and disappearing region in the hybrid single topology use the single topology region and classify all other atoms not in it as either appearing or disappearing

    Source code in ties/topology_superimposer.py
    def get_single_topology_app(self):\n    \"\"\"\n    fixme - called app but gives both app and dis\n    get the appearing and disappearing region in the hybrid single topology\n    use the single topology region and classify all other atoms not in it\n    as either appearing or disappearing\n    \"\"\"\n    single_top_area = self.get_single_topology_region()\n\n    # turn it into a set\n    single_top_set = set()\n    for left, right in single_top_area:\n        single_top_set.add(left)\n        single_top_set.add(right)\n\n    # these unmatched atoms could be due to charge etc.\n    # so they historically refer to the dual-topology\n    unmatched_app = self.get_appearing_atoms()\n    app = {a for a in unmatched_app if a not in single_top_set}\n    unmatched_dis = self.get_disappearing_atoms()\n    dis = {a for a in unmatched_dis if a not in single_top_set}\n\n    return app, dis\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.ringring","title":"ringring","text":"
    ringring()\n

    Rings can only be matched to rings.

    Source code in ties/topology_superimposer.py
    def ringring(self):\n    \"\"\"\n    Rings can only be matched to rings.\n    \"\"\"\n    l_circles, r_circles = self.get_original_circles()\n    removed_h = []\n    ringring_removed = []\n    for l, r in self.matched_pairs[::-1]:\n        if (l, r) in removed_h:\n            continue\n\n        l_ring = any([l in c for c in l_circles])\n        r_ring = any([r in c for c in r_circles])\n        if l_ring + r_ring == 1:\n            removed_h.extend(self.remove_attached_hydrogens((l, r)))\n            self.remove_node_pair((l, r))\n            ringring_removed.append((l,r))\n\n    if ringring_removed:\n        logger.debug(f'(ST{self.id}) Ring only matches ring filter, removed: {ringring_removed} with hydrogens {removed_h}')\n    return ringring_removed, removed_h\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.is_or_was_matched","title":"is_or_was_matched","text":"
    is_or_was_matched(atom_name1, atom_name2)\n

    A helper function. For whatever reasons atoms get discarded. E.g. they had a different charge, or were part of the disjointed component, etc. This function simply checks if the most original match was made between the two atoms. It helps with verifying the original matching.

    Source code in ties/topology_superimposer.py
    def is_or_was_matched(self, atom_name1, atom_name2):\n    \"\"\"\n    A helper function. For whatever reasons atoms get discarded.\n    E.g. they had a different charge, or were part of the disjointed component, etc.\n    This function simply checks if the most original match was made between the two atoms.\n    It helps with verifying the original matching.\n    \"\"\"\n    if self.contains_atom_name_pair(atom_name1, atom_name2):\n        return True\n\n    # check if it was unmatched\n    unmatched_lists = [\n                        self._removed_because_disjointed_cc,\n                        # ignore the charges in this list\n                        [pair for pair, q in self._removed_due_to_net_charge],\n                        [pair for pair, q in self._removed_pairs_with_charge_difference]\n                       ]\n    for unmatched_list in unmatched_lists:\n        for atom1, atom2 in unmatched_list:\n            if atom1.name == atom_name1 and atom2.name == atom_name2:\n                return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_unmatched_atoms","title":"get_unmatched_atoms","text":"
    get_unmatched_atoms()\n

    Find the atoms in both topologies which were unmatched and return them. These are both, appearing and disappearing.

    Note that some atoms were removed due to charges.

    Source code in ties/topology_superimposer.py
    def get_unmatched_atoms(self):\n    \"\"\"\n    Find the atoms in both topologies which were unmatched and return them.\n    These are both, appearing and disappearing.\n\n    Note that some atoms were removed due to charges.\n    \"\"\"\n    unmatched_atoms = []\n    for node in self.top1:\n        if not self.contains_node(node):\n            unmatched_atoms.append(node)\n\n    for node in self.top2:\n        if not self.contains_node(node):\n            unmatched_atoms.append(node)\n\n    return unmatched_atoms\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_unique_atom_count","title":"get_unique_atom_count","text":"
    get_unique_atom_count()\n

    Requires that the .assign_atoms_ids() was called. This should be rewritten. But basically, it needs to count each matched pair as one atom, and the appearing and disappearing atoms separately.

    Source code in ties/topology_superimposer.py
    def get_unique_atom_count(self):\n    \"\"\"\n    Requires that the .assign_atoms_ids() was called.\n    This should be rewritten. But basically, it needs to count each matched pair as one atom,\n    and the appearing and disappearing atoms separately.\n    \"\"\"\n    return self.unique_atom_count\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.align_ligands_using_mcs","title":"align_ligands_using_mcs","text":"
    align_ligands_using_mcs(overwrite_original=False)\n

    Align the two ligands using the MCS (Maximum Common Substructure). The ligA here is the reference (docked) to which the ligZ is aligned.

    :param overwrite_original: After aligning by MCS, update the internal coordinates which will be saved to a file at the end. :type overwrite_original: bool

    Source code in ties/topology_superimposer.py
    def align_ligands_using_mcs(self, overwrite_original=False):\n    \"\"\"\n    Align the two ligands using the MCS (Maximum Common Substructure).\n    The ligA here is the reference (docked) to which the ligZ is aligned.\n\n    :param overwrite_original: After aligning by MCS, update the internal coordinates\n        which will be saved to a file at the end.\n    :type overwrite_original: bool\n    \"\"\"\n\n    if self.mda_ligA is None or self.mda_ligB is None:\n        # todo comment\n        return self.rmsd()\n\n    ligA = self.mda_ligA\n    ligB = self.mda_ligB\n\n    # back up\n    ligA_original_positions = ligA.atoms.positions[:]\n    ligB_original_positions = ligB.atoms.positions[:]\n\n    # select the atoms for the MCS,\n    # the following uses 0-based indexing\n    mcs_ligA_ids = [left.id for left, right in self.matched_pairs]\n    mcs_ligB_ids = [right.id for left, right in self.matched_pairs]\n\n    ligA_fragment = ligA.atoms[mcs_ligA_ids]\n    ligB_fragment = ligB.atoms[mcs_ligB_ids]\n\n    # move all to the origin of the fragment\n    ligA_mcs_centre = ligA_fragment.centroid()\n    ligA.atoms.translate(-ligA_mcs_centre)\n    ligB.atoms.translate(-ligB_fragment.centroid())\n\n    rotation_matrix, rmsd = MDAnalysis.analysis.align.rotation_matrix(ligB_fragment.positions, ligA_fragment.positions)\n\n    # apply the rotation to\n    ligB.atoms.rotate(rotation_matrix)\n    # move back to ligA\n    ligB.atoms.translate(ligA_mcs_centre)\n\n    # save the superimposed coordinates\n    ligB_sup = self.mda_ligB.atoms.positions[:]\n\n    # restore the MDAnalysis positions (\"working copy\")\n    # in theory you do not need to do this every time\n    self.mda_ligA.atoms.positions = ligA_original_positions\n    self.mda_ligB.atoms.positions = ligB_original_positions\n\n    if not overwrite_original:\n        # return the RMSD of the superimposed matched pairs only\n        return rmsd\n\n    # update the atoms with the mapping done via IDs\n    logger.debug(f'Aligned by MCS with the RMSD value {rmsd}')\n\n    # use the aligned coordinates\n    self.parmed_ligZ.coordinates = ligB_sup\n\n    # ideally this would now be done with MDAnalysis which can now write .mol2\n    # overwrite the internal atom positions with the final generated alignment\n    for parmed_atom in self.parmed_ligZ.atoms:\n        found = False\n        for atom in self.top2:\n            if parmed_atom.idx == atom.id:\n                atom.position = parmed_atom.xx, parmed_atom.xy, parmed_atom.xz\n                found = True\n                break\n        assert found\n\n    return rmsd\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.rm_matched_pairs_with_different_bonds","title":"rm_matched_pairs_with_different_bonds","text":"
    rm_matched_pairs_with_different_bonds()\n

    Scan the matched pairs. Assume you have three pairs A-B=C with the double bond on the right side, and the alternative bonds A=B-C remove all A, B and C pairs because of the different bonds Remove them by finding that A-B is not A=B, and B=C is not B-C

    return: the list of removed pairs

    Source code in ties/topology_superimposer.py
    def rm_matched_pairs_with_different_bonds(self):\n    \"\"\"\n    Scan the matched pairs. Assume you have three pairs\n    A-B=C with the double bond on the right side,\n    and the alternative bonds\n    A=B-C remove all A, B and C pairs because of the different bonds\n    Remove them by finding that A-B is not A=B, and B=C is not B-C\n\n    return: the list of removed pairs\n    \"\"\"\n\n    # extract the bonds for the matched molecules first\n    removed_pairs = []\n    for from_pair, bonded_pair_list in list(self.matched_pairs_bonds.items())[::-1]:\n        for bonded_pair, bond_type in bonded_pair_list:\n            # ignore if this combination was already checked\n            if bonded_pair in removed_pairs and from_pair in removed_pairs:\n                continue\n\n            if bond_type[0] != bond_type[1]:\n                # resolve this, remove the bonded pair from the matched atoms\n                if from_pair not in removed_pairs:\n                    self.remove_node_pair(from_pair)\n                    removed_pairs.append(from_pair)\n                if bonded_pair not in removed_pairs:\n                    self.remove_node_pair(bonded_pair)\n                    removed_pairs.append(bonded_pair)\n\n                # keep the history\n                self._removed_because_diff_bonds.append((from_pair, bonded_pair))\n\n    return removed_pairs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_dual_topology_bonds","title":"get_dual_topology_bonds","text":"
    get_dual_topology_bonds()\n

    Get the bonds between all the atoms. Use the atom IDs for the bonds.

    Source code in ties/topology_superimposer.py
    def get_dual_topology_bonds(self):\n    \"\"\"\n    Get the bonds between all the atoms.\n    Use the atom IDs for the bonds.\n    \"\"\"\n    assert self.top1 is not None and self.top2 is not None\n    # fixme - check if the atoms IDs have been generated\n    assert self.internal_ids is not None\n\n    # extract the bonds for the matched molecules first\n    bonds = set()\n    for from_pair, bonded_pair_list in self.matched_pairs_bonds.items():\n        from_pair_id = self.get_generated_atom_id(from_pair)\n        for bonded_pair, bond_type in bonded_pair_list:\n            if not self.ignore_bond_types:\n                if bond_type[0] != bond_type[1]:\n                    logger.error(f'ERROR: bond types do not match, even though they apply to the same atoms')\n                    logger.error(f'ERROR: left bond is \"{bond_type[0]}\" and right bond is \"{bond_type[1]}\"')\n                    logger.error(f'ERROR: the bonded atoms are {bonded_pair}')\n                    raise Exception('The bond types do not correspond to each other')\n            # every bonded pair has to be in the topology\n            assert bonded_pair in self.matched_pairs\n            to_pair_id = self.get_generated_atom_id(bonded_pair)\n            # before adding them to bonds, check if they are not already there\n            bond_sorted = sorted([from_pair_id, to_pair_id])\n            bond_sorted.append(bond_type[0])\n            bonds.add(tuple(bond_sorted))\n\n    # extract the bond information from the unmatched\n    unmatched_atoms = self.get_unmatched_atoms()\n    # for every atom, check to which \"pair\" the bond connects,\n    # and use that pair's ID to make the link\n\n    # several iterations of walking through the atoms,\n    # this is to ensure that we remove each atom one by one\n    # e.g. imagine this PAIR-SingleA1-SingleA2-SingleA3\n    # so only the first SingleA1 is connected to a pair,\n    # so the first iteration would take care of that,\n    # the next iteration would connect SingleA2 to SingleA1, etc\n    # first, remove the atoms that are connected to pairs\n    for atom in unmatched_atoms:\n        for bond in atom.bonds:\n            unmatched_atom_id = self.get_generated_atom_id(atom)\n            # check if the unmatched atom is bonded to any pair\n            pair = self.find_pair_with_atom(bond.atom)\n            if pair is not None:\n                # this atom is bound to a pair, so add the bond to the pair\n                pair_id = self.get_generated_atom_id(pair[0])\n                # add the bond between the atom and the pair\n                bond_sorted = sorted([unmatched_atom_id, pair_id])\n                bond_sorted.append(bond.type)\n                bonds.add(tuple(bond_sorted))\n            else:\n                # it is not directly linked to a matched pair,\n                # simply add this missing bond to whatever atom it is bound\n                another_unmatched_atom_id = self.get_generated_atom_id(bond.atom)\n                bond_sorted = sorted([unmatched_atom_id, another_unmatched_atom_id])\n                bond_sorted.append(bond.type)\n                bonds.add(tuple(bond_sorted))\n\n    # fixme - what about circles etc? these bonds\n    # that form circles should probably be added while checking if the circles make sense etc\n    # also, rather than checking if it is a circle, we could check if the new linked atom,\n    # is in a pair to which the new pair refers (the same rule that is used currently)\n    return bonds\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.largest_cc_survives","title":"largest_cc_survives","text":"
    largest_cc_survives(verbose=True)\n

    CC - Connected Component.

    Removes any disjoint components. Only the largest CC will be left. In the case of of equal length CCs, an arbitrary is chosen.

    How: Generates the graph where each pair is a single node, connecting the nodes if the bonds exist. Uses then networkx to find CCs.

    Source code in ties/topology_superimposer.py
    def largest_cc_survives(self, verbose=True):\n    \"\"\"\n    CC - Connected Component.\n\n    Removes any disjoint components. Only the largest CC will be left.\n    In the case of of equal length CCs, an arbitrary is chosen.\n\n    How:\n    Generates the graph where each pair is a single node, connecting the nodes if the bonds exist.\n    Uses then networkx to find CCs.\n    \"\"\"\n\n    if len(self) == 0:\n        return self, []\n\n    def lookup_up(pairs, tuple_pair):\n        for pair in pairs:\n            if pair.is_pair(tuple_pair):\n                return pair\n\n        raise Exception('Did not find the AtomPair')\n\n    g = nx.Graph()\n    atom_pairs = []\n    for pair in self.matched_pairs:\n        ap = AtomPair(pair[0], pair[1])\n        atom_pairs.append(ap)\n        g.add_node(ap)\n\n    # connect the atom pairs\n    for pair_from, pair_list in self.matched_pairs_bonds.items():\n        # lookup the corresponding atom pairs\n        ap_from = lookup_up(atom_pairs, pair_from)\n        for tuple_pair, bond_type in pair_list:\n            ap_to = lookup_up(atom_pairs, tuple_pair)\n            g.add_edge(ap_from, ap_to)\n\n    # check for connected components (CC)\n    remove_ccs = []\n    ccs = [g.subgraph(cc).copy() for cc in nx.connected_components(g)]\n    largest_cc = max([len(cc) for cc in ccs])\n\n    # there are disjoint fragments, remove the smaller one\n    for cc in ccs[::-1]:\n        # remove the cc if it smaller than the largest component\n        if len(cc) < largest_cc:\n            remove_ccs.append(cc)\n            ccs.remove(cc)\n\n    # remove the cc that have a smaller number of heavy atoms\n    largest_heavy_atom_cc = max([len([p for p in cc.nodes() if p.is_heavy_atom()])\n                                                    for cc in ccs])\n    for cc in ccs[::-1]:\n        if len([p for p in cc if p.is_heavy_atom()]) < largest_heavy_atom_cc:\n            if verbose:\n                logger.debug('Found CC that had fewer heavy atoms. Removing. ')\n            remove_ccs.append(cc)\n            ccs.remove(cc)\n\n    # remove the cc that has a smaller number of rings\n    largest_cycle_num = max([len(nx.cycle_basis(cc)) for cc in ccs])\n    for cc in ccs[::-1]:\n        if len(nx.cycle_basis(cc)) < largest_cycle_num:\n            if verbose:\n                logger.debug('Found CC that had fewer cycles. Removing. ')\n            remove_ccs.append(cc)\n            ccs.remove(cc)\n\n    # remove cc that has a smaller number of heavy atoms across rings\n    most_heavy_atoms_in_cycles = 0\n    for cc in ccs[::-1]:\n        # count the heavy atoms across the cycles\n        heavy_atom_counter = 0\n        for cycle in nx.cycle_basis(cc):\n            for a in cycle:\n                if a.is_heavy_atom():\n                    heavy_atom_counter += 1\n        if heavy_atom_counter > most_heavy_atoms_in_cycles:\n            most_heavy_atoms_in_cycles = heavy_atom_counter\n\n    for cc in ccs[::-1]:\n        # count the heavy atoms across the cycles\n        heavy_atom_counter = 0\n        for cycle in nx.cycle_basis(cc):\n            for a in cycle:\n                if a.is_heavy_atom():\n                    heavy_atom_counter += 1\n\n        if heavy_atom_counter < most_heavy_atoms_in_cycles:\n            if verbose:\n                logger.debug('Found CC that had fewer heavy atoms in cycles. Removing. ')\n            remove_ccs.append(cc)\n            ccs.remove(cc)\n\n    if len(ccs) > 1:\n        # there are equally large CCs\n        if verbose:\n            logger.debug(\"The Connected Components are equally large! Picking the first one\")\n        for cc in ccs[1:]:\n            remove_ccs.append(cc)\n            ccs.remove(cc)\n\n    assert len(ccs) == 1, \"At this point there should be left only one main component\"\n\n    # remove the worse cc\n    for cc in remove_ccs:\n        for atom_pair in cc:\n            atom_tuple = (atom_pair.left_atom, atom_pair.right_atom)\n            self.remove_node_pair(atom_tuple)\n            self._removed_because_disjointed_cc.append(atom_tuple)\n\n    return largest_cc, remove_ccs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.assign_atoms_ids","title":"assign_atoms_ids","text":"
    assign_atoms_ids(id_start=1)\n

    Assign an ID to each pair A1-B1. This means that if we request an atom ID for A1 or B1 it will be the same.

    Then assign different IDs for the other atoms

    Source code in ties/topology_superimposer.py
    def assign_atoms_ids(self, id_start=1):\n    \"\"\"\n    Assign an ID to each pair A1-B1. This means that if we request an atom ID\n    for A1 or B1 it will be the same.\n\n    Then assign different IDs for the other atoms\n    \"\"\"\n    self.internal_ids = {}\n    id_counter = id_start\n    # for each pair assign an ID\n    for left_atom, right_atom in self.matched_pairs:\n        self.internal_ids[left_atom] = id_counter\n        self.internal_ids[right_atom] = id_counter\n        # make it possible to look up the atom ID with a pair\n        self.internal_ids[(left_atom, right_atom)] = id_counter\n\n        id_counter += 1\n        self.unique_atom_count += 1\n\n    # for each atom that was not mapped to any other atom,\n    # but is still in the topology, generate an ID for it\n\n    # find the not mapped atoms in the left topology and assign them an atom ID\n    for node in self.top1:\n        # check if this node was matched\n        if not self.contains_node(node):\n            self.internal_ids[node] = id_counter\n            id_counter += 1\n            self.unique_atom_count += 1\n\n    # find the not mapped atoms in the right topology and assign them an atom ID\n    for node in self.top2:\n        # check if this node was matched\n        if not self.contains_node(node):\n            self.internal_ids[node] = id_counter\n            id_counter += 1\n            self.unique_atom_count += 1\n\n    # return the last atom\n    return id_counter\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_appearing_atoms","title":"get_appearing_atoms","text":"
    get_appearing_atoms()\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_appearing_atoms--fixme-should-check-first-if-atomname-is-unique","title":"fixme - should check first if atomName is unique","text":"

    Return a list of appearing atoms (atomName) which are the atoms that are

    Source code in ties/topology_superimposer.py
    def get_appearing_atoms(self):\n    \"\"\"\n    # fixme - should check first if atomName is unique\n    Return a list of appearing atoms (atomName) which are the\n    atoms that are\n    \"\"\"\n    unmatched = []\n    for top2_atom in self.top2:\n        is_matched = False\n        for _, matched_right_ligand_atom in self.matched_pairs:\n            if top2_atom is matched_right_ligand_atom:\n                is_matched = True\n                break\n        if not is_matched:\n            unmatched.append(top2_atom)\n\n    return unmatched\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_disappearing_atoms","title":"get_disappearing_atoms","text":"
    get_disappearing_atoms()\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_disappearing_atoms--fixme-should-check-first-if-atomname-is-unique","title":"fixme - should check first if atomName is unique","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_disappearing_atoms--fixme-update-to-using-the-node-set","title":"fixme - update to using the node set","text":"

    Return a list of appearing atoms (atomName) which are the atoms that are found in the topology, and that are not present in the matched_pairs

    Source code in ties/topology_superimposer.py
    def get_disappearing_atoms(self):\n    \"\"\"\n    # fixme - should check first if atomName is unique\n    # fixme - update to using the node set\n    Return a list of appearing atoms (atomName) which are the\n    atoms that are found in the topology, and that\n    are not present in the matched_pairs\n    \"\"\"\n    unmatched = []\n    for top1_atom in self.top1:\n        is_matched = False\n        for matched_left_ligand_atom, _ in self.matched_pairs:\n            if top1_atom is matched_left_ligand_atom:\n                is_matched = True\n                break\n        if not is_matched:\n            unmatched.append(top1_atom)\n\n    return unmatched\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.remove_lonely_hydrogens","title":"remove_lonely_hydrogens","text":"
    remove_lonely_hydrogens()\n

    You could also remove the hydrogens when you correct charges.

    Source code in ties/topology_superimposer.py
    def remove_lonely_hydrogens(self):\n    \"\"\"\n    You could also remove the hydrogens when you correct charges.\n    \"\"\"\n    logger.error('ERROR: function used that was not verified. It can create errors. '\n          'Please verify that the code works first.')\n    # in order to see any hydrogens that are by themselves, we check for any connection\n    removed_pairs = []\n    for A1, B1 in self.matched_pairs:\n        # fixme - assumes hydrogens start their names with H*\n        if not A1.name.upper().startswith('H'):\n            continue\n\n        # check if any of the bonded atoms can be found in this sup top\n        if not self.contains_any(A1.bonds) or not self.contains_node(B1.bonds):\n            # we appear disconnected, remove us\n            pass\n        for bonded_atom in A1.bonds:\n            assert not bonded_atom.name.upper().startswith('H')\n            if self.contains_node(bonded_atom):\n                continue\n\n    return removed_pairs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.match_gaff2_nondirectional_bonds","title":"match_gaff2_nondirectional_bonds","text":"
    match_gaff2_nondirectional_bonds()\n

    If needed, swap cc-cd with cd-cc. If two pairs are linked: (CC/CD) - (CD/CC), replace them according to the left side: (CC/CC) - (CD/CD). Apply this rule to all other pairs in Table I (b) at http://ambermd.org/antechamber/gaff.html

    These two define where the double bond is in a ring. GAFF decides on which one is cc or cd depending on the arbitrary atom order. This intervention we ensure that we do not remove atoms based on that arbitrary order.

    This method is idempotent.

    Source code in ties/topology_superimposer.py
    def match_gaff2_nondirectional_bonds(self):\n    \"\"\"\n    If needed, swap cc-cd with cd-cc.\n    If two pairs are linked: (CC/CD) - (CD/CC),\n    replace them according to the left side: (CC/CC) - (CD/CD).\n    Apply this rule to all other pairs in Table I (b) at http://ambermd.org/antechamber/gaff.html\n\n    These two define where the double bond is in a ring.\n    GAFF decides on which one is cc or cd depending on the arbitrary atom order.\n    This intervention we ensure that we do not remove atoms based on that arbitrary order.\n\n    This method is idempotent.\n    \"\"\"\n    nondirectionals = ({'CC', 'CD'}, {'CE', 'CF'}, {'CP', 'CQ'},\n                         {'PC', 'PD'}, {'PE', 'PF'},\n                         {'NC', 'ND'})\n\n    for no_direction_pair in nondirectionals:\n        corrected_pairs = []\n        for A1, A2 in self.matched_pairs:\n            # check if it is the right combination\n            if not {A1.type, A2.type} == no_direction_pair or (A1, A2) in corrected_pairs:\n                continue\n\n            # ignore if they are already the same\n            if A2.type == A1.type:\n                continue\n\n            # fixme - temporary solution\n            # fixme - do we want to check if we are in a ring?\n            # for now we are simply rewriting the types here so that it passes the \"specific atom type\" checks later\n            # ie so that later CC-CC and CD-CD are compared\n            # fixme - check if .type is used when writing the final output.\n            A2.type = A1.type\n            logger.debug(f'Arbitrary atom type correction. '\n                  f'Right atom type {A2.type} (in {A2}) overwritten with left atom type {A1.type} (in {A1}). ')\n\n            corrected_pairs.append((A1, A2))\n\n    return 0\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_net_charge","title":"get_net_charge","text":"
    get_net_charge()\n

    Calculate the net charge difference across the matched pairs.

    Source code in ties/topology_superimposer.py
    def get_net_charge(self):\n    \"\"\"\n    Calculate the net charge difference across\n    the matched pairs.\n    \"\"\"\n    net_charge = sum(n1.charge - n2.charge for n1, n2 in self.matched_pairs)\n    return net_charge\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_matched_with_diff_q","title":"get_matched_with_diff_q","text":"
    get_matched_with_diff_q()\n

    Returns a list of matched atom pairs that have a different q, sorted in the descending order (the first pair has the largest q diff).

    Source code in ties/topology_superimposer.py
    def get_matched_with_diff_q(self):\n    \"\"\"\n    Returns a list of matched atom pairs that have a different q,\n    sorted in the descending order (the first pair has the largest q diff).\n    \"\"\"\n    diff_q = [(n1, n2) for n1, n2 in self.matched_pairs if np.abs(n1.united_charge - n2.united_charge) > 0]\n    return sorted(diff_q, key=lambda p: abs(p[0].united_charge - p[1].united_charge), reverse=True)\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.apply_net_charge_filter","title":"apply_net_charge_filter","text":"
    apply_net_charge_filter(net_charge_threshold)\n

    Averaging the charges across paired atoms introduced inequalities. Check if the sum of the inequalities in charges is below net_charge. If not, remove pairs until that net_charge is met. Which pairs are removed depends on the approach. Greedy removal of the pairs with the highest difference can create disjoint blocks which creates issues in themselves.

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.apply_net_charge_filter--specifically-create-copies-for-each-strategy-here-and-try-a-couple-of-them","title":"Specifically, create copies for each strategy here and try a couple of them.","text":"

    Returns: a new suptop where the net_charge_threshold is enforced.

    Source code in ties/topology_superimposer.py
    def apply_net_charge_filter(self, net_charge_threshold):\n    \"\"\"\n    Averaging the charges across paired atoms introduced inequalities.\n    Check if the sum of the inequalities in charges is below net_charge.\n    If not, remove pairs until that net_charge is met.\n    Which pairs are removed depends on the approach.\n    Greedy removal of the pairs with the highest difference\n    can create disjoint blocks which creates issues in themselves.\n\n    # Specifically, create copies for each strategy here and try a couple of them.\n    Returns: a new suptop where the net_charge_threshold is enforced.\n    \"\"\"\n\n    approaches = ['greedy', 'terminal_alch_linked', 'terminal', 'alch_linked', 'leftovers', 'smart']\n    rm_disjoint_at_each_step = [True, False]\n\n    # best configuration info\n    best_approach = None\n    suptop_size = -1\n    rm_disjoint_each_step_conf = False\n\n    # try all confs\n    for rm_disjoint_each_step in rm_disjoint_at_each_step:\n        for approach in approaches:\n            # make a shallow copy of the suptop\n            next_approach = copy.copy(self)\n            # first overall\n            if rm_disjoint_each_step:\n                next_approach.largest_cc_survives(verbose=False)\n\n            # try the strategy\n            while np.abs(next_approach.get_net_charge()) > net_charge_threshold:\n\n                best_candidate_with_h = next_approach._smart_netqtol_pair_picker(approach)\n                for pair in best_candidate_with_h:\n                    next_approach.remove_node_pair(pair)\n\n                if rm_disjoint_each_step:\n                    next_approach.largest_cc_survives(verbose=False)\n\n            # regardless of whether the continuous disjoint removal is being tried or not,\n            # it will be applied at the end\n            # so apply it here at the end in order to make this comparison equivalent\n            next_approach.largest_cc_survives(verbose=False)\n\n            if len(next_approach) > suptop_size:\n                suptop_size = len(next_approach)\n                best_approach = approach\n                rm_disjoint_each_step_conf = rm_disjoint_each_step\n\n    # apply the best strategy to this suptop\n    logger.debug(f'Pair removal strategy (q net tol): {best_approach} with disjoint CC removed at each step: {rm_disjoint_each_step_conf}')\n    logger.debug(f'To meet q net tol: {best_approach}')\n\n    total_diff = 0\n    if rm_disjoint_each_step_conf:\n        self.largest_cc_survives()\n    while np.abs(self.get_net_charge()) > net_charge_threshold:\n        best_candidate_with_h = self._smart_netqtol_pair_picker(best_approach)\n\n        # remove them\n        for pair in best_candidate_with_h:\n            self.remove_node_pair(pair)\n            diff_q_pairs = abs(pair[0].united_charge - pair[1].united_charge)\n            # add to the list of removed because of the net charge\n            self._removed_due_to_net_charge.append([pair, diff_q_pairs])\n            total_diff += diff_q_pairs\n\n        if rm_disjoint_each_step_conf:\n            self.largest_cc_survives()\n\n    return total_diff\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._smart_netqtol_pair_picker","title":"_smart_netqtol_pair_picker","text":"
    _smart_netqtol_pair_picker(strategy)\n

    The appearing and disappearing alchemical region have their cumulative q different by more than the netq (0.1 typically). Find the next pair with q imbalance that contributes to it. Instead of using the greedy strategy: - avoid bottleneck atoms (the removed atoms split the molecule into smaller parts) - use atoms that are close to the already mutating site

    @param strategy: 'greedy', 'terminal_alch_linked', 'terminal', 'alch_linked', 'leftovers', 'smart'

    Source code in ties/topology_superimposer.py
    def _smart_netqtol_pair_picker(self, strategy):\n    \"\"\"\n    The appearing and disappearing alchemical region have their\n    cumulative q different by more than the netq (0.1 typically).\n    Find the next pair with q imbalance that contributes to it.\n    Instead of using the greedy strategy:\n      - avoid bottleneck atoms (the removed atoms split the molecule into smaller parts)\n      - use atoms that are close to the already mutating site\n\n    @param strategy: 'greedy', 'terminal_alch_linked', 'terminal', 'alch_linked', 'leftovers', 'smart'\n    \"\"\"\n    # get pairs with different charges\n    diff_q_pairs = self.get_matched_with_diff_q()\n    if len(diff_q_pairs) == 0:\n        raise Exception('Did not find any pairs with a different q even though the net tol is not met? ')\n\n    # sort the pairs into categories\n    # use 5 pairs with the largest difference\n    diff_sorted = self._sort_pairs_into_categories_qnettol(diff_q_pairs, best_cases_num=5)\n\n    if strategy == 'smart':\n        # get the most promising category\n        for cat in diff_sorted.keys():\n            if diff_sorted[cat]:\n                category = diff_sorted[cat]\n                break\n        # remove the first pair\n        # fixme - double check this\n        return category[0]\n\n    # allow removal of pairs even if the differences are small\n    diff_sorted = self._sort_pairs_into_categories_qnettol(diff_q_pairs, best_cases_num=len(self))\n\n    # for other strategies, take the key directly, but only if there is one\n    if diff_sorted[strategy]:\n        pairs_in_category = diff_sorted[strategy]\n    else:\n        # if there is no option in that category, revert to greedy\n        pairs_in_category = diff_sorted['greedy']\n    return pairs_in_category[0]\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._sort_pairs_into_categories_qnettol","title":"_sort_pairs_into_categories_qnettol","text":"
    _sort_pairs_into_categories_qnettol(pairs, best_cases_num=6)\n

    This is a helper function which sorts matched pairs with different charges into categories, which are: - terminal_alch_linked - terminal: at most one heavy atom bonded - alch_linked: at least one bond to the alchemical region - leftovers: not terminal or alch_linked, - low_diff

    Returns: Ordered Dictionary

    Source code in ties/topology_superimposer.py
    def _sort_pairs_into_categories_qnettol(self, pairs, best_cases_num=6):\n    \"\"\"\n    This is a helper function which sorts\n    matched pairs with different charges into categories, which are:\n     - terminal_alch_linked\n     - terminal: at most one heavy atom bonded\n     - alch_linked: at least one bond to the alchemical region\n     - leftovers: not terminal or alch_linked,\n     - low_diff\n\n    Returns: Ordered Dictionary\n    \"\"\"\n\n    sorted_categories = OrderedDict()\n    sorted_categories['terminal_alch_linked'] = []\n    sorted_categories['terminal'] = []\n    sorted_categories['alch_linked'] = []\n    sorted_categories['greedy'] = []\n    sorted_categories['leftovers'] = []\n    sorted_categories['low_diff'] = []\n\n    app_atoms = self.get_appearing_atoms()\n    dis_atoms = self.get_disappearing_atoms()\n\n    # fixme: maybe use a threshold rather than a number of cases?\n    for pair in pairs[:best_cases_num]:\n        # ignore hydrogens on their own\n        # if pair[0].element == 'H':\n        #     continue\n\n        neighbours = [p for p, bonds in self.matched_pairs_bonds[pair]]\n\n        # ignore hydrogens in these connections (non-consequential)\n        hydrogens = [(a, b) for a, b in neighbours if a.element == 'H']\n        heavy = [(a, b) for a, b in neighbours if a.element != 'H']\n\n        # attach the hydrogens to be removed as well\n        to_remove = [pair] + hydrogens\n\n        sorted_categories['greedy'].append(to_remove)\n\n        # check if the current pair is linked to the alchemical region\n        linked_to_alchemical = False\n        for bond in pair[0].bonds:\n            if bond.atom in dis_atoms:\n                linked_to_alchemical = True\n        for bond in pair[1].bonds:\n            if bond.atom in app_atoms:\n                linked_to_alchemical = True\n\n        if len(heavy) == 1 and linked_to_alchemical:\n            sorted_categories['terminal_alch_linked'].append(to_remove)\n        if len(heavy) == 1:\n            sorted_categories['terminal'].append(to_remove)\n        if linked_to_alchemical:\n            sorted_categories['alch_linked'].append(to_remove)\n        if len(heavy) != 1 and not linked_to_alchemical:\n            sorted_categories['leftovers'].append(to_remove)\n\n    # carry out for the pairs that have a smaller Q diff\n    for pair in pairs[best_cases_num:]:\n        neighbours = [p for p, bonds in self.matched_pairs_bonds[pair]]\n        # consider the attached hydrogens\n        hydrogens = [(a, b) for a, b in neighbours if a.element == 'H']\n        # attach the hydrogens to be removed as well\n        to_remove = [pair] + hydrogens\n\n        sorted_categories['low_diff'].append(to_remove)\n\n    return sorted_categories\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.remove_attached_hydrogens","title":"remove_attached_hydrogens","text":"
    remove_attached_hydrogens(node_pair)\n

    The node_pair to which these hydrogens are attached was removed. Remove the dangling hydrogens.

    Check if these hydrogen are matched/superimposed. If that is the case. Remove the pairs.

    Note that if the hydrogens are paired and attached to node_pairA, they have to be attached to node_pairB, as a rule of being a match.

    Source code in ties/topology_superimposer.py
    def remove_attached_hydrogens(self, node_pair):\n    \"\"\"\n    The node_pair to which these hydrogens are attached was removed.\n    Remove the dangling hydrogens.\n\n    Check if these hydrogen are matched/superimposed. If that is the case. Remove the pairs.\n\n    Note that if the hydrogens are paired and attached to node_pairA,\n    they have to be attached to node_pairB, as a rule of being a match.\n    \"\"\"\n\n    # skip if no hydrogens found\n    if node_pair not in self.matched_pairs_bonds:\n        logger.debug('No dangling hydrogens')\n        return []\n\n    attached_pairs = self.matched_pairs_bonds[node_pair]\n\n    removed_pairs = []\n    for pair, bond_types in list(attached_pairs):\n        # ignore non hydrogens\n        if not pair[0].element == 'H':\n            continue\n\n        self.remove_node_pair(pair)\n        logger.debug(f'Removed dangling hydrogen pair: {pair}')\n        removed_pairs.append(pair)\n    return removed_pairs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.find_lowest_rmsd_mirror","title":"find_lowest_rmsd_mirror","text":"
    find_lowest_rmsd_mirror()\n

    Walk through the different mirrors and out of all options select the one that has the lowest RMSD. This way we increase the chance of getting a better match. However, long term it will be necessary to use the dihedrals to ensure that we match the atoms better.

    Source code in ties/topology_superimposer.py
    def find_lowest_rmsd_mirror(self):\n    \"\"\"\n    Walk through the different mirrors and out of all options select the one\n    that has the lowest RMSD. This way we increase the chance of getting a better match.\n    However, long term it will be necessary to use the dihedrals to ensure that we match\n    the atoms better.\n    \"\"\"\n    # fixme - you have to also take into account the \"weird / other symmetries\" besides mirrors\n    winner = self\n    lowest_rmsd = self.rmsd()\n    for mirror in self.mirrors:\n        mirror_rmsd = mirror.rmsd()\n        if mirror_rmsd < lowest_rmsd:\n            lowest_rmsd = mirror_rmsd\n            winner = mirror\n\n    if self is winner:\n        # False here means that it is not a mirror\n        return lowest_rmsd, self, False\n    else:\n        return lowest_rmsd, winner, True\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.is_subgraph_of_global_top","title":"is_subgraph_of_global_top","text":"
    is_subgraph_of_global_top()\n

    Check if after superimposition, one graph is a subgraph of another :return:

    Source code in ties/topology_superimposer.py
    def is_subgraph_of_global_top(self):\n    \"\"\"\n    Check if after superimposition, one graph is a subgraph of another\n    :return:\n    \"\"\"\n    # check if one topology is a subgraph of another topology\n    if len(self.matched_pairs) == len(self.top1) or len(self.matched_pairs) == len(self.top2):\n        return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.rmsd","title":"rmsd","text":"
    rmsd()\n

    For each pair take the distance, and then get rmsd, so root(mean(square(deviation)))

    Source code in ties/topology_superimposer.py
    def rmsd(self):\n    \"\"\"\n    For each pair take the distance, and then get rmsd, so root(mean(square(deviation)))\n    \"\"\"\n\n    assert len(self.matched_pairs) > 0\n\n    dsts = []\n    for atomA, atomB in self.matched_pairs:\n        dst = np.sqrt(np.sum(np.square((atomA.position - atomB.position))))\n        dsts.append(dst)\n    return np.sqrt(np.mean(np.square(dsts)))\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.link_pairs","title":"link_pairs","text":"
    link_pairs(from_pair, pairs)\n

    This helps take care of the bonds.

    Source code in ties/topology_superimposer.py
    def link_pairs(self, from_pair, pairs):\n    \"\"\"\n    This helps take care of the bonds.\n    \"\"\"\n    assert from_pair in self.matched_pairs_bonds\n    for pair, bond_types in pairs:\n        # the parent pair should have its list of pairs\n        assert pair in self.matched_pairs_bonds, f'not found pair {pair}'\n\n        # link X-Y\n        self.matched_pairs_bonds[from_pair].add((pair, bond_types))\n        # link Y-X\n        self.matched_pairs_bonds[pair].add((from_pair, bond_types))\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.find_mirror_choices","title":"find_mirror_choices","text":"
    find_mirror_choices()\n

    For each pair (A1, B1) find all the other options in the mirrors where (A1, B2)

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.find_mirror_choices--ie-ignore-x-b1-search-if-we-repair-from-a-to-b-then-b-to-a-should-be-repaired-too","title":"ie Ignore (X, B1) search, if we repair from A to B, then B to A should be repaired too","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.find_mirror_choices--fixme-is-this-still-necessary-if-we-are-traversing-all-paths","title":"fixme - is this still necessary if we are traversing all paths?","text":"Source code in ties/topology_superimposer.py
    def find_mirror_choices(self):\n    \"\"\"\n    For each pair (A1, B1) find all the other options in the mirrors where (A1, B2)\n    # ie Ignore (X, B1) search, if we repair from A to B, then B to A should be repaired too\n\n    # fixme - is this still necessary if we are traversing all paths?\n    \"\"\"\n    choices = {}\n    for A1, B1 in self.matched_pairs:\n        options_for_a1 = []\n        for mirror in self.mirrors:\n            for A2, B2 in mirror.matched_pairs:\n                if A1 is A2 and B1 is not B2:\n                    options_for_a1.append(B2)\n\n        if options_for_a1:\n            options_for_a1.insert(0, B1)\n            choices[A1] = options_for_a1\n\n    return choices\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.add_alternative_mapping","title":"add_alternative_mapping","text":"
    add_alternative_mapping(weird_symmetry)\n

    This means that there is another way to traverse and overlap the two molecules, but that the self is better (e.g. lower rmsd) than the other one

    Source code in ties/topology_superimposer.py
    def add_alternative_mapping(self, weird_symmetry):\n    \"\"\"\n    This means that there is another way to traverse and overlap the two molecules,\n    but that the self is better (e.g. lower rmsd) than the other one\n    \"\"\"\n    self.alternative_mappings.append(weird_symmetry)\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.correct_for_coordinates","title":"correct_for_coordinates","text":"
    correct_for_coordinates()\n

    Use the coordinates of the atoms, to figure out which symmetries are the correct ones. Rearrange so that the overall topology represents the one that has appropriate coordinates, whereas all the mirrors represent the other poor matches.

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.correct_for_coordinates--fixme-ensure-that-each-node-is-used-only-once-at-the-end","title":"fixme - ensure that each node is used only once at the end","text":"Source code in ties/topology_superimposer.py
    def correct_for_coordinates(self):\n    \"\"\"\n    Use the coordinates of the atoms, to figure out which symmetries are the correct ones.\n    Rearrange so that the overall topology represents the one that has appropriate coordinates,\n    whereas all the mirrors represent the other poor matches.\n\n    # fixme - ensure that each node is used only once at the end\n    \"\"\"\n\n    # check if you have coordinates\n    # fixme - rn we have it, check\n\n    # superimpose the coordinates, ensure a good match\n    # fixme - this was done before, so let's leave this way for now\n\n    # fixme - consider putting this conf as a mirror, and then modifying this\n\n    # check which are preferable for each of the mirrors\n    # we have to match mirrors to each other, ie say we have (O1=O3) and (O2=O4)\n    # we should find the mirror matching (O1=O4) and (O2=O3)\n    # so note that we have a closure here: All 4 atoms are used in both cases, and each time are paired differently.\n    # So this is how we defined the mirror - and therefore we can reduce this issue to the minimal mirrors.\n    # fixme - is this a cycle? O1-O3-O2-O4-O1\n    # Let's try to define a chain: O1 =O3, and O1 =O4, and O2 is =O3 or =O4\n    # So we have to define how to find O1 matching to different parts, and then decide\n    choices_mapping = self.find_mirror_choices()\n\n    # fixme - rewrite this method to eliminate one by one the hydrogens that fit in perfectly,\n    # some of them will have a plural significant match, while others might be hazy,\n    # so we have to eliminate them one by one, searching the best matches and then eliminating them\n\n    removed_nodes = set()\n    for A1, choices in choices_mapping.items():\n        # remove the old tuple\n        # fixme - not sure if this is the right way to go,\n        # but we break all the rules when applying this simplistic strategy\n        self.remove_node_pair((A1, choices[0]))\n        removed_nodes.add(A1)\n        removed_nodes.add(choices[0])\n\n    shortest_dsts = []\n\n    added_nodes = set()\n\n    # better matches\n    # for each atom that mismatches, scan all molecules and find the best match and eliminate it\n    blacklisted_bxs = []\n    for _ in range(len(choices_mapping)):\n        # fixme - optimisation of this could be such that if they two atoms are within 0.2A or something\n        # then they are straight away fixed\n        closest_dst = 9999999\n        closest_a1 = None\n        closest_bx = None\n        for A1, choices in choices_mapping.items():\n            # so we have several choices for A1, and now naively we are taking the one that is closest, and\n            # assuming the superimposition is easy, this would work\n\n            # FIXME - you cannot use simply distances, if for A1 and A2 the best is BX, then BX there should be\n            # rules for that\n            for BX in choices:\n                if BX in blacklisted_bxs:\n                    continue\n                # use the distance_array because of PBC correction and speed\n                a1_bx_dst = np.sqrt(np.sum(np.square(A1.position-BX.position)))\n                if a1_bx_dst < closest_dst:\n                    closest_dst = a1_bx_dst\n                    closest_bx = BX\n                    closest_a1 = A1\n\n        # across all the possible choices, found the best match now:\n        blacklisted_bxs.append(closest_bx)\n        shortest_dsts.append(closest_dst)\n        logger.debug(f'{closest_a1.name} is matching best with {closest_bx.name}')\n\n        # remove the old tuple and insert the new one\n        self.add_node_pair((closest_a1, closest_bx))\n        added_nodes.add(closest_a1)\n        added_nodes.add(closest_bx)\n        # remove from consideration\n        del choices_mapping[closest_a1]\n        # blacklist\n\n    # fixme - check that the added and the removed nodes are the same set\n    assert removed_nodes == added_nodes\n\n    # this is the corrected region score (there might not be any)\n    if len(shortest_dsts) != 0:\n        avg_dst = np.mean(shortest_dsts)\n    else:\n        # fixme\n        avg_dst = 0\n\n    return avg_dst\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.enforce_no_partial_rings","title":"enforce_no_partial_rings","text":"
    enforce_no_partial_rings()\n

    http://www.alchemistry.org/wiki/Constructing_a_Pathway_of_Intermediate_States It is the opening or closing of the rings that is an issue. This means that if any atom on a ring disappears, it breaks the ring, and therefore the entire ring should be removed and appeared again.

    If any atom is removed, it should check if it affects other rings, therefore cascading removing further rings.

    Source code in ties/topology_superimposer.py
    def enforce_no_partial_rings(self):\n    \"\"\"\n    http://www.alchemistry.org/wiki/Constructing_a_Pathway_of_Intermediate_States\n    It is the opening or closing of the rings that is an issue.\n    This means that if any atom on a ring disappears, it breaks the ring,\n    and therefore the entire ring should be removed and appeared again.\n\n    If any atom is removed, it should check if it affects other rings,\n    therefore cascading removing further rings.\n    \"\"\"\n    MAX_CIRCLE_SIZE = 7\n\n    # get circles in the original ligands\n    l_circles, r_circles = self.get_original_circles()\n    l_matched_circles, r_matched_circles = self.get_circles()\n\n    # right now we are filtering out circles that are larger than 7 atoms,\n    l_circles = list(filter(lambda c: len(c) <= MAX_CIRCLE_SIZE, l_circles))\n    r_circles = list(filter(lambda c: len(c) <= MAX_CIRCLE_SIZE, r_circles))\n    l_matched_circles = list(filter(lambda c: len(c) <= MAX_CIRCLE_SIZE, l_matched_circles))\n    r_matched_circles = list(filter(lambda c: len(c) <= MAX_CIRCLE_SIZE, r_matched_circles))\n\n    # first, see which matched circles eliminate themselves (simply matched circles)\n    correct_circles = []\n    for l_matched_circle in l_matched_circles[::-1]:\n        for r_matched_circle in r_matched_circles[::-1]:\n            if self.are_matched_sets(l_matched_circle, r_matched_circle):\n                # These two circles fully overlap, so they are fine\n                l_matched_circles.remove(l_matched_circle)\n                r_matched_circles.remove(r_matched_circle)\n                # update the original circles\n                l_circles.remove(l_matched_circle)\n                r_circles.remove(r_matched_circle)\n                correct_circles.append((l_matched_circle, r_matched_circle))\n\n    # at this point, we should not have any matched circles, in either R and L\n    # this is because we do not allow one ligand to have a matched circle, while another ligand not\n    assert len(l_matched_circles) == len(r_matched_circles) == 0\n\n    while True:\n        # so now we have to work with the original rings which have not been overlapped,\n        # these most likely means that there are mutations preventing it from overlapping\n        l_removed_pairs = self._remove_unmatched_ring_atoms(l_circles)\n        r_removed_pairs = self._remove_unmatched_ring_atoms(r_circles)\n\n        for l_circle, r_circle in correct_circles:\n            # checked if any removed atom affected any of the correct circles\n            affected_l_circle = any(l_atom in l_circle for l_atom, r_atom in l_removed_pairs)\n            affected_r_circle = any(r_atom in r_circle for l_atom, r_atom in r_removed_pairs)\n            # add the circle to be disassembled\n            if affected_l_circle or affected_r_circle:\n                l_circles.append(l_circle)\n                r_circles.append(r_circle)\n\n        if len(l_removed_pairs) == len(r_removed_pairs) == 0:\n            break\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._remove_unmatched_ring_atoms","title":"_remove_unmatched_ring_atoms","text":"
    _remove_unmatched_ring_atoms(circles)\n

    A helper function. Removes pairs with the given atoms.

    The removed atoms are classified as unmatched_rings.

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._remove_unmatched_ring_atoms--parameters","title":"Parameters","text":"

    circles : list A list of iterables. Each atom in a circle, if matched, is removed together with the corresponding atom from the suptop. The user should ensure that the rings/circles are partial

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._remove_unmatched_ring_atoms--returns","title":"Returns","text":"

    removed : bool True if any atom was removed. False otherwise.

    Source code in ties/topology_superimposer.py
    def _remove_unmatched_ring_atoms(self, circles):\n    \"\"\"\n    A helper function. Removes pairs with the given atoms.\n\n    The removed atoms are classified as unmatched_rings.\n\n    Parameters\n    ----------\n    circles : list\n        A list of iterables. Each atom in a circle, if matched, is removed together with\n        the corresponding atom from the suptop.\n        The user should ensure that the rings/circles are partial\n\n    Returns\n    -------\n    removed : bool\n        True if any atom was removed. False otherwise.\n    \"\"\"\n    removed_pairs = []\n    for circle in circles:\n        for unmatched_ring_atom in circle:\n            # find if the ring has a match\n            if self.contains_node(unmatched_ring_atom):\n                # remove the pair from matched\n                pair = self.get_pair_with_atom(unmatched_ring_atom)\n                self.remove_node_pair(pair)\n                self._removed_because_unmatched_rings.append(pair)\n                removed_pairs.append(pair)\n    return removed_pairs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_topology_similarity_score","title":"get_topology_similarity_score","text":"
    get_topology_similarity_score()\n

    Having the superimposed A(Left) and B(Right), score the match. This is a rather naive approach. It compares A-B match by checking if any of the node X and X' in A and B have a bond to another node Y that is not present in A-B, but that is directly reachable from X and X' in a similar way. We ignore the charge of Y and focus here only on the topology.

    For every \"external bond\" from the component we try to see if topologically it scores well. So for any matched pair, we extend the topology and the score is equal to the size of such an component. Then we do this for all other matching nodes and sum the score.

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_topology_similarity_score--fixme-maybe-you-should-use-the-entire-graphs-in-order-to-see-if-this-is-good-or-not","title":"fixme - maybe you should use the entire graphs in order to see if this is good or not?","text":"

    so the simpler approach is to ignore charges for a second to only understand the relative place in the topology, in other words, the question is, how similar are two nodes A and B vs A and C? let's traverse A and B together, and then A and C together, and while doing that, ignore the charges. In this case, A and B could get together 20 parts, whereas A and C traverses together 22 parts, meaning that topologically, it is a more suitable one, because it closer corresponds to the actual atom. Note that this approach has problem: - you can imagine A and B traversing where B is in a completely wrong global place, but it happens to have a bigger part common to A, than C which globally is correct. Answer to this: at the same time, ideally B would be excluded, because it should have been already matched to another topology.

    Alternative approach: take into consideration other components and the distance from this component to them. Specifically, allows mismatches

    FIXME - allow flexible mismatches. Meaning if someone mutates one bonded atom, then it might be noticed that

    Source code in ties/topology_superimposer.py
    def get_topology_similarity_score(self):\n    \"\"\"\n    Having the superimposed A(Left) and B(Right), score the match.\n    This is a rather naive approach. It compares A-B match by checking\n    if any of the node X and X' in A and B have a bond to another node Y that is\n    not present in A-B, but that is directly reachable from X and X' in a similar way.\n    We ignore the charge of Y and focus here only on the topology.\n\n    For every \"external bond\" from the component we try to see if topologically it scores well.\n    So for any matched pair, we extend the topology and the score is equal to the size of\n    such an component. Then we do this for all other matching nodes and sum the score.\n\n    # fixme - maybe you should use the entire graphs in order to see if this is good or not?\n    so the simpler approach is to ignore charges for a second to only understand the relative place in the topology,\n    in other words, the question is, how similar are two nodes A and B vs A and C? let's traverse A and B together,\n    and then A and C together, and while doing that, ignore the charges. In this case, A and B could\n    get together 20 parts, whereas A and C traverses together 22 parts, meaning that topologically,\n    it is a more suitable one, because it closer corresponds to the actual atom.\n    Note that this approach has problem:\n    - you can imagine A and B traversing where B is in a completely wrong global place, but it\n    happens to have a bigger part common to A, than C which globally is correct. Answer to this:\n    at the same time, ideally B would be excluded, because it should have been already matched to another\n    topology.\n\n    Alternative approach: take into consideration other components and the distance from this component\n    to them. Specifically, allows mismatches\n\n    FIXME - allow flexible mismatches. Meaning if someone mutates one bonded atom, then it might be noticed\n    that\n    \"\"\"\n    overall_score = 0\n    for node_a, node_b in self.matched_pairs:\n        # for every neighbour in Left\n        for a_bond in node_a.bonds:\n            # if this bonded atom is present in this superimposed topology (or component), ignore\n            # fixme - surely this can be done better, you could have \"contains this atom or something\"\n            in_this_sup_top = False\n            for other_a, _ in self.matched_pairs:\n                if a_bond.atom == other_a:\n                    in_this_sup_top = True\n                    break\n            if in_this_sup_top:\n                continue\n\n            # a candidate is found that could make the node_a and node_b more similar,\n            # so check if it is also present in node_b,\n            # ignore the charges to focus only on the topology and put aside the parameterisation\n            for b_bond in node_b.bonds:\n                # fixme - what if the atom is mutated into a different atom? we have to be able\n                # to relies on other measures than just this one, here the situation is that the topology\n                # is enough to answer the question (because only charges were modified),\n                # however, this gets more tricky\n                # fixme - hardcoded\n                score = len(_overlay(a_bond.atom, b_bond.atom))\n\n                # this is a purely topology based score, the bigger the overlap the better the match\n                overall_score += score\n\n            # check if the neighbour points to any node X that is not used in Left,\n\n            # if node_b leads to the same node X\n    return overall_score\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.unmatch_pairs_with_different_charges","title":"unmatch_pairs_with_different_charges","text":"
    unmatch_pairs_with_different_charges(atol)\n

    Removes the matched pairs where atom charges are more different than the provided absolute tolerance atol (units in Electrons).

    remove_dangling_h: After removing any pair it also removes any bound hydrogen(s).

    Source code in ties/topology_superimposer.py
    def unmatch_pairs_with_different_charges(self, atol):\n    \"\"\"\n    Removes the matched pairs where atom charges are more different\n    than the provided absolute tolerance atol (units in Electrons).\n\n    remove_dangling_h: After removing any pair it also removes any bound hydrogen(s).\n    \"\"\"\n    removed_hydrogen_pairs = []\n    for node1, node2 in self.matched_pairs[::-1]:\n        if node1.united_eq(node2, atol=atol) or (node1, node2) in removed_hydrogen_pairs:\n            continue\n\n        # remove this pair\n        # use full logging for this kind of information\n        # print('Q: removing nodes', (node1, node2)) # to do - consider making this into a logging feature\n        self.remove_node_pair((node1, node2))\n\n        # keep track of the removed atoms due to the charge\n        self._removed_pairs_with_charge_difference.append(\n            ((node1, node2), math.fabs(node2.united_charge - node1.united_charge)))\n\n        # Removed functionality: remove the dangling hydrogens\n        removed_h_pairs = self.remove_attached_hydrogens((node1, node2))\n        removed_hydrogen_pairs.extend(removed_h_pairs)\n        for h_pair in removed_h_pairs:\n            self._removed_pairs_with_charge_difference.append(\n                (h_pair, 'dangling'))\n\n    # sort the removed in a descending order\n    self._removed_pairs_with_charge_difference.sort(key=lambda x: x[1], reverse=True)\n\n    return self._removed_pairs_with_charge_difference\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.is_consistent_with","title":"is_consistent_with","text":"
    is_consistent_with(suptop)\n
    Conditions
    • There should be a minimal overlap of at least 1 node.
    • There is no pair (Na=Nb) in this sup top such that (Na=Nc) or (Nb=Nc) for some Nc in the other suptop.
    • The number of cycles in this suptop and the other suptop must be the same (?removing for now, fixme)
    • merging cannot lead to new cycles?? (fixme). What is the reasoning behind this? I mean, I guess the assumption is that, if the cycles were compatible, they would be created during the search, rather than now while merging. ??
    Source code in ties/topology_superimposer.py
    def is_consistent_with(self, suptop):\n    \"\"\"\n    Conditions:\n        - There should be a minimal overlap of at least 1 node.\n        - There is no pair (Na=Nb) in this sup top such that (Na=Nc) or (Nb=Nc) for some Nc in the other suptop.\n        - The number of cycles in this suptop and the other suptop must be the same (?removing for now, fixme)\n        - merging cannot lead to new cycles?? (fixme). What is the reasoning behind this?\n            I mean, I guess the assumption is that, if the cycles were compatible,\n            they would be created during the search, rather than now while merging. ??\n    \"\"\"\n\n    # confirm that there is no mismatches, ie (A=B) in suptop1 and (A=C) in suptop2 where (C!=B)\n    for st1Na, st1Nb in self.matched_pairs:\n        for st2Na, st2Nb in suptop.matched_pairs:\n            if (st1Na is st2Na) and not (st1Nb is st2Nb) or (st1Nb is st2Nb) and not (st1Na is st2Na):\n                return False\n\n    # ensure there is at least one common pair\n    if self.count_common_node_pairs(suptop) == 0:\n        return False\n\n    # why do we need this?\n    # if not self.is_consistent_cycles(suptop):\n    #     return False\n\n    return True\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._rename_ligand","title":"_rename_ligand staticmethod","text":"
    _rename_ligand(atoms, name_counter=None)\n

    name_counter: a dictionary with atom as the key such as 'N', 'C', etc, the counter keeps track of the last used counter for each name. Empty means that the counting will start from 1.

    Source code in ties/topology_superimposer.py
    @staticmethod\ndef _rename_ligand(atoms, name_counter=None):\n    \"\"\"\n    name_counter: a dictionary with atom as the key such as 'N', 'C', etc,\n    the counter keeps track of the last used counter for each name.\n    Empty means that the counting will start from 1.\n    \"\"\"\n    if name_counter is None:\n        name_counter = {}\n\n    for atom in atoms:\n        # get the first letters that is not a character\n        after_letters = [i for i, l in enumerate(atom.name) if l.isalpha()][-1] + 1\n\n        atom_name = atom.name[:after_letters]\n        last_used_counter = name_counter.get(atom_name, 0)\n\n        # rename\n        last_used_counter += 1\n        new_atom_name = atom_name + str(last_used_counter)\n        logger.info(f'Renaming {atom.name} to {new_atom_name}')\n        atom.name = new_atom_name\n\n        # update the counter\n        name_counter[atom_name] = last_used_counter\n\n    return name_counter\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._get_atom_names_counter","title":"_get_atom_names_counter staticmethod","text":"
    _get_atom_names_counter(atoms)\n

    name_counter: a dictionary with atom as the key such as 'N', 'C', etc, the counter keeps track of the last used counter for each name. Ie if there are C1, C2, C3, this will return {'C':3} as the last counter.

    Source code in ties/topology_superimposer.py
    @staticmethod\ndef _get_atom_names_counter(atoms):\n    \"\"\"\n    name_counter: a dictionary with atom as the key such as 'N', 'C', etc,\n    the counter keeps track of the last used counter for each name.\n    Ie if there are C1, C2, C3, this will return {'C':3} as the last counter.\n    \"\"\"\n    name_counter = {}\n\n    for atom in atoms:\n        # get the first letters that is not a character\n        after_letters = [i for i, l in enumerate(atom.name) if l.isalpha()][-1] + 1\n\n        atom_name = atom.name[:after_letters]\n        atom_number = int(atom.name[after_letters:])\n        last_used_counter = name_counter.get(atom_name, 0)\n\n        # update the counter\n        name_counter[atom_name] = max(last_used_counter, atom_number)\n\n    return name_counter\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_circles","title":"get_circles","text":"
    get_circles()\n

    Return circles found in the matched pairs.

    Source code in ties/topology_superimposer.py
    def get_circles(self):\n    \"\"\"\n    Return circles found in the matched pairs.\n    \"\"\"\n    gl, gr = self.get_nx_graphs()\n    gl_circles = [set(circle) for circle in nx.cycle_basis(gl)]\n    gr_circles = [set(circle) for circle in nx.cycle_basis(gr)]\n    return gl_circles, gr_circles\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_original_circles","title":"get_original_circles","text":"
    get_original_circles()\n

    Return the original circles present in the input topologies.

    Source code in ties/topology_superimposer.py
    def get_original_circles(self):\n    \"\"\"\n    Return the original circles present in the input topologies.\n    \"\"\"\n    # create a circles\n    l_original = self._get_original_circle(self.top1)\n    r_original = self._get_original_circle(self.top2)\n\n    l_circles = [set(circle) for circle in nx.cycle_basis(l_original)]\n    r_circles = [set(circle) for circle in nx.cycle_basis(r_original)]\n    return l_circles, r_circles\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._get_original_circle","title":"_get_original_circle","text":"
    _get_original_circle(atom_list)\n

    Create a networkx circle out of the list atom_list - list of AtomNode

    Source code in ties/topology_superimposer.py
    def _get_original_circle(self, atom_list):\n    \"\"\"Create a networkx circle out of the list\n    atom_list - list of AtomNode\n    \"\"\"\n    g = nx.Graph()\n    # add each node\n    for atom in atom_list:\n        g.add_node(atom)\n\n    # add all the edges\n    for atom in atom_list:\n        # add the edges from nA\n        for other in atom_list:\n            if atom.bound_to(other):\n                g.add_edge(atom, other)\n\n    return g\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.cycle_spans_multiple_cycles","title":"cycle_spans_multiple_cycles","text":"
    cycle_spans_multiple_cycles()\n

    What is the circle is shared? We are using cycles which excluded atoms that join different rings. fixme - could this lead to a special case?

    Source code in ties/topology_superimposer.py
    def cycle_spans_multiple_cycles(self):\n    # This filter checks whether a newly created suptop cycle spans multiple cycles\n    # this is one of the filters (#106)\n    # fixme - should this be applied whenever we work with more than 1 cycle?\n    # it checks whether any cycles in the left molecule,\n    # is paired with more than one cycle in the right molecule\n    \"\"\"\n    What is the circle is shared?\n    We are using cycles which excluded atoms that join different rings.\n    fixme - could this lead to a special case?\n    \"\"\"\n\n    for l_cycle in self._nonoverlapping_l_cycles:\n        overlap_counter = 0\n        for r_cycle in self._nonoverlapping_r_cycles:\n            # check if the cycles overlap\n            if self._cycles_overlap(l_cycle, r_cycle):\n                overlap_counter += 1\n\n        if overlap_counter > 1:\n            return True\n\n    for r_cycle in self._nonoverlapping_r_cycles:\n        overlap_counter = 0\n        for l_cycle in self._nonoverlapping_l_cycles:\n            # check if the cycles overlap\n            if self._cycles_overlap(l_cycle, r_cycle):\n                overlap_counter += 1\n\n        if overlap_counter > 1:\n            return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.merge","title":"merge","text":"
    merge(suptop)\n

    Absorb the other suptop by adding all the node pairs that are not present in the current sup top.

    WARNING: ensure that the other suptop is consistent with this sup top.

    Source code in ties/topology_superimposer.py
    def merge(self, suptop):\n    \"\"\"\n    Absorb the other suptop by adding all the node pairs that are not present\n    in the current sup top.\n\n    WARNING: ensure that the other suptop is consistent with this sup top.\n    \"\"\"\n    # assert self.is_consistent_with(suptop)\n\n    # print(\"About the merge two sup tops\")\n    # self.print_summary()\n    # other_suptop.print_summary()\n\n    merged_pairs = []\n    for pair in suptop.matched_pairs:\n        # check if this pair is present\n        if not self.contains(pair):\n            n1, n2 = pair\n            if self.contains_node(n1) or self.contains_node(n2):\n                raise Exception('already uses that node')\n            # pass the bonded pairs here\n            self.add_node_pair(pair)\n            merged_pairs.append(pair)\n    # after adding all the nodes, now add the bonds\n    for pair in merged_pairs:\n        # add the connections\n        bonded_pairs = suptop.matched_pairs_bonds[pair]\n        assert len(bonded_pairs) > 0\n        self.link_pairs(pair, bonded_pairs)\n\n    # removed from the \"merged\" the ones that agree, so it contains only the new stuff\n    # to make it easier to read\n    self.nodes_added_log.append((\"merged with\", merged_pairs))\n\n    # check for duplication, fixme - temporary\n    return merged_pairs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.validate_charges","title":"validate_charges staticmethod","text":"
    validate_charges(atom_list_l, atom_list_right)\n

    Check the original charges: - ensure that the total charge of L and R are integers - ensure that they are equal to the same integer

    Source code in ties/topology_superimposer.py
    @staticmethod\ndef validate_charges(atom_list_l, atom_list_right):\n    \"\"\"\n    Check the original charges:\n    - ensure that the total charge of L and R are integers\n    - ensure that they are equal to the same integer\n    \"\"\"\n    whole_left_charge = sum(a.charge for a in atom_list_l)\n    np.testing.assert_almost_equal(whole_left_charge, round(whole_left_charge), decimal=2,\n                                   err_msg=f'left charges are not integral. Expected {round(whole_left_charge)}'\n                                           f' but found {whole_left_charge}')\n\n    whole_right_charge = sum(a.charge for a in atom_list_right)\n    np.testing.assert_almost_equal(whole_right_charge, round(whole_right_charge), decimal=2,\n                                   err_msg=f'right charges are not integral. Expected {round(whole_right_charge)}'\n                                           f' but found {whole_right_charge}'\n                                   )\n    # same integer\n    np.testing.assert_almost_equal(whole_left_charge, whole_right_charge, decimal=2)\n\n    return round(whole_left_charge)\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.redistribute_charges","title":"redistribute_charges","text":"
    redistribute_charges()\n

    After the match is made and the user commits to the superimposed topology, the charges can be revised. We calculate the average charges between every match, and check how that affects the rest of the molecule (the unmatched atoms). Then, we distribute the charges to the unmatched atoms to get the net charge as a whole number/integer.

    This function should be called after removing the matches for whatever reason. ie at the end of anything that could modify the atom pairing.

    Source code in ties/topology_superimposer.py
    def redistribute_charges(self):\n    \"\"\"\n    After the match is made and the user commits to the superimposed topology,\n    the charges can be revised.\n    We calculate the average charges between every match, and check how that affects\n    the rest of the molecule (the unmatched atoms).\n    Then, we distribute the charges to the unmatched atoms to get\n    the net charge as a whole number/integer.\n\n    This function should be called after removing the matches for whatever reason.\n    ie at the end of anything that could modify the atom pairing.\n    \"\"\"\n\n    SuperimposedTopology.validate_charges(self.top1, self.top2)\n\n    # find the integral net charge of the molecule\n    net_charge = round(sum(a.charge for a in self.top1))\n    net_charge_test = round(sum(a.charge for a in self.top2))\n    if net_charge != net_charge_test:\n        raise Exception('The internally computed net charges of the molecules are different')\n    # fixme - use the one passed by the user?\n    logger.info(f'Internally computed net charge: {net_charge}')\n\n    # the total charge in the matched region before the changes\n    matched_total_charge_l = sum(left.charge for left, right in self.matched_pairs)\n    matched_total_charge_r = sum(right.charge for left, right in self.matched_pairs)\n\n    # get the unmatched atoms in Left and Right\n    l_unmatched = self.get_disappearing_atoms()\n    r_unmatched = self.get_appearing_atoms()\n\n    init_q_dis = sum(a.charge for a in l_unmatched)\n    init_q_app = sum(a.charge for a in r_unmatched)\n    logger.debug(f'Initial cumulative charge of the appearing={init_q_app:.6f}, disappearing={init_q_dis:.6f} '\n          f'alchemical regions')\n\n    # average the charges between matched atoms in the joint area of the dual topology\n    total_charge_matched = 0    # represents the net charge of the joint area minus molecule charge\n    for left, right in self.matched_pairs:\n        avg_charge = (left.charge + right.charge) / 2.0\n        # write the new charge\n        left.charge = right.charge = avg_charge\n        total_charge_matched += avg_charge\n    # total_partial_charge_matched e.g. -0.9 (partial charges) - -1 (net molecule charge) = 0.1\n    total_partial_charge_matched = total_charge_matched - net_charge\n    logger.debug(f'Total partial charge in the joint area = {total_partial_charge_matched:.6f}')\n\n    # calculate what the correction should be in the alchemical regions\n    r_delta_charge_total = - (total_partial_charge_matched + init_q_app)\n    l_delta_charge_total = - (total_partial_charge_matched + init_q_dis)\n    logger.debug(f'Total charge imbalance to be distributed in '\n          f'dis={l_delta_charge_total:.6f} and app={r_delta_charge_total:.6f}')\n\n    if len(l_unmatched) == 0 and l_delta_charge_total != 0:\n        logger.error('----------------------------------------------------------------------------------------------')\n        logger.error('ERROR? AFTER AVERAGING CHARGES, THERE ARE NO UNMATCHED ATOMS TO ASSIGN THE CHARGE TO: '\n              'left ligand.')\n        logger.error('----------------------------------------------------------------------------------------------')\n    if len(r_unmatched) == 0 and r_delta_charge_total != 0:\n        logger.error('----------------------------------------------------------------------------------------------')\n        logger.error('ERROR? AFTER AVERAGING CHARGES, THERE ARE NO UNMATCHED ATOMS TO ASSIGN THE CHARGE TO: '\n              'right ligand. ')\n        logger.error('----------------------------------------------------------------------------------------------')\n\n    # distribute the charges over the alchemical regions\n    if len(l_unmatched) != 0:\n        l_delta_per_atom = float(l_delta_charge_total) / len(l_unmatched)\n    else:\n        # fixme - no unmatching atoms, so there should be no charge to redistribute\n        l_delta_per_atom = 0\n\n    if len(r_unmatched) != 0:\n        r_delta_per_atom = float(r_delta_charge_total) / len(r_unmatched)\n    else:\n        r_delta_per_atom = 0\n        # fixme - no matching atoms, so there should be no charge to redistribute\n    logger.debug(f'Charge imbalance per atom in dis={l_delta_per_atom:.6f} and app={r_delta_per_atom:.6f}')\n\n    # redistribute that delta q over the atoms in the left and right molecule\n    for atom in l_unmatched:\n        atom.charge += l_delta_per_atom\n    for atom in r_unmatched:\n        atom.charge += r_delta_per_atom\n\n    # check if the appearing atoms and the disappearing atoms have the same net charge\n    dis_q_sum = sum(a.charge for a in l_unmatched)\n    app_q_sum = sum(a.charge for a in r_unmatched)\n    logger.debug(f'Final cumulative charge of the appearing={app_q_sum:.6f}, disappearing={dis_q_sum:.6f} '\n          f'alchemical regions')\n    if not np.isclose(dis_q_sum, app_q_sum):\n        logger.error('The partial charges in app/dis region are not equal to each other. ')\n        raise Exception('The alchemical region in app/dis do not have equal partial charges.')\n\n    # note that we are really modifying right now the original nodes.\n    SuperimposedTopology.validate_charges(self.top1, self.top2)\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.contains_same_atoms_symmetric","title":"contains_same_atoms_symmetric","text":"
    contains_same_atoms_symmetric(other_sup_top)\n

    The atoms can be paired differently, but they are the same.

    Source code in ties/topology_superimposer.py
    def contains_same_atoms_symmetric(self, other_sup_top):\n    \"\"\"\n    The atoms can be paired differently, but they are the same.\n    \"\"\"\n    if len(self.nodes.symmetric_difference(other_sup_top.nodes)) == 0:\n        return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.is_subgraph_of","title":"is_subgraph_of","text":"
    is_subgraph_of(other_sup_top)\n

    Checks if this superimposed topology is a subgraph of another superimposed topology. Or if any mirror topology is a subgraph.

    Source code in ties/topology_superimposer.py
    def is_subgraph_of(self, other_sup_top):\n    \"\"\"\n    Checks if this superimposed topology is a subgraph of another superimposed topology.\n    Or if any mirror topology is a subgraph.\n    \"\"\"\n    # subgraph cannot be equivalent self.eq, it is only proper subgraph (ie proper subset)\n    if len(self.matched_pairs) >= len(other_sup_top.matched_pairs):\n        return False\n\n    # self is smaller, so it might be a subgraph\n    if other_sup_top.contains_all(self):\n        return True\n\n    # self is not a subgraph, but it could be a subgraph of one of the mirrors\n    for mirror in self.mirrors:\n        if other_sup_top.contains_all(mirror):\n            return True\n\n    # other is bigger than self, but not a subgraph of self\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.subgraph_relationship","title":"subgraph_relationship","text":"
    subgraph_relationship(other_sup_top)\n

    Return 1 if self is a supergraph of other, -1 if self is a subgraph of other 0 if they have the same number of elements (regardless of what the nodes are)

    Source code in ties/topology_superimposer.py
    def subgraph_relationship(self, other_sup_top):\n    \"\"\"\n    Return\n    1 if self is a supergraph of other,\n    -1 if self is a subgraph of other\n    0 if they have the same number of elements (regardless of what the nodes are)\n    \"\"\"\n    if len(self.matched_pairs) == len(other_sup_top.matched_pairs):\n        return 0\n\n    if len(self.matched_pairs) > len(other_sup_top.matched_pairs):\n        # self is bigger than other,\n        # check if self contains all nodes in other\n        if self.contains_all(other_sup_top):\n            return 1\n\n        # other is not a subgraph, but check the mirrors if any of them are\n        for mirror in self.mirrors:\n            if mirror.contains_all(other_sup_top):\n                return 1\n\n        # other is smaller but not a subgraph of this graph or any of its mirrors\n        return 0\n\n    if len(self.matched_pairs) < len(other_sup_top.matched_pairs):\n        # other is bigger, so self might be a subgraph\n        # check if other contains all nodes in self\n        if other_sup_top.contains_all(self):\n            return -1\n\n        # self is not a subgraph, but it could be a subgraph of one of the mirrors\n        for mirror in self.mirrors:\n            if other_sup_top.contains_all(mirror):\n                return -1\n\n        # other is bigger than self, but it is not a subgraph\n        return 0\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.is_mirror_of","title":"is_mirror_of","text":"
    is_mirror_of(other_sup_top)\n

    this is a naive check fixme - check if the found superimposed topology is the same (ie the same matches), what then?

    some of the superimposed topologies represent symmetrical matches, for example, imagine T1A and T1B is a symmetrical version of T2A and T2B, this means that - the number of nodes in T1A, T1B, T2A, and T2B is the same - all the nodes in T1A are in T2A, - all the nodes in T1B are in T2B

    Source code in ties/topology_superimposer.py
    def is_mirror_of(self, other_sup_top):\n    \"\"\"\n    this is a naive check\n    fixme - check if the found superimposed topology is the same (ie the same matches), what then?\n\n    some of the superimposed topologies represent symmetrical matches,\n    for example, imagine T1A and T1B is a symmetrical version of T2A and T2B,\n    this means that\n     - the number of nodes in T1A, T1B, T2A, and T2B is the same\n     - all the nodes in T1A are in T2A,\n     - all the nodes in T1B are in T2B\n    \"\"\"\n\n    if len(self.matched_pairs) != len(other_sup_top.matched_pairs):\n        return False\n\n    if self.contains_same_atoms_symmetric(other_sup_top):\n        return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.eq","title":"eq","text":"
    eq(sup_top)\n

    Check if the superimposed topology is \"the same\". This means that every pair has a corresponding pair in the other topology (but possibly in a different order)

    Source code in ties/topology_superimposer.py
    def eq(self, sup_top):\n    \"\"\"\n    Check if the superimposed topology is \"the same\". This means that every pair has a corresponding pair in the\n    other topology (but possibly in a different order)\n    \"\"\"\n    # fixme - should replace this with networkx\n    if len(self) != len(sup_top):\n        return False\n\n    for pair in self.matched_pairs:\n        # find for every pair the matching pair\n        if not sup_top.contains(pair):\n            return False\n\n    return True\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.toJSON","title":"toJSON","text":"
    toJSON()\n

    \" Extract all the important information and return a json string.

    Source code in ties/topology_superimposer.py
    def toJSON(self):\n    \"\"\"\"\n        Extract all the important information and return a json string.\n    \"\"\"\n    summary = {\n        # metadata\n        # renamed atoms, new name : old name\n        'renamed_atoms': {\n            'start_ligand': {a.name: a.original_name for a in self.top1},\n            'end_ligand': {a.name: a.original_name for a in self.top2},\n        },\n        # the dual topology information\n        'superimposition': {\n            'matched': {str(n1): str(n2) for n1, n2 in self.matched_pairs},\n            'appearing': list(map(str, self.get_appearing_atoms())),\n            'disappearing': list(map(str, self.get_disappearing_atoms())),\n            'removed': { # because of:\n                # replace atoms with their names\n                'net_charge': [((a1.name, a2.name), d) for (a1, a2), d in self._removed_due_to_net_charge],\n                'pair_q': [((a1.name, a2.name), d) for (a1, a2), d in self._removed_pairs_with_charge_difference],\n                'disjointed': [(a1.name, a2.name) for a1, a2 in self._removed_because_disjointed_cc],\n                'bonds': [((a1.name, a2.name), d) for (a1, a2), d in self._removed_because_diff_bonds],\n                'unmatched_rings': [((a1.name, a2.name), d) for (a1, a2), d in self._removed_because_unmatched_rings],\n            },\n            'charges_delta': {\n                'start_ligand': {a.name: a.charge - a._original_charge for a in self.top1 if a._original_charge != a.charge},\n                'end_ligand': {a.name: a.charge - a._original_charge for a in self.top2 if a._original_charge != a.charge}\n            }\n        },\n        'config': self.config.get_serializable(),\n        'internal': 'atoms' # fixme\n    }\n    return summary\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.get_largest","title":"get_largest","text":"
    get_largest(lists)\n

    return a list of largest solutions

    Source code in ties/topology_superimposer.py
    def get_largest(lists):\n    \"\"\"\n    return a list of largest solutions\n    \"\"\"\n    solution_sizes = [len(st) for st in lists]\n    largest_sol_size = max(solution_sizes)\n    return list(filter(lambda st: len(st) == largest_sol_size, lists))\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.long_merge","title":"long_merge","text":"
    long_merge(suptop1, suptop2)\n

    Carry out a merge and apply all checks. Merge suptop2 into suptop1.

    Source code in ties/topology_superimposer.py
    def long_merge(suptop1, suptop2):\n    \"\"\"\n    Carry out a merge and apply all checks.\n    Merge suptop2 into suptop1.\n\n    \"\"\"\n    if suptop1 is suptop2:\n        return suptop1\n\n    if suptop1.eq(suptop2):\n        log(\"Merge: the two are the equal. Ignoring\")\n        return suptop1\n\n    if suptop2.is_subgraph_of(suptop1):\n        log(\"Merge: this is already a superset. Ignoring\")\n        return suptop1\n\n    # check if the two are consistent\n    # ie there is no clashes\n    if not suptop1.is_consistent_with(suptop2):\n        log(\"Merge: cannot merge - not consistent\")\n        return -1\n\n    # fixme - this can be removed because it is now taken care of in the other functions?\n    # g1, g2 = suptop1.getNxGraphs()\n    # assert len(nx.cycle_basis(g1)) == len(nx.cycle_basis(g2))\n    # g3, g4 = suptop2.getNxGraphs()\n    # assert len(nx.cycle_basis(g3)) == len(nx.cycle_basis(g4))\n    #\n    # assert suptop1.sameCircleNumber()\n    newly_added_pairs = suptop1.merge(suptop2)\n\n    # if not suptop1.sameCircleNumber():\n    #     raise Exception('something off')\n    # # remove sol2 from the solutions:\n    # all_solutions.remove(sol2)\n    return newly_added_pairs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.merge_compatible_suptops","title":"merge_compatible_suptops","text":"
    merge_compatible_suptops(suptops)\n

    Imagine mapping of two carbons C1 and C2 to another pair of carbons C1' and C2'. If C1 was mapped to C1', and C2 to C2', and each craeted a suptop, then we have to join the two suptops.

    fixme - appears to be doing too many combinations Consider using a queue. Add the new combinations here rather than restarting again and again. You could keep a list of \"combinations\" in a queue, and each time you make a new element,

    Source code in ties/topology_superimposer.py
    def merge_compatible_suptops(suptops):\n    \"\"\"\n    Imagine mapping of two carbons C1 and C2 to another pair of carbons C1' and C2'.\n    If C1 was mapped to C1', and C2 to C2', and each craeted a suptop, then we have to join the two suptops.\n\n    fixme - appears to be doing too many combinations\n    Consider using a queue. Add the new combinations here rather than restarting again and again.\n    You could keep a list of \"combinations\" in a queue, and each time you make a new element,\n\n    \"\"\"\n\n    if len(suptops) == 1:\n        return suptops\n\n    # consier simplifying in case of \"2\"\n\n    # keep track of which suptops have been used to build a bigger one\n    # these can be likely later discarded\n    ingredients = {}\n    excluded = []\n    while True:\n        any_new_suptop = False\n        for st1, st2 in itertools.combinations(suptops, r=2):\n            if {st1, st2} in excluded:\n                continue\n\n            if st1 in ingredients.get(st2, []) or st2 in ingredients.get(st1, []):\n                continue\n\n            if st1.is_subgraph_of(st2) or st2.is_subgraph_of(st1):\n                continue\n\n            # fixme - verify this one\n            if st1.eq(st2):\n                continue\n\n            # check if the two suptops are compatible\n            elif st1.is_consistent_with(st2):\n                # merge them!\n                large_suptop = copy.copy(st1)\n                # add both the pairs and the bonds that are not present in st1\n                large_suptop.merge(st2)\n                suptops.append(large_suptop)\n\n                ingredients[large_suptop] = {st1, st2}.union(ingredients.get(st1, set())).union(ingredients.get(st2, set()))\n                excluded.append({st1, st2})\n\n                # break\n                any_new_suptop = True\n\n        if not any_new_suptop:\n            break\n\n    # flatten\n    all_ingredients = list(itertools.chain(*ingredients.values()))\n\n    # return the larger suptops, but not the constituents\n    new_suptops = [st for st in suptops if st not in all_ingredients]\n    return new_suptops\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.merge_compatible_suptops_faster","title":"merge_compatible_suptops_faster","text":"
    merge_compatible_suptops_faster(pairing_suptop: Dict, min_bonds: int)\n

    :param pairing_suptop: :param min_bonds: if the End molecule at this point has only two bonds, they can be mapped to two other bonds in the start molecule. :return:

    Source code in ties/topology_superimposer.py
    def merge_compatible_suptops_faster(pairing_suptop: Dict, min_bonds: int):\n    \"\"\"\n\n    :param pairing_suptop:\n    :param min_bonds: if the End molecule at this point has only two bonds, they can be mapped to two other bonds\n        in the start molecule.\n    :return:\n    \"\"\"\n\n    if len(pairing_suptop) == 1:\n        return [pairing_suptop.popitem()[1]]\n\n    # any to any\n    all_pairings = list(itertools.combinations(pairing_suptop.keys(), r=min_bonds))\n\n    selected_pairings = []\n    for pairings in all_pairings:\n        n = set()\n        for pairing in pairings:\n            n.add(pairing[0])\n            n.add(pairing[1])\n        #\n        if 2 * len(pairings) == len(n):\n            selected_pairings.append(pairings)\n\n    # attempt to combine the different traversals\n    built_topologies = []\n    for mapping in selected_pairings:\n        # mapping the different bonds to different bonds\n\n        # check if the suptops are consistent with each other\n        if not are_consistent_topologies([pairing_suptop[key] for key in mapping]):\n            continue\n\n        # merge them!\n        large_suptop = copy.copy(pairing_suptop[mapping[0]])\n        for next_map in mapping[1:]:\n            next_suptop = pairing_suptop[next_map]\n\n            # add both the pairs and the bonds that are not present in st1\n            large_suptop.merge(next_suptop)\n\n        built_topologies.append(large_suptop)\n\n    return built_topologies\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer._overlay","title":"_overlay","text":"
    _overlay(n1, n2, parent_n1, parent_n2, bond_types, suptop, ignore_coords=False, use_element_type=True, exact_coords_cue=False)\n

    Jointly and recursively traverse the molecule while building up the suptop.

    If n1 and n2 are the same, we will be traversing through both graphs, marking the jointly travelled areas.

    Return the topology of the common substructure between the two molecules.

    n1 from the left molecule, n2 from the right molecule

    Source code in ties/topology_superimposer.py
    def _overlay(n1, n2, parent_n1, parent_n2, bond_types, suptop, ignore_coords=False, use_element_type=True,\n             exact_coords_cue=False):\n    \"\"\"\n    Jointly and recursively traverse the molecule while building up the suptop.\n\n    If n1 and n2 are the same, we will be traversing through both graphs, marking the jointly travelled areas.\n\n    Return the topology of the common substructure between the two molecules.\n\n    *n1 from the left molecule,\n    *n2 from the right molecule\n    \"\"\"\n\n    # ignore if either of the nodes is part of the suptop\n    if suptop.contains_node(n1) or suptop.contains_node(n2):\n        return None\n\n    if use_element_type and not n1.same_element(n2):\n        return None\n\n    # make more specific, ie if \"use_specific_type\"\n    if not use_element_type and not n1.same_type(n2):\n        return None\n\n    # Check for cycles\n    # if a new cycle is created by adding this node,\n    # then the cycle should be present in both, left and right ligand\n    safe = True\n    # if n1 is linked with node in suptop other than parent\n    for b1 in n1.bonds:\n        # if this bound atom is not a parent and is already a suptop\n        if b1.atom != parent_n1 and suptop.contains_node(b1.atom):\n            safe = False  # n1 forms cycle, now need to check n2\n            for b2 in n2.bonds:\n                if b2.atom != parent_n2 and suptop.contains_node(b2.atom):\n                    # b2 forms cycle, now need to check it's the same in both\n                    if suptop.contains((b1.atom, b2.atom)):\n                        safe = True\n                        break\n            if not safe:  # only n1 forms a cycle\n                break\n    if not safe:  # either only n1 forms cycle or both do but different cycles\n        return None\n\n    # now the same for any remaining unchecked bonds in n2\n    safe = True\n    for b2 in n2.bonds:\n        if b2.atom != parent_n2 and suptop.contains_node(b2.atom):\n            safe = False\n            for b1 in n1.bonds:\n                if b1.atom != parent_n1 and suptop.contains_node(b1.atom):\n                    if suptop.contains((b1.atom, b2.atom)):\n                        safe = True\n                        break\n            if not safe:\n                break\n    if not safe:\n        return None\n\n    # check if the cycle spans multiple cycles present in the left and right molecule,\n    if suptop.cycle_spans_multiple_cycles():\n        logger.debug('Found a cycle spanning multiple cycles')\n        return None\n\n    logger.debug(f\"Adding {(n1, n2)} to suptop.matched_pairs\")\n\n    # all looks good, create a new copy for this suptop\n    suptop = copy.copy(suptop)\n\n    # append both nodes as a pair to ensure that we keep track of the mapping\n    # having both nodes appended also ensure that we do not revisit/read neither n1 and n2\n    suptop.add_node_pair((n1, n2))\n    if not (parent_n1 is parent_n2 is None):\n        # fixme - adding a node pair should automatically take care of the bond, maybe using inner data?\n        # fixme why is this link different than a normal link?\n        suptop.link_with_parent((n1, n2), (parent_n1, parent_n2), bond_types)\n\n    # the extra bonds are legitimate\n    # so let's make sure they are added\n    # fixme: add function get_bonds_without_parent? or maybe make them \"subtractable\" even without the type\n    # for this it would be enough that the bonds is an object too, it will make it more managable\n    # bookkeeping? Ideally adding \"add_node_pair\" would take care of this\n    for n1_bonded in n1.bonds:\n        # ignore left parent\n        if n1_bonded.atom is parent_n1:\n            continue\n        for n2_bonded in n2.bonds:\n            # ignore right parent\n            if n2_bonded.atom is parent_n2:\n                continue\n\n            # if the pair exists, add a bond between the two pairs\n            if suptop.contains((n1_bonded.atom, n2_bonded.atom)):\n                # fixme: this linking of pairs should also be corrected\n                # 1) add \"pair\" as an object rather than a tuple (n1, n2)\n                # 2) this always has to happen, ie it is impossible to find (n1, n2)\n                # ie make it into a more sensible method,\n                # fixme: this does not link pairs?\n                suptop.link_pairs((n1, n2),\n                                  [((n1_bonded.atom, n2_bonded.atom), (n1_bonded.type, n2_bonded.type)), ])\n\n    # fixme: sort so that heavy atoms go first\n    p1_bonds = n1.bonds.without(parent_n1)\n    p2_bonds = n2.bonds.without(parent_n2)\n    candidate_pairings = list(itertools.product(p1_bonds, p2_bonds))\n\n    # check if any of the pairs have exactly the same location, use that as a hidden signal\n    # it is possible at this stage to use predetermine the distances\n    # and trick it to use the ones that have exactly the same distances,\n    # and treat that as a signal\n    # now the issue here is that someone might \"predetermine\" one part, ia CA1 mapping to CB1 rathern than CB2\n    # but if CA1 and CA2 is present, and CA2 is not matched to CB2 in a predetermined manner, than CB2 should not be deleted\n    # so we have to delete only the offers where CA1 = CB2 which would not be correct to pursue\n    if exact_coords_cue:\n        predetermined = {a1: a2 for a1, a2 in candidate_pairings if np.array_equal(a1.atom.position, a2.atom.position)}\n        predetermined.update(zip(list(predetermined.values()), list(predetermined.keys())))\n\n        # skip atom pairings that have been predetermined for other atoms\n        for n1_bond, n2_bond in candidate_pairings:\n            if n1_bond in predetermined or n2 in predetermined:\n                if predetermined[n1_bond] != n2_bond or predetermined[n2_bond] != n1_bond:\n                    candidate_pairings.remove((n1_bond, n2_bond))\n\n    # but they will be considered as a group\n    larger_suptops = []\n    pairing_and_suptop = {}\n    for n1_bond, n2_bond in candidate_pairings:\n        # fixme - ideally we would allow other typing than just the chemical element\n        if n1_bond.atom.element is not n2_bond.atom.element:\n            continue\n\n        logger.debug(f'sampling {n1_bond}, {n2_bond}')\n\n        # create a copy of the sup_top to allow for different traversals\n        # fixme: note that you could just send bonds, and that would have both parent etc with a bit of work\n        larger_suptop = _overlay(n1_bond.atom, n2_bond.atom,\n                                  parent_n1=n1, parent_n2=n2,\n                                  bond_types=(n1_bond.type, n2_bond.type),\n                                  suptop=suptop,\n                                  ignore_coords=ignore_coords,\n                                  use_element_type=use_element_type,\n                                  exact_coords_cue=exact_coords_cue)\n\n        if larger_suptop is not None:\n            larger_suptops.append(larger_suptop)\n            pairing_and_suptop[(n1_bond, n2_bond)] = larger_suptop\n\n    # todo\n    # check for \"predetermined\" atoms. Ie if they have the same coordinates,\n    # then that's the path to take, rather than a competing path??\n\n    # nothing further grown out of this suptop, so it is final\n    if not larger_suptops:\n        return suptop\n\n    # fixme: compare every two pairs of returned suptops, if they are compatible, join them\n    # fixme - note that we are repeating this partly below\n    # it also removes subgraph suptops\n    #all_solutions = merge_compatible_suptops(larger_suptops)\n    all_solutions = merge_compatible_suptops_faster(pairing_and_suptop, min(len(p1_bonds), len(p2_bonds)))\n\n    # if you couldn't merge any solutions, return the largest one\n    if not all_solutions:\n        all_solutions = list(pairing_and_suptop.values())\n\n    # sort in the descending order\n    all_solutions.sort(key=lambda st: len(st), reverse=True)\n    for sol1, sol2 in itertools.combinations(all_solutions, r=2):\n        if sol1.eq(sol2):\n            logger.debug(f\"Found the same solution and removing, solution: {sol1.matched_pairs}\")\n            if sol2 in all_solutions:\n                all_solutions.remove(sol2)\n\n    best_suptop = extract_best_suptop(all_solutions, ignore_coords)\n    return best_suptop\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.superimpose_topologies","title":"superimpose_topologies","text":"
    superimpose_topologies(top1_nodes, top2_nodes, pair_charge_atol=0.1, use_charges=True, use_coords=True, starting_node_pairs=None, force_mismatch=None, disjoint_components=False, net_charge_filter=True, net_charge_threshold=0.1, redistribute_charges_over_unmatched=True, parmed_ligA=None, parmed_ligZ=None, align_molecules=True, partial_rings_allowed=True, ignore_charges_completely=False, ignore_bond_types=True, ignore_coords=False, use_general_type=True, use_only_element=False, check_atom_names_unique=True, starting_pairs_heuristics=True, starting_pair_seed=None, config=None)\n

    The main function that manages the entire process.

    TODO: - check if each molecule topology is connected

    Source code in ties/topology_superimposer.py
    def superimpose_topologies(top1_nodes,\n                           top2_nodes,\n                           pair_charge_atol=0.1,\n                           use_charges=True,\n                           use_coords=True,\n                           starting_node_pairs=None,\n                           force_mismatch=None,\n                           disjoint_components=False,\n                           net_charge_filter=True,\n                           net_charge_threshold=0.1,\n                           redistribute_charges_over_unmatched=True,\n                           parmed_ligA=None,\n                           parmed_ligZ=None,\n                           align_molecules=True,\n                           partial_rings_allowed=True,\n                           ignore_charges_completely=False,\n                           ignore_bond_types=True,\n                           ignore_coords=False,\n                           use_general_type=True,\n                           use_only_element=False,\n                           check_atom_names_unique=True,\n                           starting_pairs_heuristics=True,\n                           starting_pair_seed=None,\n                           config=None):\n    \"\"\"\n    The main function that manages the entire process.\n\n    TODO:\n    - check if each molecule topology is connected\n    \"\"\"\n\n    if not ignore_charges_completely:\n        whole_charge = SuperimposedTopology.validate_charges(top1_nodes, top2_nodes)\n\n    # ensure that none of the atom names across the two molecules are the different\n    if check_atom_names_unique:\n        same_atom_names = {a.name for a in top1_nodes}.intersection({a.name for a in top2_nodes})\n        if len(same_atom_names) != 0:\n            logger.debug(f\"The atoms across the two ligands have the same atom names. \"\n                  f\"This might make it harder to trace back any problems. \"\n                  f\"Please ensure atom names are unique across the two ligands. : {same_atom_names}\")\n\n    if config is None:\n        weights = [1, 1]\n    else:\n        weights = config.weights\n\n    # Get the superimposed topology(/ies).\n    suptops = _superimpose_topologies(top1_nodes, top2_nodes, parmed_ligA, parmed_ligZ,\n                                      starting_node_pairs=starting_node_pairs,\n                                      ignore_coords=ignore_coords,\n                                      use_general_type=use_general_type,\n                                      starting_pairs_heuristics=starting_pairs_heuristics,\n                                      starting_pair_seed=starting_pair_seed,\n                                      weights=weights)\n    if not suptops:\n        warnings.warn('Did not find a single superimposition state.')\n        return None\n\n    logger.debug(f'Phase 1: The number of SupTops found: {len(suptops)}')\n    logger.debug(f'SupTops lengths:  {\", \".join([f\"ST{st.id}: {len(st)}\" for st in suptops])}')\n\n    # ignore bond types\n    # they are ignored when creating the run file with tleap anyway\n    for st in suptops:\n        # fixme - transition to config\n        st.ignore_bond_types = ignore_bond_types\n\n    # link the suptops to their original molecule data\n    for suptop in suptops:\n        # fixme - transition to config\n        suptop.set_tops(top1_nodes, top2_nodes)\n        suptop.set_parmeds(parmed_ligA, parmed_ligZ)\n\n    # align the 3D coordinates before applying further changes\n    # use the largest suptop to align the molecules\n    if align_molecules and not ignore_coords:\n        def take_largest(x, y):\n            return x if len(x) > len(y) else y\n        reduce(take_largest, suptops).align_ligands_using_mcs()\n        logger.info(f'RMSD of the best overlay: {suptops[0].align_ligands_using_mcs():.2f}')\n\n    # fixme - you might not need because we are now doing this on the way back\n    # if useCoords:\n    #     for sup_top in sup_tops:\n    #         sup_top.correct_for_coordinates()\n\n    # mismatch atoms as requested\n    if force_mismatch:\n        for sp in suptops:\n            for (a1, a2) in sp.matched_pairs[::-1]:\n                if (a1.name, a2.name) in force_mismatch:\n                    sp.remove_node_pair((a1, a2))\n                    logger.debug(f'Removing the pair: {((a1, a2))}, as requested')\n\n    # ensure that ring-atoms are not matched to non-ring atoms\n    for st in suptops:\n        st.ringring()\n\n    # introduce exceptions to the atom type types so that certain\n    # different atom types are seen as the same\n    # ie allow to swap cc-cd with cd-cc (and other pairs)\n    for st in suptops:\n        st.match_gaff2_nondirectional_bonds()\n\n    # remove matched atom pairs that have a different specific atom type\n    if not use_only_element:\n        for st in suptops:\n            # fixme - rename\n            st.enforce_matched_atom_types_are_the_same()\n\n    # ensure that the bonds are used correctly.\n    # If the bonds disagree, but atom types are the same, remove both bonded pairs\n    # we cannot have A-B where the bonds are different. In this case, we have A-B=C and A=B-C in a ring,\n    # we could in theory remove A,B,C which makes sense as these will show slightly different behaviour,\n    # and this we we avoid tensions in the bonds, and represent both\n    # fixme - apparently we are not relaying on these?\n    # turned off as this is reflected in the atom type\n    if not ignore_bond_types and False:\n        for st in suptops:\n            removed = st.removeMatchedPairsWithDifferentBonds()\n            if not removed:\n                logger.debug(f'Removed bonded pairs due to different bonds: {removed}')\n\n    if not partial_rings_allowed:\n        # remove partial rings, note this is a cascade problem if there are double rings\n        for suptop in suptops:\n            suptop.enforce_no_partial_rings()\n            logger.debug(f'Removed pairs because partial rings are not allowed {suptop._removed_because_unmatched_rings}')\n\n    # note that charges need to be checked before assigning IDs.\n    # ie if charges are different, the matched pair\n    # becomes two different atoms with different IDs\n    if use_charges and not ignore_charges_completely:\n        for sup_top in suptops:\n            removed = sup_top.unmatch_pairs_with_different_charges(atol=pair_charge_atol)\n            if removed:\n                logger.debug(f'Removed pairs with charge incompatibility: '\n                      f'{[(s[0], f\"{s[1]:.3f}\") for s in sup_top._removed_pairs_with_charge_difference]}')\n\n    if not partial_rings_allowed:\n        # We once again check if partial rings were created due to different charges on atoms.\n        for suptop in suptops:\n            suptop.enforce_no_partial_rings()\n            logger.debug(f'Removed pairs because partial rings are not allowed {suptop._removed_because_unmatched_rings}')\n\n    if net_charge_filter and not ignore_charges_completely:\n        # Note that we apply this rule to each suptop.\n        # This is because we are only keeping one suptop right now.\n        # However, if disjointed components are allowed, these number might change.\n        # ensure that each suptop component has net charge differences < 0.1\n        # Furthermore, disjointed components has not yet been applied,\n        # even though it might have an effect, fixme - should disjointed be applied first?\n        # to account for this implement #251\n        logger.debug(f'Accounting for net charge limit of {net_charge_threshold:.3f}')\n        for suptop in suptops[::-1]:\n            suptop.apply_net_charge_filter(net_charge_threshold)\n\n            # remove the suptop from the list if it's empty\n            if len(suptop) == 0:\n                suptops.remove(suptop)\n                continue\n\n            # Display information\n            if suptop._removed_due_to_net_charge:\n                logger.debug(f'SupTop: Removed pairs due to net charge: '\n                      f'{[[p[0], f\"{p[1]:.3f}\"] for p in suptop._removed_due_to_net_charge]}')\n\n    if not partial_rings_allowed:\n        # This is the 3rd check of partial rings. This time they might have been created due to net_charges.\n        for suptop in suptops:\n            suptop.enforce_no_partial_rings()\n            logger.debug(f'Removed pairs because partial rings are not allowed {suptop._removed_because_unmatched_rings}')\n\n    # remove the suptops that are empty\n    for st in suptops[::-1]:\n        if len(st) == 0:\n            suptops.remove(st)\n\n    if not disjoint_components:\n        logger.debug(f'Checking for disjoint components in the {len(suptops)} suptops')\n        # ensure that each suptop represents one CC\n        # check if the graph was divided after removing any pairs (e.g. due to charge mismatch)\n        # fixme - add the log about which atoms are removed?\n        [st.largest_cc_survives() for st in suptops]\n\n        for st in suptops:\n            logger.debug('Removed disjoint components: ', st._removed_because_disjointed_cc)\n\n        # fixme\n        # remove the smaller suptop, or one arbitrary if they are equivalent\n        # if len(suptops) > 1:\n        #     max_len = max([len(suptop) for suptop in suptops])\n        #     for suptop in suptops[::-1]:\n        #         if len(suptop) < max_len:\n        #             suptops.remove(suptop)\n        #\n        #     # if there are equal length suptops left, take only the first one\n        #     if len(suptops) > 1:\n        #         suptops = [suptops[0]]\n        #\n        # assert len(suptops) == 1, suptops\n\n    suptop = extract_best_suptop(suptops, ignore_coords, get_list=False)\n\n    if redistribute_charges_over_unmatched and not ignore_charges_completely:\n        # assume that none of the suptops are disjointed\n        logger.debug('Assuming that all suptops are separate at this point')\n        # fixme: apply distribution of q only on the first st, that's the best one anyway,\n\n        # we only want to apply redistribution once on the largest piece for now\n        suptop.redistribute_charges()\n\n    # atom ID assignment has to come after any removal of atoms due to their mismatching charges\n    suptop.assign_atoms_ids(1)\n\n    # there might be several best solutions, order them according the RMSDs\n    # suptops.sort(key=lambda st: st.rmsd())\n\n    # fixme - remove the hydrogens without attached heavy atoms\n\n    # resolve_sup_top_multiple_match(sup_tops_charges)\n    # sup_top_correct_chirality(sup_tops_charges, sup_tops_no_charges, atol=atol)\n\n    # carry out a check. Each\n    if align_molecules and not ignore_coords:\n        main_rmsd = suptop.align_ligands_using_mcs()\n        for mirror in suptop.mirrors:\n            mirror_rmsd = mirror.align_ligands_using_mcs()\n            if mirror_rmsd < main_rmsd:\n                logger.debug('THE MIRROR RMSD IS LOWER THAN THE MAIN RMSD')\n        suptop.align_ligands_using_mcs(overwrite_original=True)\n\n    # print a general summary\n    logger.info('-------- Summary -----------')\n    logger.info(f'Number of matched pairs: {len(suptop.matched_pairs)} out of {len(top1_nodes)}L/{len(top2_nodes)}R')\n    logger.info(f'Disappearing atoms: { (len(top1_nodes) - len(suptop.matched_pairs)) / len(top1_nodes) * 100:.1f}%')\n    logger.info(f'Appearing atoms: { (len(top2_nodes) - len(suptop.matched_pairs)) / len(top2_nodes) * 100:.1f}%')\n\n    return suptop\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.extract_best_suptop","title":"extract_best_suptop","text":"
    extract_best_suptop(suptops, ignore_coords, weights=[1, 1], get_list=False)\n

    Assumes that any merging possible already took place. We now have a set of solutions and have to select the best ones.

    :param suptops: :param ignore_coords: :return:

    Source code in ties/topology_superimposer.py
    def extract_best_suptop(suptops, ignore_coords, weights=[1, 1], get_list=False):\n    \"\"\"\n    Assumes that any merging possible already took place.\n    We now have a set of solutions and have to select the best ones.\n\n    :param suptops:\n    :param ignore_coords:\n    :return:\n    \"\"\"\n    # fixme - ignore coords currently does not work\n    # multiple different paths to traverse the topologies were found\n    # this means some kind of symmetry in the topologies\n    # For example, in the below drawn case (starting from C1-C11) there are two\n    # solutions: (O1-O11, O2-O12) and (O1-O12, O2-O11).\n    #     LIGAND 1        LIGAND 2\n    #        C1              C11\n    #        \\                \\\n    #        N1              N11\n    #        /\\              / \\\n    #     O1    O2        O11   O12\n    # Here we decide which of the mappings is better.\n    # fixme - uses coordinates to decide which mapping is better.\n    #  - Improve: use dihedral angles to decide which mapping is better too\n    def item_or_list(suptops):\n        if get_list:\n            return suptops\n        else:\n            return suptops[0]\n\n    if len(suptops) == 0:\n        warnings.warn('Cannot decide on the best mapping without any suptops...')\n        return None\n\n    elif len(suptops) == 1:\n        return item_or_list(suptops)\n\n    #candidates = copy.copy(suptops)\n\n    # sort from largest to smallest\n    suptops.sort(key=lambda st: len(st), reverse=True)\n\n    if ignore_coords:\n        return item_or_list(suptops)\n\n    # when length is the same, take the smaller RMSD\n    # most likely this is about hydrogens\n    different_length_suptops = []\n    for key, same_length_suptops in itertools.groupby(suptops, key=lambda st: len(st)):\n        # order by RMSD\n        sorted_by_rmsd = sorted(same_length_suptops, key=lambda st: st.align_ligands_using_mcs())\n        # these have the same lengths and the same RMSD, so they must be mirrors\n        for suptop in sorted_by_rmsd[1:]:\n            if suptop.is_mirror_of(sorted_by_rmsd[0]):\n                sorted_by_rmsd[0].add_mirror_suptop(suptop)\n            else:\n                # add it as a different solution\n                different_length_suptops.append(suptop)\n        different_length_suptops.append(sorted_by_rmsd[0])\n\n    # sort using weights\n    # score = mcs_score * weight - rmsd * weight ;\n    def score(st):\n        # inverse for 0 to be optimal\n        mcs_score = (1 - st.mcs_score()) * weights[0]\n\n        # rmsd 0 is best as well\n        rmsd_score = st.align_ligands_using_mcs() * weights[1]\n\n        return (mcs_score + rmsd_score) / len(weights)\n\n    different_length_suptops.sort(key=score)\n    # if they have a different length, there must be a reason why it is better.\n    # todo\n\n    return item_or_list(different_length_suptops)\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.is_mirror_of_one","title":"is_mirror_of_one","text":"
    is_mirror_of_one(candidate_suptop, suptops, ignore_coords)\n

    \"Mirror\" in the sense that it is an alternative topological way to traverse the molecule.

    Depending on the \"better\" fit between the two mirrors, we pick the one that is better.

    Source code in ties/topology_superimposer.py
    def is_mirror_of_one(candidate_suptop, suptops, ignore_coords):\n    \"\"\"\n    \"Mirror\" in the sense that it is an alternative topological way to traverse the molecule.\n\n    Depending on the \"better\" fit between the two mirrors, we pick the one that is better.\n    \"\"\"\n    for next_suptop in suptops:\n        if next_suptop.is_mirror_of(candidate_suptop):\n            # the suptop saved as the mirror should be the suptop\n            # that is judged to be of a lower quality\n            best_suptop = extract_best_suptop([candidate_suptop, next_suptop], ignore_coords)\n\n            if next_suptop is best_suptop:\n                next_suptop.add_mirror_suptop(candidate_suptop)\n            else:\n                suptops.remove(next_suptop)\n                suptops.append(candidate_suptop)\n\n            return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.generate_nxg_from_list","title":"generate_nxg_from_list","text":"
    generate_nxg_from_list(atoms)\n

    Helper function. Generates a graph from a list of atoms @parameter atoms: follow the internal format for atoms

    Source code in ties/topology_superimposer.py
    def generate_nxg_from_list(atoms):\n    \"\"\"\n    Helper function. Generates a graph from a list of atoms\n    @parameter atoms: follow the internal format for atoms\n    \"\"\"\n    g = nx.Graph()\n    # add attoms\n    [g.add_node(a) for a in atoms]\n    # add all the edges\n    for a in atoms:\n        # add the edges from nA\n        for a_bonded in a.bonds:\n            g.add_edge(a, a_bonded.atom)\n\n    return g\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.get_starting_configurations","title":"get_starting_configurations","text":"
    get_starting_configurations(left_atoms, right_atoms, fraction=0.2, filter_ring_c=True)\n
    Minimise the number of starting configurations to optimise the process speed.\nUse:\n * the rarity of the specific atom types,\n * whether the atoms are bottlenecks (so they do not suffer from symmetry).\n    The issue with symmetry is that it is impossible to find the proper\n    symmetry match if you start from the wrong symmetry.\n@parameter fraction: ensure that the number of atoms used to start the traversal is not more\n    than the fraction value of the overall number of possible matches, counted as\n    a fraction of the maximum possible number of pairs (MIN(LEFTNODES, RIGHTNODES))\n@parameter filter_ring_c: filter out the carbon elements in the rings to avoid any issues\n    with the symmetry. This assumes that a ring usually has one N element, etc.\n

    TODO - ignore hydrogens?

    Source code in ties/topology_superimposer.py
    def get_starting_configurations(left_atoms, right_atoms, fraction=0.2, filter_ring_c=True):\n    \"\"\"\n        Minimise the number of starting configurations to optimise the process speed.\n        Use:\n         * the rarity of the specific atom types,\n         * whether the atoms are bottlenecks (so they do not suffer from symmetry).\n            The issue with symmetry is that it is impossible to find the proper\n            symmetry match if you start from the wrong symmetry.\n        @parameter fraction: ensure that the number of atoms used to start the traversal is not more\n            than the fraction value of the overall number of possible matches, counted as\n            a fraction of the maximum possible number of pairs (MIN(LEFTNODES, RIGHTNODES))\n        @parameter filter_ring_c: filter out the carbon elements in the rings to avoid any issues\n            with the symmetry. This assumes that a ring usually has one N element, etc.\n\n    TODO - ignore hydrogens?\n    \"\"\"\n    logger.debug('Superimposition: optimising the search by narrowing down the starting configuration. ')\n    left_atoms_noh = list(filter(lambda a: a.element != 'H', left_atoms))\n    right_atoms_noh = list(filter(lambda a: a.element != 'H', right_atoms))\n\n    # find out which atoms types are common across the two molecules\n    # fixme - consider subclassing atom from MDAnalysis class and adding functions for some of these features\n    # first, find the unique types for each molecule\n    left_types = {left_atom.type for left_atom in left_atoms_noh}\n    right_types = {right_atom.type for right_atom in right_atoms_noh}\n    common_types = left_types.intersection(right_types)\n\n    # for each atom type, check how many maximum atoms can theoretically be matched\n    per_type_max_counter = {}\n    for atom_type in common_types:\n        left_count_by_type = sum([1 for left_atom in left_atoms if left_atom.type == atom_type])\n        right_count_by_type = sum([1 for right_atom in right_atoms if right_atom.type == atom_type])\n        per_type_max_counter[atom_type] = min(left_count_by_type, right_count_by_type)\n    max_overlap_size = sum(per_type_max_counter.values())\n    logger.info(f'Largest MCS size: {max_overlap_size}')\n\n    left_atoms_starting = left_atoms_noh[:]\n    right_atoms_starting = right_atoms_noh[:]\n\n    # ignore carbons in cycles\n    # fixme - we should not use this for macrocycles, which should be ignored here\n    if filter_ring_c:\n        nxl = generate_nxg_from_list(left_atoms)\n        for cycle in nx.cycle_basis(nxl):\n            # ignore the carbons in the cycle\n            cycle_carbons = list(filter(lambda a: a.element == 'C', cycle))\n            logger.debug(f'Superimposition of left atoms: Ignoring carbons as starting configurations because '\n                  f'they are carbons in a cycle: {cycle_carbons}')\n            [left_atoms_starting.remove(a) for a in cycle_carbons if a in left_atoms_starting]\n        nxr = generate_nxg_from_list(right_atoms_starting)\n        for cycle in nx.cycle_basis(nxr):\n            # ignore the carbons in the cycle\n            cycle_carbons = list(filter(lambda a: a.element == 'C', cycle))\n            logger.debug(f'Superimposition of right atoms: Ignoring carbons as starting configurations because '\n                  f'they are carbons in a cycle: {cycle_carbons}')\n            [right_atoms_starting.remove(a) for a in cycle_carbons if a in right_atoms_starting]\n\n    # find out which atoms types are common across the two molecules\n    # fixme - consider subclassing atom from MDAnalysis class and adding functions for some of these features\n    # first, find the unique types for each molecule\n    left_types = {left_atom.type for left_atom in left_atoms_starting}\n    right_types = {right_atom.type for right_atom in right_atoms_starting}\n    common_types = left_types.intersection(right_types)\n\n    # for each atom type, check how many maximum atoms can theoretically be matched\n    paired_by_type = []\n    max_after_cycle_carbons = 0\n    for atom_type in common_types:\n        picked_left = list(filter(lambda a: a.type == atom_type, left_atoms_starting))\n        picked_right = list(filter(lambda a: a.type == atom_type, right_atoms_starting))\n        paired_by_type.append([picked_left, picked_right])\n        max_after_cycle_carbons += min(len(picked_left), len(picked_right))\n    logger.debug(f'Superimposition: simple max match of atoms after cycle carbons exclusion: {max_after_cycle_carbons}')\n\n    # sort atom according to their type rarity\n    # use the min across, since 1x4 mapping will give 4 options only, so we count this as one,\n    # but 4x4 would give 16,\n    sorted_paired_by_type = sorted(paired_by_type, key=lambda p: min(len(p[0]), len(p[1])))\n\n    # find the atoms in each type and generate appropriate pairs,\n    # use only a fraction of the maximum theoretical match\n    desired_number_of_pairs = int(fraction * max_overlap_size)\n\n    starting_configurations = []\n    added_counter = 0\n    for rare_left_atoms, rare_right_atoms in sorted_paired_by_type:\n        # starting_configurations\n        starting_configurations.extend(list(itertools.product(rare_left_atoms, rare_right_atoms)))\n        added_counter += min(len(rare_left_atoms), len(rare_right_atoms))\n        if added_counter > desired_number_of_pairs:\n            break\n\n    logger.debug(f'Superimposition: initial starting pairs for the search: {starting_configurations}')\n    return starting_configurations\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer._superimpose_topologies","title":"_superimpose_topologies","text":"
    _superimpose_topologies(top1_nodes, top2_nodes, mda1_nodes=None, mda2_nodes=None, starting_node_pairs=None, ignore_coords=False, use_general_type=True, starting_pairs_heuristics=True, starting_pair_seed=None, weights=[1, 1])\n

    Superimpose two molecules.

    @parameter rare_atoms_starting_pair: instead of trying every possible pair for the starting configuration, use several information to narrow down the good possible starting configuration. Specifically, use two things: 1) the extact atom type, find how rare they are, and use the rarity to make the call, 2) use the \"linkers\" and areas that are not parts of the rings to avoid the issue of symmetry in the ring. We are striving here to have 5% starting configurations.

    Source code in ties/topology_superimposer.py
    def _superimpose_topologies(top1_nodes, top2_nodes, mda1_nodes=None, mda2_nodes=None,\n                            starting_node_pairs=None,\n                            ignore_coords=False,\n                            use_general_type=True,\n                            starting_pairs_heuristics=True,\n                            starting_pair_seed=None,\n                            weights=[1, 1]):\n    \"\"\"\n    Superimpose two molecules.\n\n    @parameter rare_atoms_starting_pair: instead of trying every possible pair for the starting configuration,\n        use several information to narrow down the good possible starting configuration. Specifically,\n        use two things: 1) the extact atom type, find how rare they are, and use the rarity to make the call,\n        2) use the \"linkers\" and areas that are not parts of the rings to avoid the issue of symmetry in the ring.\n        We are striving here to have 5% starting configurations.\n    \"\"\"\n\n    # superimposed topologies\n    suptops = []\n    # grow the topologies using every combination node1-node2 as the starting point\n    # fixme - Test/Optimisation: create a theoretical maximum of a match between two molecules\n    # - Proposal 1: find junctions and use them to start the search\n    # - Analyse components of the graph (ie rotatable due to a single bond connection) and\n    #   pick a starting point from each component\n    if not starting_node_pairs:\n        # generate each to each nodes\n        if starting_pair_seed:\n            left_atom = [a for a in list(top1_nodes) if a.name == starting_pair_seed[0]][0]\n            right_atom = [a for a in list(top2_nodes) if a.name == starting_pair_seed[1]][0]\n            starting_node_pairs = [(left_atom, right_atom), ]\n        elif starting_pairs_heuristics:\n            starting_node_pairs = get_starting_configurations(top1_nodes, top2_nodes)\n            logger.debug('Using heuristics to select the initial pairs for searching the maximum overlap.'\n                  'Could produce non-optimal results.')\n        else:\n            starting_node_pairs = list(itertools.product(top1_nodes, top2_nodes))\n            logger.debug('Checking all possible initial pairs to find the optimal MCS. ')\n\n    for node1, node2 in starting_node_pairs:\n        # with the given starting two nodes, generate the maximum common component\n        suptop = SuperimposedTopology(list(top1_nodes), list(top2_nodes), mda1_nodes, mda2_nodes)\n        # fixme turn into a property\n        candidate_suptop = _overlay(node1, node2, parent_n1=None, parent_n2=None, bond_types=(None, None),\n                                    suptop=suptop,\n                                    ignore_coords=ignore_coords,\n                                    use_element_type=use_general_type)\n        if candidate_suptop is None:\n            # there is no overlap, ignore this case\n            continue\n\n        # check if the maximal possible solution was found\n        # Optimise - can you at this point finish the superimposition if the molecules are fully superimposed?\n        # candidate_suptop.is_subgraph_of_global_top()\n\n        if exists_in(candidate_suptop, suptops):\n            continue\n\n        # ignore if it is a subgraph of another solution\n        if subgraph_of(candidate_suptop, suptops):\n            continue\n\n        # check if this superimposed topology is a mirror of one that already exists\n        # fixme the order matters in this place\n        # fixme - what if the mirror has a lower rmsd match? in that case, pick that mirror here\n        if is_mirror_of_one(candidate_suptop, suptops, ignore_coords):\n            continue\n\n        #\n        remove_candidates_subgraphs(candidate_suptop, suptops)\n\n        # while comparing partial overlaps, suptops can be modified\n        # and_ignore = solve_partial_overlaps(candidate_suptop, suptops)\n        # if and_ignore:\n        #     continue\n\n        # fixme - what to do when about the odd pairs randomH-randomH etc? they won't be found in other graphs\n        # follow a rule: if this node was used before in a larger superimposed topology, than it should\n        # not be in the final list (we guarantee that each node is used only once)\n        suptops.append(candidate_suptop)\n\n    # if there are only hydrogens superimposed without a connection to any heavy atoms, ignore these too\n    for suptop in suptops[::-1]:\n        all_hydrogens = True\n        for node1, _ in suptop.matched_pairs:\n            if not node1.type == 'H':\n                all_hydrogens = False\n                break\n        if all_hydrogens:\n            logger.debug(f\"Removing sup top because only hydrogens found {suptop.matched_pairs}\")\n            suptops.remove(suptop)\n\n    # TEST: check that each node was used only once, fixme use only on the winner\n    # for suptop in suptops:\n    #     [all_nodes.extend([node1, node2]) for node1, node2 in suptop.matched_pairs]\n    #     pair_count += len(suptop.matched_pairs)\n    #     assert len(set(all_nodes)) == 2 * pair_count\n\n    # TEST: check that the nodes on the left are always from topology 1 and the nodes on the right are always from top2\n    for suptop in suptops:\n        for node1, node2 in suptop.matched_pairs:\n            assert node1 in list(top1_nodes)\n            assert node2 in list(top2_nodes)\n\n    # clean the overlays by removing sub_overlays.\n    # ie if all atoms in an overlay are found to be a bigger part of another overlay,\n    # then that overlay is better\n    logger.info(f\"Found overlays: {len(suptops)}\")\n\n    # finally, once again, order the suptops and return the best one\n    suptops = extract_best_suptop(suptops, ignore_coords, weights, get_list=True)\n\n    # fixme - return other info\n    return suptops\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.get_atoms_bonds_from_mol2","title":"get_atoms_bonds_from_mol2","text":"
    get_atoms_bonds_from_mol2(ref_filename, mob_filename, use_general_type=True)\n

    Use Parmed to load the files.

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.get_atoms_bonds_from_mol2--returns","title":"returns","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer.get_atoms_bonds_from_mol2--1-a-dictionary-with-charges-eg-item-c17-0222903","title":"1) a dictionary with charges, e.g. Item: \"C17\" : -0.222903","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer.get_atoms_bonds_from_mol2--2-a-list-of-bonds","title":"2) a list of bonds","text":"Source code in ties/topology_superimposer.py
    def get_atoms_bonds_from_mol2(ref_filename, mob_filename, use_general_type=True):\n    \"\"\"\n    Use Parmed to load the files.\n\n    # returns\n    # 1) a dictionary with charges, e.g. Item: \"C17\" : -0.222903\n    # 2) a list of bonds\n    \"\"\"\n    ref = parmed.load_file(str(ref_filename), structure=True)\n    mobile = parmed.load_file(str(mob_filename), structure=True)\n\n    def create_atoms(parmed_atoms):\n        \"\"\"\n        # convert the Parmed atoms into Atom objects.\n        \"\"\"\n        atoms = []\n        for parmed_atom in parmed_atoms:\n            atom_type = parmed_atom.type\n            # atom type might be empty if\n            if not atom_type:\n                # use the atom name as the atom type, e.g. C7\n                atom_type = parmed_atom.name\n\n\n            try:\n                atom = Atom(name=parmed_atom.name, atom_type=atom_type, charge=parmed_atom.charge, use_general_type=use_general_type)\n            except AttributeError:\n                # most likely the charges were missing, manually set the charges to 0\n                atom = Atom(name=parmed_atom.name, atom_type=atom_type, charge=0.0, use_general_type=use_general_type)\n                logger.warning('One of the input files is missing charges. Setting the charge to 0')\n            atom.id = parmed_atom.idx\n            atom.position = [parmed_atom.xx, parmed_atom.xy, parmed_atom.xz]\n            atom.resname = parmed_atom.residue.name\n            atoms.append(atom)\n        return atoms\n\n    universe_ref_atoms = create_atoms(ref.atoms)\n    # note that these coordinate should be superimposed\n    universe_mob_atoms = create_atoms(mobile.atoms)\n\n    # fixme - add a check that all the charges come to 0 as declared in the header\n    universe_ref_bonds = [(b.atom1.idx, b.atom2.idx, b.order) for b in ref.bonds]\n    universe_mob_bonds = [(b.atom1.idx, b.atom2.idx, b.order) for b in mobile.bonds]\n\n    return universe_ref_atoms, universe_ref_bonds, \\\n           universe_mob_atoms, universe_mob_bonds, \\\n           ref, mobile\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.assign_coords_from_pdb","title":"assign_coords_from_pdb","text":"
    assign_coords_from_pdb(atoms, pdb_atoms)\n

    Match the atoms from the ParmEd object based on a .pdb file and overwrite the coordinates from ParmEd. :param atoms: internal Atom representation (fixme: refer to it here in docu), will have their coordinates overwritten. :param pdb_atoms: atoms loaded with ParmEd with the coordinates to be used

    Source code in ties/topology_superimposer.py
    def assign_coords_from_pdb(atoms, pdb_atoms):\n    \"\"\"\n    Match the atoms from the ParmEd object based on a .pdb file\n    and overwrite the coordinates from ParmEd.\n    :param atoms: internal Atom representation (fixme: refer to it here in docu),\n        will have their coordinates overwritten.\n    :param pdb_atoms: atoms loaded with ParmEd with the coordinates to be used\n\n    \"\"\"\n    for atom in atoms:\n        # find the corresponding atom\n        found_match = False\n        for pdb_atom in pdb_atoms.atoms:\n            if pdb_atom.name.upper() == atom.name.upper():\n                # charges?\n                atom.position = (pdb_atom.xx, pdb_atom.xy, pdb_atom.xz)\n                found_match = True\n                break\n        if not found_match:\n            logger.error(f\"Did not find atom? {atom.name}\")\n            raise Exception(\"wait a minute\")\n
    "},{"location":"usage/api/","title":"Examples - Python","text":"

    TIES also offers a python API. Here is a minimal example:

    from ties import Pair\n\n# load the two ligands and use the default configuration\npair = Pair('l02.mol2', 'l03.mol2')\n# superimpose the ligands passed above\nhybrid = pair.superimpose()\n\n# save the results\nhybrid.write_metadata('meta_l02_l03.json')\nhybrid.write_pdb('l02_l03_morph.pdb')\nhybrid.write_mol2('l02_l03_morph.mol2')\n

    This minimal example can be extended with the protein to generate the input for the TIES_MD package for the simulations in either NAMD or OpenMM.

    Note that in this example we do not set any explicit settings. For that we need to employ the Config class which we can then pass to the Pair.

    Info

    Config contains the settings for all classes in the TIES package, and therefore can be used to define a protocol.

    Whereas all settings can be done in :class:Config, for clarity some can be passed separately here to the :class:Pair. This way, it overwrites the settings in the config object:

    from ties import Pair\nfrom ties import Config\nfrom ties import Protein\n\n\nconfig = Config()\n# configure the two settings\nconfig.workdir = 'ties20'\nconfig.md_engine = 'openmm'\n# set ligand_net_charge as a parameter,\n# which is equivalent to using config.ligand_net_charge\npair = Pair('l02.mol2', 'l03.mol2', ligand_net_charge=-1, config=config)\n# rename atoms to help with any issues\npair.make_atom_names_unique()\n\nhybrid = pair.superimpose()\n\n# save meta data to files\nhybrid.write_metadata()\nhybrid.write_pdb()\nhybrid.write_mol2()\n\n# add the protein for the full RBFE protocol\nconfig.protein = 'protein.pdb'\nconfig.protein_ff = 'leaprc.protein.ff14SB'\nprotein = Protein(config.protein, config)\nhybrid.prepare_inputs(protein=protein)\n

    Below we show the variation in which we are using :class:Config to pass the net charge of the molecule.

    from ties import Pair\nfrom ties import Config\n\n# explicitly create config (which will be used by all classes underneath)\nconfig = Config()\nconfig.ligand_net_charge = -1\n\npair = Pair('l02.mol2', 'l03.mol2', config=config)\npair.make_atom_names_unique()\n\n# overwrite the previous config settings with relevant parameters\nhybrid = pair.superimpose(use_element_in_superimposition=True, redistribute_q_over_unmatched=True)\n\n# save meta data to specific locations\nhybrid.write_metadata('result.json')\nhybrid.write_pdb('result.pdb')\nhybrid.write_mol2('result.mol2')\n\nhybrid.prepare_inputs()\n

    Note that there is also the :class:Ligand that supports additional operations, and can be passed directly to :class:Ligand.

    from ties import Ligand\n\n\nlig = Ligand('l02_same_atom_name.mol2')\n\n# prepare the .mol2 input\nlig.antechamber_prepare_mol2()\n\n# the final .mol2 file\nassert lig.current.exists()\n
    "},{"location":"usage/cli/","title":"CLI","text":"

    TIES can be access via both command line and python interface.

    In the smallest example one can carry out a superimposition employing only two ligands

    ties --ligands l03.mol2 l02.mol2\n

    Ideally these .mol2 files already have a pre-assigned charges in the last column for each atom. See for example MCL1 case.

    In the case of this example, we are working on molecules that are negatively charges (-1e), and we need to specify it:

    ties --ligands l03.mol2 l02.mol2 --ligand-net-charge -1\n

    The order the of the ligands matters and more ligands can be passed. This command creates by default a ties-input directory with all output files. These include meta_*_*.json files which contain the details about how the ligands were superimposed, and what configuration was used. The general directory structure will look like this:

        ties\n    \u251c\u2500\u2500 mol2\n    \u2502    \u251c\u2500\u2500 l02\n    \u2502    \u2514\u2500\u2500 l03\n    \u251c\u2500\u2500 prep\n    \u2502\u00a0\u00a0 \u251c\u2500\u2500 ligand_frcmods\n    \u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u251c\u2500\u2500 l02\n    \u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 l03\n    \u2502\u00a0\u00a0 \u2514\u2500\u2500 morph_frcmods\n    \u2502\u00a0\u00a0     \u2514\u2500\u2500 tests\n    \u2502\u00a0\u00a0         \u2514\u2500\u2500 l02_l03\n    \u2514\u2500\u2500 ties-l02-l03\n        \u2514\u2500\u2500 lig\n            \u2514\u2500\u2500 build\n

    Note that all the output generated by ambertools is stored, and can be investigated.

    The full RBFE requires also the protein, as well as the net charge of the ligands used in the transformation:

    ties -l l02.mol2 l03.mol2 -nc -1 --protein protein.pdb\n

    Check all the options with

    ties -h\n

    Warning

    This code is currently experimental and under active development. If you notice any problems, report them. *

    "}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Overview","text":"

    TIES is a python library for the relative binding free energy (RBFE) calculations.

    TIES superimposes ligands and prepares the input files for molecular dynamics simulations. These simulations can be carried out with either NAMD or OpenMM using the package TIES_MD.

    For the details about the protocol and its validation please refer to the following publications

    Mateusz K. Bieniek, Agastya P. Bhati, Shunzhou Wan, and Peter V. Coveney. Ties 20: relative binding free energy with a flexible superimposition algorithm and partial ring morphing. Journal of Chemical Theory and Computation, 17(2):1250\u20131265, 2021. PMID: 33486956. doi:10.1021/acs.jctc.0c01179.

    "},{"location":"installation/","title":"Installation","text":"

    The easiest way to start is to use the latest published conda-forge package. Create a new environment and use:

    conda install ties\n
    "},{"location":"installation/#development-version","title":"Development version","text":"

    Whereas most of the dependancies can be installed with pip, ambertools has to be either compiled. The easiest way is to use the conda-forge environment:

    mamba env create -f environment.yml\nconda activate ties \npip install --no-deps . \n
    "},{"location":"publications/","title":"Publications","text":"

    We list here a list of publication that utilised the software for carrying our RBFE calculations.

    Mateusz K. Bieniek, Agastya P. Bhati, Shunzhou Wan, and Peter V. Coveney. Ties 20: relative binding free energy with a flexible superimposition algorithm and partial ring morphing. Journal of Chemical Theory and Computation, 17(2):1250\u20131265, 2021. PMID: 33486956. doi:10.1021/acs.jctc.0c01179.

    Mateusz K Bieniek, Alexander D Wade, Agastya P Bhati, Shunzhou Wan, and Peter V Coveney. TIES 2.0: A Dual-Topology Open Source Relative Binding Free Energy Builder with Web Portal. Journal of Chemical Information and Modeling, 63(3):718\u2013724, 2023. URL: https://doi.org/10.1021/acs.jcim.2c01596, doi:10.1021/acs.jcim.2c01596.

    "},{"location":"superimposition/","title":"Superimposition","text":"

    superimposition

    "},{"location":"theory/","title":"TIES Protocol","text":""},{"location":"theory/#superimposition-and-defining-the-alchemical-region","title":"Superimposition and defining the alchemical region","text":"

    Any two pairs are superimposed using a recursive joint traversal of two molecules starting from any two pairs.

    A heuristics (on by default) reduces the search space by selecting the rarer atoms that are present across the two molecules as the starting points for the traversal, decreasing substantially the computational cost.

    "},{"location":"theory/#charge-treatment","title":"Charge treatment","text":"

    TIES 20 supports the transformation between ligands that have the same net charge.

    We employ a dual topology approach which divides the atoms in each transformation into three groups:

    1. Joint region. This is the region of the molecule where the atoms are the same meaning that they are shared across the two ligands in the transformation.
    2. Disappearing region. Atoms present only in the starting ligand of the transformation which are fully represented at lambda=0 and which will be scaled accordingly during the lambda progression.
    3. Appearing region. Atoms present only in the ending ligand of the transformation and therefore not present at lambda=0. These atoms start appearing during the lambda progression and are fully represented at lambda=1.

    When the two ligands in a transformation are superimposed together, the treatment of charges depends on which group they belong to.

    "},{"location":"theory/#joint-region-matched-atoms-and-their-charges","title":"Joint region: matched atoms and their charges","text":"

    In the joint region of the transformation, first --q-pair-tolerance is used to determine whether the two original atoms are truly the same atoms. If their charges differ by more than this value (default 0.1e), then the two atoms will be added to the alchemical regions (Disappearing and appearing).

    It is possible that a lot of matched atoms in the joint region, with each pair being within 0.1e of each other, cumulatively have rather different charges between the starting and the ending ligand. For this reason, TIES 20 sums the differences between the starting and the ending atoms in the joint region, and if the total is larger than -netqtol (default 0.1e) then we further expand the alchemical region until the \"appearing\" and \"disappearing\" regions in the joint region are of a sufficiently similar net charge.

    Abiding by -netqtol rule has the further effect that, inversely, the alchemical regions (disappearing and appearing regions), will have very similar net charges - which is a necessary condition for the calculation of the partial derivative of the potential energy with respect to the lambda.

    If -netqtol rule is violated, different schemes for the removal of the matched atoms in the joint region are tried to satisfy the net charge limit. The scheme that removes fewest matched pairs, is used. In other words, TIES 20 is trying to use the smallest alchemical region possible while satisfying the rule.

    Note that we are not summing together the absolute differences in charges in the joint region. This means that if one atom pair has 0.02e charge difference, and another pair has -0.02e charge difference, then their total is zero. In other words, we are not worried about the distribution of the differences in charges in the joint region.

    The hydrogen charges are considered by absorbing them into the heavy atoms.

    The charges in the joint region for each pair are averaged.

    The last step is redistribution, where the final goal is that the net charge is the same in the Appearing and in the Disappearing alchemical region. After averaging the charges in the joint region, its overall charge summed with the charge of each alchemical region should be equal to the whole molecule net charge: :math:q_{joint} + q_{appearing} == q_{joint} + q_{disappearing} == q_{molecule}. Therefore, after averaging the charges, :math:q_{molecule} - q_{joint} - q_{appearing} is distributed equally in the region :math:q_{appearing}. The same rule is applied in :math:q_{disappearing}.

    "},{"location":"api/config/","title":"Config","text":""},{"location":"api/config/#ties.Config","title":"Config","text":"
    Config(**kwargs)\n

    The configuration with parameters that can be used to define the entire protocol. The settings can be overridden later in the actual classes.

    The settings are stored as properties in the object and can be overwritten.

    Methods:

    • get_element_map \u2013

      :return:

    • get_serializable \u2013

      Get a JSON serializable structure of the config.

    Attributes:

    • workdir \u2013

      Working directory for antechamber calls.

    • protein \u2013

      Path to the protein

    • ligand_files \u2013

      A list of ligand filenames.

    • ambertools_home \u2013

      Ambertools HOME path. If not configured, the env variable AMBERHOME as AMBER_PREFIX will be checked.

    • ambertools_antechamber \u2013

      Antechamber path based on the .ambertools_home

    • ambertools_parmchk2 \u2013

      Parmchk2 path based on the .ambertools_home

    • ambertools_tleap \u2013

      Tleap path based on the .ambertools_home

    • antechamber_dr \u2013

      Whether to use -dr setting when calling antechamber.

    • ligand_net_charge \u2013

      The ligand charge. If not provided, neutral charge is assumed.

    • coordinates_file \u2013

      A file from which coordinate can be taken.

    • atom_pair_q_atol \u2013

      It defines the maximum difference in charge

    • net_charge_threshold \u2013

      Defines how much the superimposed regions can, in total, differ in charge.

    • ignore_charges_completely \u2013

      Ignore the charges during the superimposition. Useful for debugging.

    • allow_disjoint_components \u2013

      Defines whether there might be multiple superimposed areas that are

    • use_element_in_superimposition \u2013

      Use element rather than the actual atom type for the superimposition

    • align_molecules_using_mcs \u2013

      After determining the maximum common substructure (MCS),

    • use_original_coor \u2013

      Antechamber when assigning charges can modify the charges slightly.

    • ligands_contain_q \u2013

      If not provided, it tries to deduce whether charges are provided.

    • superimposition_starting_pair \u2013

      Set a starting pair for the superimposition to narrow down the MCS search.

    • manually_matched_atom_pairs \u2013

      Either a list of pairs or a file with a list of pairs of atoms

    • manually_mismatched_pairs \u2013

      A path to a file with a list of a pairs that should be mismatched.

    • protein_ff \u2013

      The protein forcefield to be used by ambertools for the protein parameterisation.

    • md_engine \u2013

      The MD engine, with the supported values NAMD2.13, NAMD2.14, NAMD3 and OpenMM

    • ligand_ff \u2013

      The forcefield for the ligand.

    • ligand_ff_name \u2013

      Either GAFF or GAFF2

    • redistribute_q_over_unmatched \u2013

      The superimposed and matched atoms have every slightly different charges.

    • use_hybrid_single_dual_top \u2013

      Hybrid single dual topology (experimental). Currently not implemented.

    • ligand_tleap_in \u2013

      The name of the tleap input file for ambertools for the ligand.

    • complex_tleap_in \u2013

      The tleap input file for the complex.

    • prep_dir \u2013

      Path to the prep directory. Currently in the workdir

    • pair_morphfrcmods_dir \u2013

      Path to the .frcmod files for the morph.

    • pair_morphfrmocs_tests_dir \u2013

      Path to the location where a test is carried out with .frcmod

    • pair_unique_atom_names_dir \u2013

      Location of the morph files with unique filenames.

    • lig_unique_atom_names_dir \u2013

      Directory location for files with unique atom names.

    • lig_frcmod_dir \u2013

      Directory location with the .frcmod created for each ligand.

    • lig_acprep_dir \u2013

      Directory location where the .ac charges are converted into the .mol2 format.

    • lig_dir \u2013

      Directory location with the .mol2 files.

    Source code in ties/config.py
    def __init__(self, **kwargs):\n    # set the path to the scripts\n    self.code_root = pathlib.Path(os.path.dirname(__file__))\n\n    # scripts/input files,\n    # these are specific to the host\n    self.script_dir = self.code_root / 'scripts'\n    self.namd_script_dir = self.script_dir / 'namd'\n    self.ambertools_script_dir = self.script_dir / 'ambertools'\n    self.tleap_check_protein = self.ambertools_script_dir / 'check_prot.in'\n    self.vmd_vis_script = self.script_dir / 'vmd' / 'vis_morph.vmd'\n    self.vmd_vis_script_sh = self.script_dir / 'vmd' / 'vis_morph.sh'\n\n    self._workdir = None\n    self._antechamber_dr = False\n    self._ambertools_home = None\n\n    self._protein = None\n\n    self._ligand_net_charge = None\n    self._atom_pair_q_atol = 0.1\n    self._net_charge_threshold = 0.1\n    self._redistribute_q_over_unmatched = True\n    self._allow_disjoint_components = False\n    # use only the element in the superimposition rather than the specific atom type\n    self._use_element = False\n    self._use_element_in_superimposition = True\n    self.starting_pairs_heuristics = True\n    # weights in choosing the best MCS, the weighted sum of \"(1 - MCS fraction) and RMSD\".\n    self.weights = [1, 0.5]\n\n    # coordinates\n    self._align_molecules_using_mcs = False\n    self._use_original_coor = False\n    self._coordinates_file = None\n\n    self._ligand_files = set()\n    self._manually_matched_atom_pairs = None\n    self._manually_mismatched_pairs = None\n    self._ligands_contain_q = None\n\n    self._ligand_tleap_in = None\n    self._complex_tleap_in = None\n\n    self._superimposition_starting_pair = None\n\n    self._protein_ff = None\n    self._ligand_ff = 'leaprc.gaff'\n    self._ligand_ff_name = 'gaff'\n\n    # MD/NAMD production input file\n    self._md_engine = 'namd'\n    #default to modern CPU version\n    self.namd_version = '2.14'\n    self._lambda_rep_dir_tree = False\n\n    # experimental\n    self._use_hybrid_single_dual_top = False\n    self._ignore_charges_completely = False\n\n    self.ligands = None\n\n    # if True, do not allow ligands with the same ligand name\n    self.uses_cmd = False\n\n    # assign all the initial configuration values\n    self.set_configs(**kwargs)\n
    "},{"location":"api/config/#ties.Config.workdir","title":"workdir property writable","text":"
    workdir\n

    Working directory for antechamber calls. If None, a temporary directory in /tmp/ will be used.

    :return: Work dir :rtype: str

    "},{"location":"api/config/#ties.Config.protein","title":"protein property writable","text":"
    protein\n

    Path to the protein

    :return: Protein filename :rtype: str

    "},{"location":"api/config/#ties.Config.ligand_files","title":"ligand_files property writable","text":"
    ligand_files\n

    A list of ligand filenames. :return:

    "},{"location":"api/config/#ties.Config.ambertools_home","title":"ambertools_home property writable","text":"
    ambertools_home\n

    Ambertools HOME path. If not configured, the env variable AMBERHOME as AMBER_PREFIX will be checked.

    :return: ambertools path

    "},{"location":"api/config/#ties.Config.ambertools_antechamber","title":"ambertools_antechamber property","text":"
    ambertools_antechamber\n

    Antechamber path based on the .ambertools_home

    :return:

    "},{"location":"api/config/#ties.Config.ambertools_parmchk2","title":"ambertools_parmchk2 property","text":"
    ambertools_parmchk2\n

    Parmchk2 path based on the .ambertools_home :return:

    "},{"location":"api/config/#ties.Config.ambertools_tleap","title":"ambertools_tleap property","text":"
    ambertools_tleap\n

    Tleap path based on the .ambertools_home :return:

    "},{"location":"api/config/#ties.Config.antechamber_dr","title":"antechamber_dr property writable","text":"
    antechamber_dr\n

    Whether to use -dr setting when calling antechamber.

    :return:

    "},{"location":"api/config/#ties.Config.ligand_net_charge","title":"ligand_net_charge property writable","text":"
    ligand_net_charge\n

    The ligand charge. If not provided, neutral charge is assumed. The charge is necessary for calling antechamber (-nc).

    :return:

    "},{"location":"api/config/#ties.Config.coordinates_file","title":"coordinates_file property writable","text":"
    coordinates_file\n

    A file from which coordinate can be taken.

    :return:

    "},{"location":"api/config/#ties.Config.atom_pair_q_atol","title":"atom_pair_q_atol property writable","text":"
    atom_pair_q_atol\n

    It defines the maximum difference in charge between any two superimposed atoms a1 and a2. If the two atoms differ in charge more than this value, they will be unmatched and added to the alchemical regions.

    :return: default (0.1e) :rtype: float

    "},{"location":"api/config/#ties.Config.net_charge_threshold","title":"net_charge_threshold property writable","text":"
    net_charge_threshold\n

    Defines how much the superimposed regions can, in total, differ in charge. If the total exceeds the thresholds, atom pairs will be unmatched until the threshold is met.

    :return: default (0.1e) :rtype: float

    "},{"location":"api/config/#ties.Config.ignore_charges_completely","title":"ignore_charges_completely property writable","text":"
    ignore_charges_completely\n

    Ignore the charges during the superimposition. Useful for debugging. :return: default (False) :rtype: bool

    "},{"location":"api/config/#ties.Config.allow_disjoint_components","title":"allow_disjoint_components property writable","text":"
    allow_disjoint_components\n

    Defines whether there might be multiple superimposed areas that are separated by alchemical region.

    :return: default (False) :rtype: bool

    "},{"location":"api/config/#ties.Config.use_element_in_superimposition","title":"use_element_in_superimposition property writable","text":"
    use_element_in_superimposition\n

    Use element rather than the actual atom type for the superimposition during the joint-traversal of the two molecules.

    :return: default (False) :rtype: bool

    "},{"location":"api/config/#ties.Config.align_molecules_using_mcs","title":"align_molecules_using_mcs property writable","text":"
    align_molecules_using_mcs\n

    After determining the maximum common substructure (MCS), use it to align the coordinates of the second molecule to the first.

    :return: default (False) :rtype: bool

    "},{"location":"api/config/#ties.Config.use_original_coor","title":"use_original_coor property writable","text":"
    use_original_coor\n

    Antechamber when assigning charges can modify the charges slightly. If that's the case, use the original charges in order to correct this slight divergence in coordinates.

    :return: default (?) :rtype: bool

    "},{"location":"api/config/#ties.Config.ligands_contain_q","title":"ligands_contain_q property writable","text":"
    ligands_contain_q\n

    If not provided, it tries to deduce whether charges are provided. If all charges are set to 0, then it assumes that charges are not provided.

    If set to False explicitly, charges are ignored and computed again.

    :return: default (None) :rtype: bool

    "},{"location":"api/config/#ties.Config.superimposition_starting_pair","title":"superimposition_starting_pair property writable","text":"
    superimposition_starting_pair\n

    Set a starting pair for the superimposition to narrow down the MCS search. E.g. \"C2-C12\"

    :rtype: str

    "},{"location":"api/config/#ties.Config.manually_matched_atom_pairs","title":"manually_matched_atom_pairs property writable","text":"
    manually_matched_atom_pairs\n

    Either a list of pairs or a file with a list of pairs of atoms that should be superimposed/matched.

    :return:

    "},{"location":"api/config/#ties.Config.manually_mismatched_pairs","title":"manually_mismatched_pairs property writable","text":"
    manually_mismatched_pairs\n

    A path to a file with a list of a pairs that should be mismatched.

    "},{"location":"api/config/#ties.Config.protein_ff","title":"protein_ff property writable","text":"
    protein_ff\n

    The protein forcefield to be used by ambertools for the protein parameterisation.

    :return: default (leaprc.ff19SB) :rtype: string

    "},{"location":"api/config/#ties.Config.md_engine","title":"md_engine property writable","text":"
    md_engine\n

    The MD engine, with the supported values NAMD2.13, NAMD2.14, NAMD3 and OpenMM

    :return: NAMD2.13, NAMD2.14, NAMD3 and OpenMM :rtype: string

    "},{"location":"api/config/#ties.Config.ligand_ff","title":"ligand_ff property","text":"
    ligand_ff\n

    The forcefield for the ligand.

    "},{"location":"api/config/#ties.Config.ligand_ff_name","title":"ligand_ff_name property writable","text":"
    ligand_ff_name\n

    Either GAFF or GAFF2

    :return:

    "},{"location":"api/config/#ties.Config.redistribute_q_over_unmatched","title":"redistribute_q_over_unmatched property writable","text":"
    redistribute_q_over_unmatched\n

    The superimposed and matched atoms have every slightly different charges. Taking an average charge between any two atoms introduces imbalances in the net charge of the alchemical regions, due to the different charge distribution.

    :return: default(True)

    "},{"location":"api/config/#ties.Config.use_hybrid_single_dual_top","title":"use_hybrid_single_dual_top property writable","text":"
    use_hybrid_single_dual_top\n

    Hybrid single dual topology (experimental). Currently not implemented.

    :return: default(False).

    "},{"location":"api/config/#ties.Config.ligand_tleap_in","title":"ligand_tleap_in property","text":"
    ligand_tleap_in\n

    The name of the tleap input file for ambertools for the ligand.

    :return: Default ('leap_ligand.in') :rtype: string

    "},{"location":"api/config/#ties.Config.complex_tleap_in","title":"complex_tleap_in property","text":"
    complex_tleap_in\n

    The tleap input file for the complex.

    :return: Default 'leap_complex.in' :type: string

    "},{"location":"api/config/#ties.Config.prep_dir","title":"prep_dir property","text":"
    prep_dir\n

    Path to the prep directory. Currently in the workdir

    :return: Default (workdir/prep)

    "},{"location":"api/config/#ties.Config.pair_morphfrcmods_dir","title":"pair_morphfrcmods_dir property","text":"
    pair_morphfrcmods_dir\n

    Path to the .frcmod files for the morph.

    :return: Default (workdir/prep/morph_frcmods)

    "},{"location":"api/config/#ties.Config.pair_morphfrmocs_tests_dir","title":"pair_morphfrmocs_tests_dir property","text":"
    pair_morphfrmocs_tests_dir\n

    Path to the location where a test is carried out with .frcmod

    :return: Default (workdir/prep/morph_frcmods/tests)

    "},{"location":"api/config/#ties.Config.pair_unique_atom_names_dir","title":"pair_unique_atom_names_dir property","text":"
    pair_unique_atom_names_dir\n

    Location of the morph files with unique filenames.

    :return: Default (workdir/prep/morph_unique_atom_names)

    "},{"location":"api/config/#ties.Config.lig_unique_atom_names_dir","title":"lig_unique_atom_names_dir property","text":"
    lig_unique_atom_names_dir\n

    Directory location for files with unique atom names.

    :return: Default (workdir/prep/unique_atom_names)

    "},{"location":"api/config/#ties.Config.lig_frcmod_dir","title":"lig_frcmod_dir property","text":"
    lig_frcmod_dir\n

    Directory location with the .frcmod created for each ligand.

    :return: Default (workdir/prep/ligand_frcmods)

    "},{"location":"api/config/#ties.Config.lig_acprep_dir","title":"lig_acprep_dir property","text":"
    lig_acprep_dir\n

    Directory location where the .ac charges are converted into the .mol2 format.

    :return: Default (workdir/prep/acprep_to_mol2)

    "},{"location":"api/config/#ties.Config.lig_dir","title":"lig_dir property","text":"
    lig_dir\n

    Directory location with the .mol2 files.

    :return: Default (workdir/mol2)

    "},{"location":"api/config/#ties.Config._guess_ligands_contain_q","title":"_guess_ligands_contain_q","text":"
    _guess_ligands_contain_q()\n

    Checks if the first .mol2 file contains charges. :return:

    Source code in ties/config.py
    def _guess_ligands_contain_q(self):\n    \"\"\"\n    Checks if the first .mol2 file contains charges.\n    :return:\n    \"\"\"\n    # if all ligands are .mol2, then charges are provided\n    if all(l.suffix.lower() == '.mol2' for l in self.ligand_files):\n        # if all atoms have q = 0 that means they're a placeholder\n        u = parmed.load_file(str(list(self.ligand_files)[0]), structure=True)\n        all_q_0 = all(a.charge == 0 for a in u.atoms)\n        if all_q_0:\n            return False\n\n        return True\n\n    return False\n
    "},{"location":"api/config/#ties.Config._get_first_ligand_net_q","title":"_get_first_ligand_net_q","text":"
    _get_first_ligand_net_q()\n

    :return: Returns the net charge from the parmed file with partial charges.

    Source code in ties/config.py
    def _get_first_ligand_net_q(self):\n    \"\"\"\n    :return: Returns the net charge from the parmed file with partial charges.\n    \"\"\"\n\n    # if all atoms have q = 0 that means they're a placeholder\n    u = parmed.load_file(str(list(self.ligand_files)[0]), structure=True)\n    net_q = sum(a.charge == 0 for a in u.atoms)\n    return net_q\n
    "},{"location":"api/config/#ties.Config.get_element_map","title":"get_element_map staticmethod","text":"
    get_element_map()\n

    :return:

    Source code in ties/config.py
    @staticmethod\ndef get_element_map():\n    \"\"\"\n\n\n    :return:\n    \"\"\"\n    # Get the mapping of atom types to elements\n    element_map_filename = pathlib.Path(os.path.dirname(__file__)) / 'data' / 'element_atom_type_map.txt'\n    # remove the comments lines with #\n    lines = filter(lambda l: not l.strip().startswith('#') and not l.strip() == '', open(element_map_filename).readlines())\n    # convert into a dictionary\n\n    element_map = {}\n    for line in lines:\n        element, atom_types = line.split('=')\n\n        for atom_type in atom_types.split():\n            element_map[atom_type.strip()] = element.strip()\n\n    return element_map\n
    "},{"location":"api/config/#ties.Config.get_serializable","title":"get_serializable","text":"
    get_serializable()\n

    Get a JSON serializable structure of the config.

    pathlib.Path is not JSON serializable, so replace it with str

    todo - consider capturing all information about the system here, including each suptop.get_serializable() so that you can record specific information such as the charge changes etc.

    :return: Dictionary {key:value} with the settings :rtype: Dictionary

    Source code in ties/config.py
    def get_serializable(self):\n    \"\"\"\n    Get a JSON serializable structure of the config.\n\n    pathlib.Path is not JSON serializable, so replace it with str\n\n    todo - consider capturing all information about the system here,\n    including each suptop.get_serializable() so that you can record\n    specific information such as the charge changes etc.\n\n    :return: Dictionary {key:value} with the settings\n    :rtype: Dictionary\n    \"\"\"\n\n    host_specific = ['code_root', 'script_dir0', 'namd_script_dir',\n                     'ambertools_script_dir', 'tleap_check_protein', 'vmd_vis_script']\n\n    ser = {}\n    for k, v in self.__dict__.items():\n        if k in host_specific:\n            continue\n\n        if type(v) is pathlib.PosixPath:\n            v = str(v)\n\n        # account for the ligands being pathlib objects\n        if k == 'ligands' and v is not None:\n            # a list of ligands, convert to strings\n            v = [str(l) for l in v]\n        if k == '_ligand_files':\n            continue\n\n        ser[k] = v\n\n    return ser\n
    "},{"location":"api/ligand/","title":"Ligand","text":""},{"location":"api/ligand/#ties.Ligand","title":"Ligand","text":"
    Ligand(ligand, config=None, save=True)\n

    The ligand helper class. Helps to load and manage the different copies of the ligand file. Specifically, it tracks the different copies of the original input files as it is transformed (e.g. charge assignment).

    :param ligand: ligand filepath :type ligand: string :param config: Optional configuration from which the relevant ligand settings can be used :type config: :class:Config :param save: write a file with unique atom names for further inspection :type save: bool

    Methods:

    • convert_acprep_to_mol2 \u2013

      If the file is not a prep/ac file, this function does not do anything.

    • are_atom_names_correct \u2013

      Checks if atom names:

    • correct_atom_names \u2013

      Ensure that each atom name:

    • antechamber_prepare_mol2 \u2013

      Converts the ligand into a .mol2 format.

    • removeDU_atoms \u2013

      Ambertools antechamber creates sometimes DU dummy atoms.

    • generate_frcmod \u2013

      params

    • overwrite_coordinates_with \u2013

      Load coordinates from another file and overwrite the coordinates in the current file.

    Attributes:

    • renaming_map \u2013

      Otherwise, key: newName, value: oldName.

    Source code in ties/ligand.py
    def __init__(self, ligand, config=None, save=True):\n    \"\"\"Constructor method\n    \"\"\"\n\n    self.save = save\n    # save workplace root\n    self.config = Config() if config is None else config\n    self.config.ligand_files = ligand\n\n    self.original_input = Path(ligand).absolute()\n\n    # internal name without an extension\n    self.internal_name = self.original_input.stem\n\n    # ligand names have to be unique\n    if self.internal_name in Ligand._USED_FILENAMES and self.config.uses_cmd:\n        raise ValueError(f'ERROR: the ligand filename {self.internal_name} is not unique in the list of ligands. ')\n    else:\n        Ligand._USED_FILENAMES.add(self.internal_name)\n\n    # last used representative Path file\n    self.current = self.original_input\n\n    # internal index\n    # TODO - move to config\n    self.index = Ligand.LIG_COUNTER\n    Ligand.LIG_COUNTER += 1\n\n    self._renaming_map = None\n    self.ligand_with_uniq_atom_names = None\n\n    # If .ac format (ambertools, similar to .pdb), convert it to .mol2 using antechamber\n    self.convert_acprep_to_mol2()\n
    "},{"location":"api/ligand/#ties.Ligand.renaming_map","title":"renaming_map property writable","text":"
    renaming_map\n

    Otherwise, key: newName, value: oldName.

    If None, means no renaming took place.

    "},{"location":"api/ligand/#ties.Ligand.convert_acprep_to_mol2","title":"convert_acprep_to_mol2","text":"
    convert_acprep_to_mol2()\n

    If the file is not a prep/ac file, this function does not do anything. Antechamber is called to convert the .prepi/.prep/.ac file into a .mol2 file.

    Returns: the name of the original file, or of it was .prepi, a new filename with .mol2

    Source code in ties/ligand.py
    def convert_acprep_to_mol2(self):\n    \"\"\"\n    If the file is not a prep/ac file, this function does not do anything.\n    Antechamber is called to convert the .prepi/.prep/.ac file into a .mol2 file.\n\n    Returns: the name of the original file, or of it was .prepi, a new filename with .mol2\n    \"\"\"\n\n    if self.current.suffix.lower() not in ('.ac', '.prep'):\n        return\n\n    filetype = {'.ac': 'ac', '.prep': 'prepi'}[self.current.suffix.lower()]\n\n    cwd = self.config.lig_acprep_dir / self.internal_name\n    if not cwd.is_dir():\n        cwd.mkdir(parents=True, exist_ok=True)\n\n    # prepare the .mol2 files with antechamber (ambertools), assign BCC charges if necessary\n    logger.debug(f'Antechamber: converting {filetype} to mol2')\n    new_current = cwd / (self.internal_name + '.mol2')\n\n    log_filename = cwd / \"antechamber_conversion.log\"\n    with open(log_filename, 'w') as LOG:\n        try:\n            subprocess.run([self.config.ambertools_antechamber,\n                            '-i', self.current, '-fi', filetype,\n                            '-o', new_current, '-fo', 'mol2',\n                            '-dr', self.config.antechamber_dr],\n                           stdout=LOG, stderr=LOG,\n                           check=True, text=True,\n                           cwd=cwd, timeout=30)\n        except subprocess.CalledProcessError as E:\n            raise Exception('An error occurred during the antechamber conversion from .ac to .mol2 data type. '\n                            f'The output was saved in the directory: {cwd}'\n                            f'Please see the log file for the exact error information: {log_filename}') from E\n\n    # update\n    self.original_ac = self.current\n    self.current = new_current\n    logger.debug(f'Converted .ac file to .mol2. The location of the new file: {self.current}')\n
    "},{"location":"api/ligand/#ties.Ligand.are_atom_names_correct","title":"are_atom_names_correct","text":"
    are_atom_names_correct()\n
    Checks if atom names
    • are unique
    • have a correct format \"LettersNumbers\" e.g. C17
    Source code in ties/ligand.py
    def are_atom_names_correct(self):\n    \"\"\"\n    Checks if atom names:\n     - are unique\n     - have a correct format \"LettersNumbers\" e.g. C17\n    \"\"\"\n    ligand = parmed.load_file(str(self.current), structure=True)\n    atom_names = [a.name for a in ligand.atoms]\n\n    are_uniqe = len(set(atom_names)) == len(atom_names)\n\n    return are_uniqe and self._do_atom_names_have_correct_format(atom_names)\n
    "},{"location":"api/ligand/#ties.Ligand._do_atom_names_have_correct_format","title":"_do_atom_names_have_correct_format staticmethod","text":"
    _do_atom_names_have_correct_format(names)\n

    Check if the atom name is followed by a number, e.g. \"C15\" Note that the full atom name cannot be more than 4 characters. This is because the PDB format does not allow for more characters which can lead to inconsistencies.

    :param names: a list of atom names :type names: list[str] :return True if they all follow the correct format.

    Source code in ties/ligand.py
    @staticmethod\ndef _do_atom_names_have_correct_format(names):\n    \"\"\"\n    Check if the atom name is followed by a number, e.g. \"C15\"\n    Note that the full atom name cannot be more than 4 characters.\n    This is because the PDB format does not allow for more\n    characters which can lead to inconsistencies.\n\n    :param names: a list of atom names\n    :type names: list[str]\n    :return True if they all follow the correct format.\n    \"\"\"\n    for name in names:\n        # cannot exceed 4 characters\n        if len(name) > 4:\n            return False\n\n        # count letters before any digit\n        letter_count = 0\n        for letter in name:\n            if not letter.isalpha():\n                break\n\n            letter_count += 1\n\n        # at least one character\n        if letter_count == 0:\n            return False\n\n        # extrac the number suffix\n        atom_number = name[letter_count:]\n        try:\n            int(atom_number)\n        except:\n            return False\n\n    return True\n
    "},{"location":"api/ligand/#ties.Ligand.correct_atom_names","title":"correct_atom_names","text":"
    correct_atom_names()\n
    Ensure that each atom name
    • is unique
    • has letter followed by digits
    • has max 4 characters

    E.g. C17, NX23

    :param self.save: if the path is provided, the updated file will be saved with the unique names and a handle to the new file (ParmEd) will be returned.

    Source code in ties/ligand.py
    def correct_atom_names(self):\n    \"\"\"\n    Ensure that each atom name:\n     - is unique\n     - has letter followed by digits\n     - has max 4 characters\n    E.g. C17, NX23\n\n    :param self.save: if the path is provided, the updated file\n        will be saved with the unique names and a handle to the new file (ParmEd) will be returned.\n    \"\"\"\n    if self.are_atom_names_correct():\n        return\n\n    logger.debug(f'Ligand {self.internal_name} will have its atom names renamed. ')\n\n    ligand = parmed.load_file(str(self.current), structure=True)\n\n    logger.debug(f'Atom names in the molecule ({self.original_input}/{self.internal_name}) are either not unique '\n          f'or do not follow NameDigit format (e.g. C15). Renaming')\n    _, renaming_map = ties.helpers.get_new_atom_names(ligand.atoms)\n    self._renaming_map = renaming_map\n    logger.debug(f'Rename map: {renaming_map}')\n\n    # save the output here\n    os.makedirs(self.config.lig_unique_atom_names_dir, exist_ok=True)\n\n    ligand_with_uniq_atom_names = self.config.lig_unique_atom_names_dir / (self.internal_name + self.current.suffix)\n    if self.save:\n        ligand.save(str(ligand_with_uniq_atom_names))\n\n    self.ligand_with_uniq_atom_names = ligand_with_uniq_atom_names\n    self.parmed = ligand\n    # this object is now represented by the updated ligand\n    self.current = ligand_with_uniq_atom_names\n
    "},{"location":"api/ligand/#ties.Ligand.antechamber_prepare_mol2","title":"antechamber_prepare_mol2","text":"
    antechamber_prepare_mol2(**kwargs)\n

    Converts the ligand into a .mol2 format.

    BCC charges are generated if missing or requested. It calls antechamber (the charge type -c is not used if user prefers to use their charges). Any DU atoms created in the antechamber call are removed.

    :param atom_type: Atom type bla bla :type atom_type: :param net_charge: :type net_charge: int

    Source code in ties/ligand.py
    def antechamber_prepare_mol2(self, **kwargs):\n    \"\"\"\n    Converts the ligand into a .mol2 format.\n\n    BCC charges are generated if missing or requested.\n    It calls antechamber (the charge type -c is not used if user prefers to use their charges).\n    Any DU atoms created in the antechamber call are removed.\n\n    :param atom_type: Atom type bla bla\n    :type atom_type:\n    :param net_charge:\n    :type net_charge: int\n    \"\"\"\n    self.config.set_configs(**kwargs)\n\n    if self.config.ligands_contain_q or not self.config.antechamber_charge_type:\n        logger.info(f'Antechamber: User-provided atom charges will be reused ({self.current.name})')\n\n    mol2_cwd = self.config.lig_dir / self.internal_name\n\n    # prepare the directory\n    mol2_cwd.mkdir(parents=True, exist_ok=True)\n    mol2_target = mol2_cwd / f'{self.internal_name}.mol2'\n\n    # do not redo if the target file exists\n    if not (mol2_target).is_file():\n        log_filename = mol2_cwd / \"antechamber.log\"\n        with open(log_filename, 'w') as LOG:\n            try:\n                cmd = [self.config.ambertools_antechamber,\n                       '-i', self.current,\n                       '-fi', self.current.suffix[1:],\n                       '-o', mol2_target,\n                       '-fo', 'mol2',\n                       '-at', self.config.ligand_ff_name,\n                       '-nc', str(self.config.ligand_net_charge),\n                       '-dr', str(self.config.antechamber_dr)\n                       ] +  self.config.antechamber_charge_type\n                subprocess.run(cmd,\n                               cwd=mol2_cwd,\n                               stdout=LOG, stderr=LOG,\n                               check=True, text=True,\n                               timeout=60 * 30  # 30 minutes\n                               )\n            except subprocess.CalledProcessError as ProcessError:\n                raise Exception(f'Could not convert the ligand into .mol2 file with antechamber. '\n                                f'See the log and its directory: {log_filename} . '\n                                f'Command used: {\" \".join(map(str, cmd))}') from ProcessError\n        logger.debug(f'Converted {self.original_input} into .mol2, Log: {log_filename}')\n    else:\n        logger.info(f'File {mol2_target} already exists. Skipping. ')\n\n    self.antechamber_mol2 = mol2_target\n    self.current = mol2_target\n\n    # remove any DUMMY DU atoms in the .mol2 atoms\n    self.removeDU_atoms()\n
    "},{"location":"api/ligand/#ties.Ligand.removeDU_atoms","title":"removeDU_atoms","text":"
    removeDU_atoms()\n

    Ambertools antechamber creates sometimes DU dummy atoms. These are not created when BCC charges are computed from scratch. They are only created if you reuse existing charges. They appear to be a side effect. We remove the dummy atoms therefore.

    Source code in ties/ligand.py
    def removeDU_atoms(self):\n    \"\"\"\n    Ambertools antechamber creates sometimes DU dummy atoms.\n    These are not created when BCC charges are computed from scratch.\n    They are only created if you reuse existing charges.\n    They appear to be a side effect. We remove the dummy atoms therefore.\n    \"\"\"\n    mol2 = parmed.load_file(str(self.current), structure=True)\n    # check if there are any DU atoms\n    has_DU = any(a.type == 'DU' for a in mol2.atoms)\n    if not has_DU:\n        return\n\n    # make a backup copy before (to simplify naming)\n    shutil.move(self.current, self.current.parent / ('lig.beforeRemovingDU' + self.current.suffix))\n\n    # remove DU type atoms and save the file\n    for atom in mol2.atoms:\n        if atom.name != 'DU':\n            continue\n\n        atom.residue.delete_atom(atom)\n    # save the updated molecule\n    mol2.save(str(self.current))\n    logger.debug('Removed dummy atoms with type \"DU\"')\n
    "},{"location":"api/ligand/#ties.Ligand.generate_frcmod","title":"generate_frcmod","text":"
    generate_frcmod(**kwargs)\n

    params - parmchk2 - atom_type

    Source code in ties/ligand.py
    def generate_frcmod(self, **kwargs):\n    \"\"\"\n        params\n         - parmchk2\n         - atom_type\n    \"\"\"\n    self.config.set_configs(**kwargs)\n\n    logger.debug(f'INFO: frcmod for {self} was computed before. Not repeating.')\n    if hasattr(self, 'frcmod'):\n        return\n\n    # fixme - work on the file handles instaed of the constant stitching\n    logger.debug(f'Parmchk2: generate the .frcmod for {self.internal_name}.mol2')\n\n    # prepare cwd\n    cwd = self.config.lig_frcmod_dir / self.internal_name\n    if not cwd.is_dir():\n        cwd.mkdir(parents=True, exist_ok=True)\n\n    target_frcmod = f'{self.internal_name}.frcmod'\n    log_filename = cwd / \"parmchk2.log\"\n    with open(log_filename, 'w') as LOG:\n        try:\n            subprocess.run([self.config.ambertools_parmchk2,\n                            '-i', self.current,\n                            '-o', target_frcmod,\n                            '-f', 'mol2',\n                            '-s', self.config.ligand_ff_name],\n                           stdout=LOG, stderr=LOG,\n                           check= True, text=True,\n                           cwd= cwd, timeout=20,  # 20 seconds\n                            )\n        except subprocess.CalledProcessError as E:\n            raise Exception(f\"GAFF Error: Could not generate FRCMOD for file: {self.current} . \"\n                            f'See more here: {log_filename}') from E\n\n    logger.debug(f'Parmchk2: created frcmod: {target_frcmod}')\n    self.frcmod = cwd / target_frcmod\n
    "},{"location":"api/ligand/#ties.Ligand.overwrite_coordinates_with","title":"overwrite_coordinates_with","text":"
    overwrite_coordinates_with(file, output_file)\n

    Load coordinates from another file and overwrite the coordinates in the current file.

    Source code in ties/ligand.py
    def overwrite_coordinates_with(self, file, output_file):\n    \"\"\"\n    Load coordinates from another file and overwrite the coordinates in the current file.\n    \"\"\"\n\n    # load the current atoms with ParmEd\n    template = parmed.load_file(str(self.current), structure=True)\n\n    # load the file with the coordinates we want to use\n    coords = parmed.load_file(str(file), structure=True)\n\n    # fixme: use the atom names\n    by_atom_name = True\n    by_index = False\n    by_general_atom_type = False\n\n    # mol2_filename will be overwritten!\n    logger.info(f'Writing to {self.current} the coordinates from {file}. ')\n\n    coords_sum = np.sum(coords.atoms.positions)\n\n    if by_atom_name and by_index:\n        raise ValueError('Cannot have both. They are exclusive')\n    elif not by_atom_name and not by_index:\n        raise ValueError('Either option has to be selected.')\n\n    if by_general_atom_type:\n        for mol2_atom in template.atoms:\n            found_match = False\n            for ref_atom in coords.atoms:\n                if element_from_type[mol2_atom.type.upper()] == element_from_type[ref_atom.type.upper()]:\n                    found_match = True\n                    mol2_atom.position = ref_atom.position\n                    break\n            assert found_match, \"Could not find the following atom in the original file: \" + mol2_atom.name\n    if by_atom_name:\n        for mol2_atom in template.atoms:\n            found_match = False\n            for ref_atom in coords.atoms:\n                if mol2_atom.name.upper() == ref_atom.name.upper():\n                    found_match = True\n                    mol2_atom.position = ref_atom.position\n                    break\n            assert found_match, \"Could not find the following atom name across the two files: \" + mol2_atom.name\n    elif by_index:\n        for mol2_atom, ref_atom in zip(template.atoms, coords.atoms):\n            atype = element_from_type[mol2_atom.type.upper()]\n            reftype = element_from_type[ref_atom.type.upper()]\n            if atype != reftype:\n                raise Exception(\n                    f\"The found general type {atype} does not equal to the reference type {reftype} \")\n\n            mol2_atom.position = ref_atom.position\n\n    if np.testing.assert_almost_equal(coords_sum, np.sum(mda_template.atoms.positions), decimal=2):\n        logger.debug('Different positions sums:', coords_sum, np.sum(mda_template.atoms.positions))\n        raise Exception('Copying of the coordinates did not work correctly')\n\n    # save the output file\n    mda_template.atoms.write(output_file)\n
    "},{"location":"api/pair/","title":"Pair","text":""},{"location":"api/pair/#ties.Pair","title":"Pair","text":"
    Pair(ligA, ligZ, config=None, **kwargs)\n

    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)\n

    Methods:

    • superimpose \u2013

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

    • set_suptop \u2013

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

    • make_atom_names_unique \u2013

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

    • check_json_file \u2013

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

    • merge_frcmod_files \u2013

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

    • overlap_fractions \u2013

      Calculate the size of the common area.

    Source code in ties/pair.py
    def __init__(self, ligA, ligZ, config=None, **kwargs):\n    \"\"\"\n    Please use the Config class for the documentation of the possible kwargs.\n    Each kwarg is passed to the config class.\n\n    fixme - list all relevant kwargs here\n\n        param ligand_net_charge: integer, net charge of each ligand (has to be the same)\n    \"\"\"\n\n    # create a new config if it is not provided\n    self.config = ties.config.Config() if config is None else config\n\n    # channel all config variables to the config class\n    self.config.set_configs(**kwargs)\n\n    # tell Config about the ligands if necessary\n    if self.config.ligands is None:\n        self.config.ligands = [ligA, ligZ]\n\n    # create ligands if they're just paths\n    if isinstance(ligA, ties.ligand.Ligand):\n        self.ligA = ligA\n    else:\n        self.ligA = ties.ligand.Ligand(ligA, self.config)\n\n    if isinstance(ligZ, ties.ligand.Ligand):\n        self.ligZ = ligZ\n    else:\n        self.ligZ = ties.ligand.Ligand(ligZ, self.config)\n\n    # initialise the handles to the molecules that morph\n    self.current_ligA = self.ligA.current\n    self.current_ligZ = self.ligZ.current\n\n    self.internal_name = f'{self.ligA.internal_name}_{self.ligZ.internal_name}'\n    self.mol2 = None\n    self.pdb = None\n    self.summary = None\n    self.suptop = None\n    self.mda_l1 = None\n    self.mda_l2 = None\n    self.distance = None\n
    "},{"location":"api/pair/#ties.Pair.superimpose","title":"superimpose","text":"
    superimpose(**kwargs)\n

    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
    def superimpose(self, **kwargs):\n    \"\"\"\n    Please see :class:`Config` class for the documentation of kwargs. The passed kwargs overwrite the config\n    object passed in the constructor.\n\n    fixme - list all relevant kwargs here\n\n    :param use_element_in_superimposition: bool whether the superimposition should rely on the element initially,\n        before refining the results with a more specific check of the atom type.\n    :param manually_matched_atom_pairs:\n    :param manually_mismatched_pairs:\n    :param redistribute_q_over_unmatched:\n    \"\"\"\n    self.config.set_configs(**kwargs)\n\n    # use ParmEd to load the files\n    # fixme - move this to the Morph class instead of this place,\n    # fixme - should not squash all messsages. For example, wrong type file should not be squashed\n    leftlig_atoms, leftlig_bonds, rightlig_atoms, rightlig_bonds, parmed_ligA, parmed_ligZ = \\\n        get_atoms_bonds_from_mol2(self.current_ligA, self.current_ligZ,\n                                  use_general_type=self.config.use_element_in_superimposition)\n    # fixme - manual match should be improved here and allow for a sensible format.\n\n    # in case the atoms were renamed, pass the names via the map renaming map\n    # TODO\n    # ligZ_old_new_atomname_map\n    new_mismatch_names = []\n    for a, z in self.config.manually_mismatched_pairs:\n        new_names = (self.ligA.rev_renaming_map[a], self.ligZ.rev_renaming_map[z])\n        logger.debug(f'Selecting mismatching atoms. The mismatch {(a, z)}) was renamed to {new_names}')\n        new_mismatch_names.append(new_names)\n\n    # assign\n    # fixme - Ideally I would reuse the ParmEd data for this,\n    # ParmEd can use bonds if they are present - fixme\n    # map atom IDs to their objects\n    ligand1_nodes = {}\n    for atomNode in leftlig_atoms:\n        ligand1_nodes[atomNode.id] = atomNode\n    # link them together\n    for nfrom, nto, btype in leftlig_bonds:\n        ligand1_nodes[nfrom].bind_to(ligand1_nodes[nto], btype)\n\n    ligand2_nodes = {}\n    for atomNode in rightlig_atoms:\n        ligand2_nodes[atomNode.id] = atomNode\n    for nfrom, nto, btype in rightlig_bonds:\n        ligand2_nodes[nfrom].bind_to(ligand2_nodes[nto], btype)\n\n    # fixme - this should be moved out of here,\n    #  ideally there would be a function in the main interface for this\n    manual_match = [] if self.config.manually_matched_atom_pairs is None else self.config.manually_matched_atom_pairs\n    starting_node_pairs = []\n    for l_aname, r_aname in manual_match:\n        # find the starting node pairs, ie the manually matched pair(s)\n        found_left_node = None\n        for id, ln in ligand1_nodes.items():\n            if l_aname == ln.name:\n                found_left_node = ln\n        if found_left_node is None:\n            raise ValueError(f'Manual Matching: could not find an atom name: \"{l_aname}\" in the left molecule')\n\n        found_right_node = None\n        for id, ln in ligand2_nodes.items():\n            if r_aname == ln.name:\n                found_right_node = ln\n        if found_right_node is None:\n            raise ValueError(f'Manual Matching: could not find an atom name: \"{r_aname}\" in the right molecule')\n\n        starting_node_pairs.append([found_left_node, found_right_node])\n\n    if starting_node_pairs:\n        logger.debug(f'Starting nodes will be used: {starting_node_pairs}')\n\n    # fixme - simplify to only take the ParmEd as input\n    suptop = superimpose_topologies(ligand1_nodes.values(), ligand2_nodes.values(),\n                                     disjoint_components=self.config.allow_disjoint_components,\n                                     net_charge_filter=True,\n                                     pair_charge_atol=self.config.atom_pair_q_atol,\n                                     net_charge_threshold=self.config.net_charge_threshold,\n                                     redistribute_charges_over_unmatched=self.config.redistribute_q_over_unmatched,\n                                     ignore_charges_completely=self.config.ignore_charges_completely,\n                                     ignore_bond_types=True,\n                                     ignore_coords=False,\n                                     align_molecules=self.config.align_molecules_using_mcs,\n                                     use_general_type=self.config.use_element_in_superimposition,\n                                     # fixme - not the same ... use_element_in_superimposition,\n                                     use_only_element=False,\n                                     check_atom_names_unique=True,  # fixme - remove?\n                                     starting_pairs_heuristics=self.config.starting_pairs_heuristics,  # fixme - add to config\n                                     force_mismatch=new_mismatch_names,\n                                     starting_node_pairs=starting_node_pairs,\n                                     parmed_ligA=parmed_ligA, parmed_ligZ=parmed_ligZ,\n                                     starting_pair_seed=self.config.superimposition_starting_pair,\n                                     config=self.config)\n\n    self.set_suptop(suptop, parmed_ligA, parmed_ligZ)\n    # attach the used config to the suptop\n\n    if suptop is not None:\n        suptop.config = self.config\n        # attach the morph to the suptop\n        suptop.morph = self\n\n    return suptop\n
    "},{"location":"api/pair/#ties.Pair.set_suptop","title":"set_suptop","text":"
    set_suptop(suptop, parmed_ligA, parmed_ligZ)\n

    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
    def set_suptop(self, suptop, parmed_ligA, parmed_ligZ):\n    \"\"\"\n    Attach a SuperimposedTopology object along with the ParmEd objects for the ligA and ligZ.\n\n    :param suptop: :class:`SuperimposedTopology`\n    :param parmed_ligA: An ParmEd for the ligA\n    :param parmed_ligZ: An ParmEd for the ligZ\n    \"\"\"\n    self.suptop = suptop\n    self.parmed_ligA = parmed_ligA\n    self.parmed_ligZ = parmed_ligZ\n
    "},{"location":"api/pair/#ties.Pair.make_atom_names_unique","title":"make_atom_names_unique","text":"
    make_atom_names_unique(out_ligA_filename=None, out_ligZ_filename=None, save=True)\n

    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
    def make_atom_names_unique(self, out_ligA_filename=None, out_ligZ_filename=None, save=True):\n    \"\"\"\n    Ensure that each that atoms across the two ligands have unique names.\n\n    While renaming atoms, start with the element (C, N, ..) followed by\n     the count so far (e.g. C1, C2, N1).\n\n    Resnames are set to \"INI\" and \"FIN\", this is useful for the hybrid dual topology.\n\n    :param out_ligA_filename: The new filenames for the ligands with renamed atoms. If None, the default\n        naming convention is used.\n    :type out_ligA_filename: string or bool\n    :param out_ligZ_filename: The new filenames for the ligands with renamed atoms. If None, the default\n        naming convention is used.\n    :type out_ligZ_filename: string or bool\n    :param save: Whether to save to the disk the ligands after renaming the atoms\n    :type save: bool\n    \"\"\"\n\n    # The A ligand is a template for the renaming\n    self.ligA.correct_atom_names()\n\n    # load both ligands\n    left = parmed.load_file(str(self.ligA.current), structure=True)\n    right = parmed.load_file(str(self.ligZ.current), structure=True)\n\n    common_atom_names = {a.name for a in right.atoms}.intersection({a.name for a in left.atoms})\n    atom_names_overlap = len(common_atom_names) > 0\n\n    if atom_names_overlap or not self.ligZ.are_atom_names_correct():\n        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. ')\n        if atom_names_overlap:\n            logger.debug(f'Common atom names: {common_atom_names}')\n        name_counter_L_nodes = ties.helpers.get_atom_names_counter(left.atoms)\n        _, renaming_map = ties.helpers.get_new_atom_names(right.atoms, name_counter=name_counter_L_nodes)\n        self.ligZ.renaming_map = renaming_map\n\n    # rename the residue names to INI and FIN\n    for atom in left.atoms:\n        atom.residue = 'INI'\n    for atom in right.atoms:\n        atom.residue = 'FIN'\n\n    # fixme - instead of using the save parameter, have a method pair.save(filename1, filename2) and\n    #  call it when necessary.\n    # prepare the destination directory\n    if not save:\n        return\n\n    if out_ligA_filename is None:\n        cwd = self.config.pair_unique_atom_names_dir / f'{self.ligA.internal_name}_{self.ligZ.internal_name}'\n        cwd.mkdir(parents=True, exist_ok=True)\n\n        self.current_ligA = cwd / (self.ligA.internal_name + '.mol2')\n        self.current_ligZ = cwd / (self.ligZ.internal_name + '.mol2')\n    else:\n        self.current_ligA = out_ligA_filename\n        self.current_ligZ = out_ligZ_filename\n\n    # save the updated atom names\n    left.save(str(self.current_ligA))\n    right.save(str(self.current_ligZ))\n
    "},{"location":"api/pair/#ties.Pair.check_json_file","title":"check_json_file","text":"
    check_json_file()\n

    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
    def check_json_file(self):\n    \"\"\"\n    Performance optimisation in case TIES is rerun again. Return the first matched atoms which\n    can be used as a seed for the superimposition.\n\n    :return: If the superimposition was computed before, and the .json file is available,\n        gets one of the matched atoms.\n    :rtype: [(ligA_atom, ligZ_atom)]\n    \"\"\"\n    matching_json = self.config.workdir / f'fep_{self.ligA.internal_name}_{self.ligZ.internal_name}.json'\n    if not matching_json.is_file():\n        return None\n\n    return [list(json.load(matching_json.open())['matched'].items())[0]]\n
    "},{"location":"api/pair/#ties.Pair.merge_frcmod_files","title":"merge_frcmod_files","text":"
    merge_frcmod_files(ligcom=None)\n

    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
    def merge_frcmod_files(self, ligcom=None):\n    \"\"\"\n    Merges the .frcmod files generated for each ligand separately, simply by adding them together.\n\n    The duplication has no effect on the final generated topology parm7 top file.\n\n    We are also testing the .frcmod here with the user's force field in order to check if\n    the merge works correctly.\n\n    :param ligcom: Either \"lig\" if only ligands are present, or \"com\" if the complex is present.\n        Helps with the directory structure.\n    :type ligcom: string \"lig\" or \"com\"\n    \"\"\"\n    ambertools_tleap = self.config.ambertools_tleap\n    ambertools_script_dir = self.config.ambertools_script_dir\n    if self.config.protein is None:\n        protein_ff = None\n    else:\n        protein_ff = self.config.protein_ff\n\n    ligand_ff = self.config.ligand_ff\n\n    frcmod_info1 = ties.helpers.parse_frcmod_sections(self.ligA.frcmod)\n    frcmod_info2 = ties.helpers.parse_frcmod_sections(self.ligZ.frcmod)\n\n    cwd = self.config.workdir\n\n    # fixme: use the provided cwd here, otherwise this will not work if the wrong cwd is used\n    # have some conf module instead of this\n    if ligcom:\n        morph_frcmod = cwd / f'ties-{self.ligA.internal_name}-{self.ligZ.internal_name}' / ligcom / 'build' / 'hybrid.frcmod'\n    else:\n        # fixme - clean up\n        morph_frcmod = cwd / f'ties-{self.ligA.internal_name}-{self.ligZ.internal_name}' / 'build' / 'hybrid.frcmod'\n    morph_frcmod.parent.mkdir(parents=True, exist_ok=True)\n    with open(morph_frcmod, 'w') as FOUT:\n        FOUT.write('merged frcmod\\n')\n\n        for section in ['MASS', 'BOND', 'ANGLE',\n                        'DIHE', 'IMPROPER', 'NONBON']:\n            section_lines = frcmod_info1[section] + frcmod_info2[section]\n            FOUT.write('{0:s}\\n'.format(section))\n            for line in section_lines:\n                FOUT.write('{0:s}'.format(line))\n            FOUT.write('\\n')\n\n        FOUT.write('\\n\\n')\n\n    # this is our current frcmod file\n    self.frcmod = morph_frcmod\n\n    # as part of the .frcmod writing\n    # insert dummy angles/dihedrals if a morph .frcmod requires\n    # new terms between the appearing/disappearing atoms\n    # this is a trick to make sure tleap has everything it needs to generate the .top file\n    correction_introduced = self._check_hybrid_frcmod(ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff)\n    if correction_introduced:\n        # move the .frcmod which turned out to be insufficient according to the test\n        shutil.move(morph_frcmod, str(self.frcmod) + '.uncorrected' )\n        # now copy in place the corrected version\n        shutil.copy(self.frcmod, morph_frcmod)\n
    "},{"location":"api/pair/#ties.Pair._check_hybrid_frcmod","title":"_check_hybrid_frcmod","text":"
    _check_hybrid_frcmod(ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff)\n

    Check that the output library can be used to create a valid amber topology. Add missing terms with no force to pass the topology creation. Returns the corrected .frcmod content, otherwise throws an exception.

    Source code in ties/pair.py
    def _check_hybrid_frcmod(self, ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff):\n    \"\"\"\n    Check that the output library can be used to create a valid amber topology.\n    Add missing terms with no force to pass the topology creation.\n    Returns the corrected .frcmod content, otherwise throws an exception.\n    \"\"\"\n    # prepare the working directory\n    cwd = self.config.pair_morphfrmocs_tests_dir / self.internal_name\n    if not cwd.is_dir():\n        cwd.mkdir(parents=True, exist_ok=True)\n\n    if protein_ff is None:\n        protein_ff = '# no protein ff needed'\n    else:\n        protein_ff = 'source ' + protein_ff\n\n    # prepare the superimposed .mol2 file if needed\n    if not hasattr(self.suptop, 'mol2'):\n        self.suptop.write_mol2()\n\n    # prepare tleap input\n    leap_in_test = 'leap_test_morph.in'\n    leap_in_conf = open(ambertools_script_dir / leap_in_test).read()\n    open(cwd / leap_in_test, 'w').write(leap_in_conf.format(\n                            mol2=os.path.relpath(self.suptop.mol2, cwd),\n                            frcmod=os.path.relpath(self.frcmod, cwd),\n                            protein_ff=protein_ff, ligand_ff=ligand_ff))\n\n    # attempt generating the .top\n    logger.debug('Create amber7 topology .top')\n    try:\n        tleap_process = subprocess.run([ambertools_tleap, '-s', '-f', leap_in_test],\n                                       cwd=cwd, text=True, timeout=20,\n                                       capture_output=True, check=True)\n    except subprocess.CalledProcessError as err:\n        raise Exception(\n            f'ERROR: Testing the topology with tleap broke. Return code: {err.returncode} '\n            f'ERROR: Ambertools output: {err.stdout}') from err\n\n    # save stdout and stderr\n    open(cwd / 'tleap_scan_check.log', 'w').write(tleap_process.stdout + tleap_process.stderr)\n\n    if 'Errors = 0' in tleap_process.stdout:\n        logger.debug('Test hybrid .frcmod: OK, no dummy angle/dihedrals inserted.')\n        return False\n\n    # extract the missing angles/dihedrals\n    missing_bonds = set()\n    missing_angles = []\n    missing_dihedrals = []\n    for line in tleap_process.stdout.splitlines():\n        if \"Could not find bond parameter for:\" in line:\n            bond = line.split(':')[-1].strip()\n            missing_bonds.add(bond)\n        elif \"Could not find angle parameter:\" in line or \\\n                \"Could not find angle parameter for atom types:\" in line:\n            cols = line.split(':')\n            angle = cols[-1].strip()\n            if angle not in missing_angles:\n                missing_angles.append(angle)\n        elif \"No torsion terms for\" in line:\n            cols = line.split()\n            torsion = cols[-1].strip()\n            if torsion not in missing_dihedrals:\n                missing_dihedrals.append(torsion)\n\n    modified_hybrid_frcmod = cwd / f'{self.internal_name}_corrected.frcmod'\n    if missing_angles or missing_dihedrals:\n        logger.debug('Adding dummy bonds+angles+dihedrals to frcmod to generate .top')\n        # read the original frcmod\n        frcmod_lines = open(self.frcmod).readlines()\n        # overwriting the .frcmod with dummy angles/dihedrals\n        with open(modified_hybrid_frcmod, 'w') as NEW_FRCMOD:\n            for line in frcmod_lines:\n                NEW_FRCMOD.write(line)\n                if 'BOND' in line:\n                    for bond  in missing_bonds:\n                        dummy_bond = f'{bond:<14}0  180  \\t\\t# Dummy bond\\n'\n                        NEW_FRCMOD.write(dummy_bond)\n                        logger.debug(f'Added dummy bond: \"{dummy_bond}\"')\n                if 'ANGLE' in line:\n                    for angle in missing_angles:\n                        dummy_angle = f'{angle:<14}0  120.010  \\t\\t# Dummy angle\\n'\n                        NEW_FRCMOD.write(dummy_angle)\n                        logger.debug(f'Added dummy angle: \"{dummy_angle}\"')\n                if 'DIHE' in line:\n                    for dihedral in missing_dihedrals:\n                        dummy_dihedral = f'{dihedral:<14}1  0.00  180.000  2.000   \\t\\t# Dummy dihedrals\\n'\n                        NEW_FRCMOD.write(dummy_dihedral)\n                        logger.debug(f'Added dummy dihedral: \"{dummy_dihedral}\"')\n\n        # update our tleap input test to use the corrected file\n        leap_in_test_corrected = cwd / 'leap_test_morph_corrected.in'\n        open(leap_in_test_corrected, 'w').write(leap_in_conf.format(\n                            mol2=os.path.relpath(self.suptop.mol2, cwd),\n                            frcmod=os.path.relpath(modified_hybrid_frcmod, cwd),\n                            protein_ff=protein_ff, ligand_ff=ligand_ff))\n\n        # verify that adding the dummy angles/dihedrals worked\n        tleap_process = subprocess.run([ambertools_tleap, '-s', '-f', leap_in_test_corrected],\n                                       cwd=cwd, text=True, timeout=60 * 10, capture_output=True, check=True)\n\n        if not \"Errors = 0\" in tleap_process.stdout:\n            raise Exception('ERROR: Could not generate the .top file after adding dummy angles/dihedrals')\n\n\n    logger.debug('Morph .frcmod after the insertion of dummy angle/dihedrals: OK')\n    # set this .frcmod as the correct one now,\n    self.frcmod_before_correction = self.frcmod\n    self.frcmod = modified_hybrid_frcmod\n    return True\n
    "},{"location":"api/pair/#ties.Pair.overlap_fractions","title":"overlap_fractions","text":"
    overlap_fractions()\n

    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
    def overlap_fractions(self):\n    \"\"\"\n    Calculate the size of the common area.\n\n    :return: Four decimals capturing: 1) the fraction of the common size with respect to the ligA topology,\n        2) the fraction of the common size with respect to the ligZ topology,\n        3) the percentage of the disappearing atoms in the disappearing molecule\n        4) the percentage of the appearing atoms  in the appearing molecule\n    :rtype: [float, float, float, float]\n    \"\"\"\n\n    if self.suptop is None:\n        return 0, 0, float('inf'), float('inf')\n    else:\n        mcs_size = len(self.suptop.matched_pairs)\n\n    matched_fraction_left = mcs_size / float(len(self.suptop.top1))\n    matched_fraction_right = mcs_size / float(len(self.suptop.top2))\n    disappearing_atoms_fraction = (len(self.suptop.top1) - mcs_size) \\\n                               / float(len(self.suptop.top1)) * 100\n    appearing_atoms_fraction = (len(self.suptop.top2) - mcs_size) \\\n                               / float(len(self.suptop.top2)) * 100\n\n    return matched_fraction_left, matched_fraction_right, disappearing_atoms_fraction, appearing_atoms_fraction\n
    "},{"location":"reference/","title":"Index","text":""},{"location":"reference/#ties","title":"ties","text":"

    Modules:

    • analysis \u2013

      Updated OOP approach to data analysis.

    • cli \u2013

      Exposes a terminal interface to TIES 20.

    • config \u2013
    • generator \u2013
    • helpers \u2013

      A list of functions with a clear purpose that does

    • ligand \u2013
    • ligandmap \u2013
    • md \u2013
    • namd_generator \u2013

      Load two ligands, run the topology superimposer, and then

    • pair \u2013
    • protein \u2013
    • scripts \u2013
    • topology_superimposer \u2013

      The main module responsible for the superimposition.

    "},{"location":"reference/SUMMARY/","title":"SUMMARY","text":"
    • ties
      • analysis
      • cli
      • config
      • generator
      • helpers
      • ligand
      • ligandmap
      • md
      • namd_generator
      • pair
      • protein
      • topology_superimposer
    "},{"location":"reference/analysis/","title":" analysis","text":""},{"location":"reference/analysis/#ties.analysis","title":"analysis","text":"

    Updated OOP approach to data analysis.

    Classes:

    • Replica \u2013

      Replica loads in and parses the actual information.

    • Lambda \u2013

      Reflect the real lambda rather than the \"general\" lambda.

    • Contribution \u2013

      Reflects one type of interactions. For example, appearing electrostatics, or dissapearing VDW.

    • DGSystem \u2013

      Single step dG system.

    • TCSystem \u2013

      Thermodynamics Cycle System.

    "},{"location":"reference/analysis/#ties.analysis.Replica","title":"Replica","text":"

    Replica loads in and parses the actual information. Multiple replicas can work on the lambda. So replica is defined by lambda and by its directory path. This representation should contain all the details necessary.

    "},{"location":"reference/analysis/#ties.analysis.Lambda","title":"Lambda","text":"

    Reflect the real lambda rather than the \"general\" lambda. However, stores the information about the \"general\" lambda as well. This class contains at least 1 replica.

    "},{"location":"reference/analysis/#ties.analysis.Contribution","title":"Contribution","text":"

    Reflects one type of interactions. For example, appearing electrostatics, or dissapearing VDW. This class contains lambdas with their replicas. It can calculate the integral and plot different information relevant to each contribution.

    "},{"location":"reference/analysis/#ties.analysis.DGSystem","title":"DGSystem","text":"

    Single step dG system.

    Contains 4 contributions: disappearing and appearing electrostatics and vdw Uses contributions to calculate dG. Contains lots of dG analysis and plotting.

    "},{"location":"reference/analysis/#ties.analysis.TCSystem","title":"TCSystem","text":"

    Thermodynamics Cycle System.

    Contains 2 Systems, each providing one dG. This way it can provide the ddG. Contains lots of ddG analysis and plotting.

    "},{"location":"reference/cli/","title":" cli","text":""},{"location":"reference/cli/#ties.cli","title":"cli","text":"

    Exposes a terminal interface to TIES 20.

    Classes:

    • ArgparseChecker \u2013

    Functions:

    • get_new_atom_names \u2013

      todo - add unit tests

    • get_atom_names_counter \u2013

      name_counter: a dictionary with atom as the key such as 'N', 'C', etc,

    • parse_frcmod_sections \u2013

      Copied from the previous TIES. It's simpler and this approach must be fine then.

    "},{"location":"reference/cli/#ties.cli.ArgparseChecker","title":"ArgparseChecker","text":"

    Methods:

    • str2bool \u2013

      ArgumentParser tool to figure out the bool value

    • logging_lvl \u2013

      ArgumentParser tool to figure out the bool value

    "},{"location":"reference/cli/#ties.cli.ArgparseChecker.str2bool","title":"str2bool staticmethod","text":"
    str2bool(v)\n

    ArgumentParser tool to figure out the bool value

    Source code in ties/helpers.py
    @staticmethod\ndef str2bool(v):\n    \"ArgumentParser tool to figure out the bool value\"\n    if isinstance(v, bool):\n        return v\n    if v.lower() in ('yes', 'true', 't', 'y', '1'):\n        return True\n    elif v.lower() in ('no', 'false', 'f', 'n', '0'):\n        return False\n    else:\n        raise argparse.ArgumentTypeError('Boolean value expected.')\n
    "},{"location":"reference/cli/#ties.cli.ArgparseChecker.logging_lvl","title":"logging_lvl staticmethod","text":"
    logging_lvl(v)\n

    ArgumentParser tool to figure out the bool value

    Source code in ties/helpers.py
    @staticmethod\ndef logging_lvl(v):\n    \"ArgumentParser tool to figure out the bool value\"\n    logging_levels = {\n        'NOTSET': logging.NOTSET,\n          'DEBUG': logging.DEBUG,\n          'INFO': logging.INFO,\n          'WARNING': logging.WARNING,\n          'ERROR': logging.ERROR,\n          'CRITICAL': logging.CRITICAL,\n          # extras\n           \"ALL\": logging.INFO,\n           \"FALSE\": logging.ERROR\n                      }\n\n    if isinstance(v, bool) and v is True:\n        return logging.WARNING\n    elif isinstance(v, bool) and v is False:\n        # effectively we disable logging until an error happens\n        return logging.ERROR\n    elif v.upper() in logging_levels:\n        return logging_levels[v.upper()]\n    else:\n        raise argparse.ArgumentTypeError('Meaningful logging level expected.')\n
    "},{"location":"reference/cli/#ties.cli.get_new_atom_names","title":"get_new_atom_names","text":"
    get_new_atom_names(atoms, name_counter=None)\n

    todo - add unit tests

    @parameter/returns name_counter: a dictionary with atom as the key such as 'N', 'C', etc, the counter keeps track of the last used counter for each name. Empty means that the counting will start from 1. input atoms: mdanalysis atoms

    Source code in ties/helpers.py
    def get_new_atom_names(atoms, name_counter=None):\n    \"\"\"\n    todo - add unit tests\n\n    @parameter/returns name_counter: a dictionary with atom as the key such as 'N', 'C', etc,\n    the counter keeps track of the last used counter for each name.\n    Empty means that the counting will start from 1.\n    input atoms: mdanalysis atoms\n    \"\"\"\n    if name_counter is None:\n        name_counter = {}\n\n    # {new_uniqe_name: old_atom_name}\n    reverse_renaming_map = {}\n\n    for atom in atoms:\n        # count the letters before any digit\n        letter_count = 0\n        for letter in atom.name:\n            if not letter.isalpha():\n                break\n\n            letter_count += 1\n\n        # use max 3 letters from the atom name\n        letter_count = min(letter_count, 3)\n\n        letters = atom.name[:letter_count]\n\n        # how many atoms do we have with these letters? ie C1, C2, C3 -> 3\n        last_used_counter = name_counter.get(letters, 0) + 1\n\n        # rename\n        new_name = letters + str(last_used_counter)\n\n        # if the name is longer than 4 character,\n        # shorten the number of letters\n        if len(new_name) > 4:\n            # the name is too long, use only the first character\n            new_name = letters[:4-len(str(last_used_counter))] + str(last_used_counter)\n\n            # we assume that there is fewer than 1000 atoms with that name\n            assert len(str(last_used_counter)) < 1000\n\n        reverse_renaming_map[new_name] = atom.name\n\n        atom.name = new_name\n\n        # update the counter\n        name_counter[letters] = last_used_counter\n\n    return name_counter, reverse_renaming_map\n
    "},{"location":"reference/cli/#ties.cli.get_atom_names_counter","title":"get_atom_names_counter","text":"
    get_atom_names_counter(atoms)\n

    name_counter: a dictionary with atom as the key such as 'N', 'C', etc, the counter keeps track of the last used counter for each name. Ie if there are C1, C2, C3, this will return {'C':3} as the last counter.

    Source code in ties/helpers.py
    def get_atom_names_counter(atoms):\n    \"\"\"\n    name_counter: a dictionary with atom as the key such as 'N', 'C', etc,\n    the counter keeps track of the last used counter for each name.\n    Ie if there are C1, C2, C3, this will return {'C':3} as the last counter.\n    \"\"\"\n    name_counter = {}\n\n    for atom in atoms:\n        # get the first letters that is not a character\n        afterLetters = [i for i, l in enumerate(atom.name) if l.isalpha()][-1] + 1\n\n        atom_name = atom.name[:afterLetters]\n        atom_number = int(atom.name[afterLetters:])\n\n        # we are starting the counter from 0 as we always add 1 later on\n        last_used_counter = name_counter.get(atom_name, 0)\n\n        # update the counter\n        name_counter[atom_name] = max(last_used_counter + 1, atom_number)\n\n    return name_counter\n
    "},{"location":"reference/cli/#ties.cli.parse_frcmod_sections","title":"parse_frcmod_sections","text":"
    parse_frcmod_sections(filename)\n

    Copied from the previous TIES. It's simpler and this approach must be fine then.

    Source code in ties/helpers.py
    def parse_frcmod_sections(filename):\n    \"\"\"\n    Copied from the previous TIES. It's simpler and this approach must be fine then.\n    \"\"\"\n    frcmod_info = {}\n    section = 'REMARK'\n\n    with open(filename) as F:\n        for line in F:\n            start_line = line[0:9].strip()\n\n            if start_line in ['MASS', 'BOND', 'IMPROPER',\n                              'NONBON', 'ANGLE', 'DIHE']:\n                section = start_line\n                frcmod_info[section] = []\n            elif line.strip() and section != 'REMARK':\n                frcmod_info[section].append(line)\n\n    return frcmod_info\n
    "},{"location":"reference/config/","title":" config","text":""},{"location":"reference/config/#ties.config","title":"config","text":"

    Classes:

    • Config \u2013

      The configuration with parameters that can be used to define the entire protocol.

    "},{"location":"reference/config/#ties.config.Config","title":"Config","text":"
    Config(**kwargs)\n

    The configuration with parameters that can be used to define the entire protocol. The settings can be overridden later in the actual classes.

    The settings are stored as properties in the object and can be overwritten.

    Methods:

    • get_element_map \u2013

      :return:

    • get_serializable \u2013

      Get a JSON serializable structure of the config.

    Attributes:

    • workdir \u2013

      Working directory for antechamber calls.

    • protein \u2013

      Path to the protein

    • ligand_files \u2013

      A list of ligand filenames.

    • ambertools_home \u2013

      Ambertools HOME path. If not configured, the env variable AMBERHOME as AMBER_PREFIX will be checked.

    • ambertools_antechamber \u2013

      Antechamber path based on the .ambertools_home

    • ambertools_parmchk2 \u2013

      Parmchk2 path based on the .ambertools_home

    • ambertools_tleap \u2013

      Tleap path based on the .ambertools_home

    • antechamber_dr \u2013

      Whether to use -dr setting when calling antechamber.

    • ligand_net_charge \u2013

      The ligand charge. If not provided, neutral charge is assumed.

    • coordinates_file \u2013

      A file from which coordinate can be taken.

    • atom_pair_q_atol \u2013

      It defines the maximum difference in charge

    • net_charge_threshold \u2013

      Defines how much the superimposed regions can, in total, differ in charge.

    • ignore_charges_completely \u2013

      Ignore the charges during the superimposition. Useful for debugging.

    • allow_disjoint_components \u2013

      Defines whether there might be multiple superimposed areas that are

    • use_element_in_superimposition \u2013

      Use element rather than the actual atom type for the superimposition

    • align_molecules_using_mcs \u2013

      After determining the maximum common substructure (MCS),

    • use_original_coor \u2013

      Antechamber when assigning charges can modify the charges slightly.

    • ligands_contain_q \u2013

      If not provided, it tries to deduce whether charges are provided.

    • superimposition_starting_pair \u2013

      Set a starting pair for the superimposition to narrow down the MCS search.

    • manually_matched_atom_pairs \u2013

      Either a list of pairs or a file with a list of pairs of atoms

    • manually_mismatched_pairs \u2013

      A path to a file with a list of a pairs that should be mismatched.

    • protein_ff \u2013

      The protein forcefield to be used by ambertools for the protein parameterisation.

    • md_engine \u2013

      The MD engine, with the supported values NAMD2.13, NAMD2.14, NAMD3 and OpenMM

    • ligand_ff \u2013

      The forcefield for the ligand.

    • ligand_ff_name \u2013

      Either GAFF or GAFF2

    • redistribute_q_over_unmatched \u2013

      The superimposed and matched atoms have every slightly different charges.

    • use_hybrid_single_dual_top \u2013

      Hybrid single dual topology (experimental). Currently not implemented.

    • ligand_tleap_in \u2013

      The name of the tleap input file for ambertools for the ligand.

    • complex_tleap_in \u2013

      The tleap input file for the complex.

    • prep_dir \u2013

      Path to the prep directory. Currently in the workdir

    • pair_morphfrcmods_dir \u2013

      Path to the .frcmod files for the morph.

    • pair_morphfrmocs_tests_dir \u2013

      Path to the location where a test is carried out with .frcmod

    • pair_unique_atom_names_dir \u2013

      Location of the morph files with unique filenames.

    • lig_unique_atom_names_dir \u2013

      Directory location for files with unique atom names.

    • lig_frcmod_dir \u2013

      Directory location with the .frcmod created for each ligand.

    • lig_acprep_dir \u2013

      Directory location where the .ac charges are converted into the .mol2 format.

    • lig_dir \u2013

      Directory location with the .mol2 files.

    Source code in ties/config.py
    def __init__(self, **kwargs):\n    # set the path to the scripts\n    self.code_root = pathlib.Path(os.path.dirname(__file__))\n\n    # scripts/input files,\n    # these are specific to the host\n    self.script_dir = self.code_root / 'scripts'\n    self.namd_script_dir = self.script_dir / 'namd'\n    self.ambertools_script_dir = self.script_dir / 'ambertools'\n    self.tleap_check_protein = self.ambertools_script_dir / 'check_prot.in'\n    self.vmd_vis_script = self.script_dir / 'vmd' / 'vis_morph.vmd'\n    self.vmd_vis_script_sh = self.script_dir / 'vmd' / 'vis_morph.sh'\n\n    self._workdir = None\n    self._antechamber_dr = False\n    self._ambertools_home = None\n\n    self._protein = None\n\n    self._ligand_net_charge = None\n    self._atom_pair_q_atol = 0.1\n    self._net_charge_threshold = 0.1\n    self._redistribute_q_over_unmatched = True\n    self._allow_disjoint_components = False\n    # use only the element in the superimposition rather than the specific atom type\n    self._use_element = False\n    self._use_element_in_superimposition = True\n    self.starting_pairs_heuristics = True\n    # weights in choosing the best MCS, the weighted sum of \"(1 - MCS fraction) and RMSD\".\n    self.weights = [1, 0.5]\n\n    # coordinates\n    self._align_molecules_using_mcs = False\n    self._use_original_coor = False\n    self._coordinates_file = None\n\n    self._ligand_files = set()\n    self._manually_matched_atom_pairs = None\n    self._manually_mismatched_pairs = None\n    self._ligands_contain_q = None\n\n    self._ligand_tleap_in = None\n    self._complex_tleap_in = None\n\n    self._superimposition_starting_pair = None\n\n    self._protein_ff = None\n    self._ligand_ff = 'leaprc.gaff'\n    self._ligand_ff_name = 'gaff'\n\n    # MD/NAMD production input file\n    self._md_engine = 'namd'\n    #default to modern CPU version\n    self.namd_version = '2.14'\n    self._lambda_rep_dir_tree = False\n\n    # experimental\n    self._use_hybrid_single_dual_top = False\n    self._ignore_charges_completely = False\n\n    self.ligands = None\n\n    # if True, do not allow ligands with the same ligand name\n    self.uses_cmd = False\n\n    # assign all the initial configuration values\n    self.set_configs(**kwargs)\n
    "},{"location":"reference/config/#ties.config.Config.workdir","title":"workdir property writable","text":"
    workdir\n

    Working directory for antechamber calls. If None, a temporary directory in /tmp/ will be used.

    :return: Work dir :rtype: str

    "},{"location":"reference/config/#ties.config.Config.protein","title":"protein property writable","text":"
    protein\n

    Path to the protein

    :return: Protein filename :rtype: str

    "},{"location":"reference/config/#ties.config.Config.ligand_files","title":"ligand_files property writable","text":"
    ligand_files\n

    A list of ligand filenames. :return:

    "},{"location":"reference/config/#ties.config.Config.ambertools_home","title":"ambertools_home property writable","text":"
    ambertools_home\n

    Ambertools HOME path. If not configured, the env variable AMBERHOME as AMBER_PREFIX will be checked.

    :return: ambertools path

    "},{"location":"reference/config/#ties.config.Config.ambertools_antechamber","title":"ambertools_antechamber property","text":"
    ambertools_antechamber\n

    Antechamber path based on the .ambertools_home

    :return:

    "},{"location":"reference/config/#ties.config.Config.ambertools_parmchk2","title":"ambertools_parmchk2 property","text":"
    ambertools_parmchk2\n

    Parmchk2 path based on the .ambertools_home :return:

    "},{"location":"reference/config/#ties.config.Config.ambertools_tleap","title":"ambertools_tleap property","text":"
    ambertools_tleap\n

    Tleap path based on the .ambertools_home :return:

    "},{"location":"reference/config/#ties.config.Config.antechamber_dr","title":"antechamber_dr property writable","text":"
    antechamber_dr\n

    Whether to use -dr setting when calling antechamber.

    :return:

    "},{"location":"reference/config/#ties.config.Config.ligand_net_charge","title":"ligand_net_charge property writable","text":"
    ligand_net_charge\n

    The ligand charge. If not provided, neutral charge is assumed. The charge is necessary for calling antechamber (-nc).

    :return:

    "},{"location":"reference/config/#ties.config.Config.coordinates_file","title":"coordinates_file property writable","text":"
    coordinates_file\n

    A file from which coordinate can be taken.

    :return:

    "},{"location":"reference/config/#ties.config.Config.atom_pair_q_atol","title":"atom_pair_q_atol property writable","text":"
    atom_pair_q_atol\n

    It defines the maximum difference in charge between any two superimposed atoms a1 and a2. If the two atoms differ in charge more than this value, they will be unmatched and added to the alchemical regions.

    :return: default (0.1e) :rtype: float

    "},{"location":"reference/config/#ties.config.Config.net_charge_threshold","title":"net_charge_threshold property writable","text":"
    net_charge_threshold\n

    Defines how much the superimposed regions can, in total, differ in charge. If the total exceeds the thresholds, atom pairs will be unmatched until the threshold is met.

    :return: default (0.1e) :rtype: float

    "},{"location":"reference/config/#ties.config.Config.ignore_charges_completely","title":"ignore_charges_completely property writable","text":"
    ignore_charges_completely\n

    Ignore the charges during the superimposition. Useful for debugging. :return: default (False) :rtype: bool

    "},{"location":"reference/config/#ties.config.Config.allow_disjoint_components","title":"allow_disjoint_components property writable","text":"
    allow_disjoint_components\n

    Defines whether there might be multiple superimposed areas that are separated by alchemical region.

    :return: default (False) :rtype: bool

    "},{"location":"reference/config/#ties.config.Config.use_element_in_superimposition","title":"use_element_in_superimposition property writable","text":"
    use_element_in_superimposition\n

    Use element rather than the actual atom type for the superimposition during the joint-traversal of the two molecules.

    :return: default (False) :rtype: bool

    "},{"location":"reference/config/#ties.config.Config.align_molecules_using_mcs","title":"align_molecules_using_mcs property writable","text":"
    align_molecules_using_mcs\n

    After determining the maximum common substructure (MCS), use it to align the coordinates of the second molecule to the first.

    :return: default (False) :rtype: bool

    "},{"location":"reference/config/#ties.config.Config.use_original_coor","title":"use_original_coor property writable","text":"
    use_original_coor\n

    Antechamber when assigning charges can modify the charges slightly. If that's the case, use the original charges in order to correct this slight divergence in coordinates.

    :return: default (?) :rtype: bool

    "},{"location":"reference/config/#ties.config.Config.ligands_contain_q","title":"ligands_contain_q property writable","text":"
    ligands_contain_q\n

    If not provided, it tries to deduce whether charges are provided. If all charges are set to 0, then it assumes that charges are not provided.

    If set to False explicitly, charges are ignored and computed again.

    :return: default (None) :rtype: bool

    "},{"location":"reference/config/#ties.config.Config.superimposition_starting_pair","title":"superimposition_starting_pair property writable","text":"
    superimposition_starting_pair\n

    Set a starting pair for the superimposition to narrow down the MCS search. E.g. \"C2-C12\"

    :rtype: str

    "},{"location":"reference/config/#ties.config.Config.manually_matched_atom_pairs","title":"manually_matched_atom_pairs property writable","text":"
    manually_matched_atom_pairs\n

    Either a list of pairs or a file with a list of pairs of atoms that should be superimposed/matched.

    :return:

    "},{"location":"reference/config/#ties.config.Config.manually_mismatched_pairs","title":"manually_mismatched_pairs property writable","text":"
    manually_mismatched_pairs\n

    A path to a file with a list of a pairs that should be mismatched.

    "},{"location":"reference/config/#ties.config.Config.protein_ff","title":"protein_ff property writable","text":"
    protein_ff\n

    The protein forcefield to be used by ambertools for the protein parameterisation.

    :return: default (leaprc.ff19SB) :rtype: string

    "},{"location":"reference/config/#ties.config.Config.md_engine","title":"md_engine property writable","text":"
    md_engine\n

    The MD engine, with the supported values NAMD2.13, NAMD2.14, NAMD3 and OpenMM

    :return: NAMD2.13, NAMD2.14, NAMD3 and OpenMM :rtype: string

    "},{"location":"reference/config/#ties.config.Config.ligand_ff","title":"ligand_ff property","text":"
    ligand_ff\n

    The forcefield for the ligand.

    "},{"location":"reference/config/#ties.config.Config.ligand_ff_name","title":"ligand_ff_name property writable","text":"
    ligand_ff_name\n

    Either GAFF or GAFF2

    :return:

    "},{"location":"reference/config/#ties.config.Config.redistribute_q_over_unmatched","title":"redistribute_q_over_unmatched property writable","text":"
    redistribute_q_over_unmatched\n

    The superimposed and matched atoms have every slightly different charges. Taking an average charge between any two atoms introduces imbalances in the net charge of the alchemical regions, due to the different charge distribution.

    :return: default(True)

    "},{"location":"reference/config/#ties.config.Config.use_hybrid_single_dual_top","title":"use_hybrid_single_dual_top property writable","text":"
    use_hybrid_single_dual_top\n

    Hybrid single dual topology (experimental). Currently not implemented.

    :return: default(False).

    "},{"location":"reference/config/#ties.config.Config.ligand_tleap_in","title":"ligand_tleap_in property","text":"
    ligand_tleap_in\n

    The name of the tleap input file for ambertools for the ligand.

    :return: Default ('leap_ligand.in') :rtype: string

    "},{"location":"reference/config/#ties.config.Config.complex_tleap_in","title":"complex_tleap_in property","text":"
    complex_tleap_in\n

    The tleap input file for the complex.

    :return: Default 'leap_complex.in' :type: string

    "},{"location":"reference/config/#ties.config.Config.prep_dir","title":"prep_dir property","text":"
    prep_dir\n

    Path to the prep directory. Currently in the workdir

    :return: Default (workdir/prep)

    "},{"location":"reference/config/#ties.config.Config.pair_morphfrcmods_dir","title":"pair_morphfrcmods_dir property","text":"
    pair_morphfrcmods_dir\n

    Path to the .frcmod files for the morph.

    :return: Default (workdir/prep/morph_frcmods)

    "},{"location":"reference/config/#ties.config.Config.pair_morphfrmocs_tests_dir","title":"pair_morphfrmocs_tests_dir property","text":"
    pair_morphfrmocs_tests_dir\n

    Path to the location where a test is carried out with .frcmod

    :return: Default (workdir/prep/morph_frcmods/tests)

    "},{"location":"reference/config/#ties.config.Config.pair_unique_atom_names_dir","title":"pair_unique_atom_names_dir property","text":"
    pair_unique_atom_names_dir\n

    Location of the morph files with unique filenames.

    :return: Default (workdir/prep/morph_unique_atom_names)

    "},{"location":"reference/config/#ties.config.Config.lig_unique_atom_names_dir","title":"lig_unique_atom_names_dir property","text":"
    lig_unique_atom_names_dir\n

    Directory location for files with unique atom names.

    :return: Default (workdir/prep/unique_atom_names)

    "},{"location":"reference/config/#ties.config.Config.lig_frcmod_dir","title":"lig_frcmod_dir property","text":"
    lig_frcmod_dir\n

    Directory location with the .frcmod created for each ligand.

    :return: Default (workdir/prep/ligand_frcmods)

    "},{"location":"reference/config/#ties.config.Config.lig_acprep_dir","title":"lig_acprep_dir property","text":"
    lig_acprep_dir\n

    Directory location where the .ac charges are converted into the .mol2 format.

    :return: Default (workdir/prep/acprep_to_mol2)

    "},{"location":"reference/config/#ties.config.Config.lig_dir","title":"lig_dir property","text":"
    lig_dir\n

    Directory location with the .mol2 files.

    :return: Default (workdir/mol2)

    "},{"location":"reference/config/#ties.config.Config._guess_ligands_contain_q","title":"_guess_ligands_contain_q","text":"
    _guess_ligands_contain_q()\n

    Checks if the first .mol2 file contains charges. :return:

    Source code in ties/config.py
    def _guess_ligands_contain_q(self):\n    \"\"\"\n    Checks if the first .mol2 file contains charges.\n    :return:\n    \"\"\"\n    # if all ligands are .mol2, then charges are provided\n    if all(l.suffix.lower() == '.mol2' for l in self.ligand_files):\n        # if all atoms have q = 0 that means they're a placeholder\n        u = parmed.load_file(str(list(self.ligand_files)[0]), structure=True)\n        all_q_0 = all(a.charge == 0 for a in u.atoms)\n        if all_q_0:\n            return False\n\n        return True\n\n    return False\n
    "},{"location":"reference/config/#ties.config.Config._get_first_ligand_net_q","title":"_get_first_ligand_net_q","text":"
    _get_first_ligand_net_q()\n

    :return: Returns the net charge from the parmed file with partial charges.

    Source code in ties/config.py
    def _get_first_ligand_net_q(self):\n    \"\"\"\n    :return: Returns the net charge from the parmed file with partial charges.\n    \"\"\"\n\n    # if all atoms have q = 0 that means they're a placeholder\n    u = parmed.load_file(str(list(self.ligand_files)[0]), structure=True)\n    net_q = sum(a.charge == 0 for a in u.atoms)\n    return net_q\n
    "},{"location":"reference/config/#ties.config.Config.get_element_map","title":"get_element_map staticmethod","text":"
    get_element_map()\n

    :return:

    Source code in ties/config.py
    @staticmethod\ndef get_element_map():\n    \"\"\"\n\n\n    :return:\n    \"\"\"\n    # Get the mapping of atom types to elements\n    element_map_filename = pathlib.Path(os.path.dirname(__file__)) / 'data' / 'element_atom_type_map.txt'\n    # remove the comments lines with #\n    lines = filter(lambda l: not l.strip().startswith('#') and not l.strip() == '', open(element_map_filename).readlines())\n    # convert into a dictionary\n\n    element_map = {}\n    for line in lines:\n        element, atom_types = line.split('=')\n\n        for atom_type in atom_types.split():\n            element_map[atom_type.strip()] = element.strip()\n\n    return element_map\n
    "},{"location":"reference/config/#ties.config.Config.get_serializable","title":"get_serializable","text":"
    get_serializable()\n

    Get a JSON serializable structure of the config.

    pathlib.Path is not JSON serializable, so replace it with str

    todo - consider capturing all information about the system here, including each suptop.get_serializable() so that you can record specific information such as the charge changes etc.

    :return: Dictionary {key:value} with the settings :rtype: Dictionary

    Source code in ties/config.py
    def get_serializable(self):\n    \"\"\"\n    Get a JSON serializable structure of the config.\n\n    pathlib.Path is not JSON serializable, so replace it with str\n\n    todo - consider capturing all information about the system here,\n    including each suptop.get_serializable() so that you can record\n    specific information such as the charge changes etc.\n\n    :return: Dictionary {key:value} with the settings\n    :rtype: Dictionary\n    \"\"\"\n\n    host_specific = ['code_root', 'script_dir0', 'namd_script_dir',\n                     'ambertools_script_dir', 'tleap_check_protein', 'vmd_vis_script']\n\n    ser = {}\n    for k, v in self.__dict__.items():\n        if k in host_specific:\n            continue\n\n        if type(v) is pathlib.PosixPath:\n            v = str(v)\n\n        # account for the ligands being pathlib objects\n        if k == 'ligands' and v is not None:\n            # a list of ligands, convert to strings\n            v = [str(l) for l in v]\n        if k == '_ligand_files':\n            continue\n\n        ser[k] = v\n\n    return ser\n
    "},{"location":"reference/generator/","title":" generator","text":""},{"location":"reference/generator/#ties.generator","title":"generator","text":"

    Functions:

    • join_frcmod_files \u2013

      This implementation should be used. Switch to join_frcmod_files2.

    • correct_fep_tempfactor \u2013

      fixme - this function does not need to use the file?

    • get_PBC_coords \u2013

      Return [x, y, z]

    • extract_PBC_oct_from_tleap_log \u2013

      http://ambermd.org/namd/namd_amber.html

    • prepare_antechamber_parmchk2 \u2013

      Prepare the ambertools scripts.

    • get_protein_net_charge \u2013

      Use automatic ambertools solvation of a single component to determine what is the next charge of the system.

    • prepareFile \u2013

      Either copies or sets up a relative link between the files.

    • set_coor_from_ref_by_named_pairs \u2013

      Set coordinates but use atom names provided by the user.

    • update_PBC_in_namd_input \u2013

      fixme - rename this file since it generates the .eq files

    • create_constraint_files \u2013

      :param original_pdb:

    • init_namd_file_min \u2013

      :param from_dir:

    • generate_namd_prod \u2013

      :param namd_prod:

    • generate_namd_eq \u2013

      :param namd_eq:

    • redistribute_charges \u2013

      Calculate the original charges in the matched component.

    "},{"location":"reference/generator/#ties.generator._merge_frcmod_section","title":"_merge_frcmod_section","text":"
    _merge_frcmod_section(ref_lines, other_lines)\n

    A helper function for merging lines in .frcmod files. Note that the order has to be kept. This is because some lines need to follow other lines. In this case, we exclude lines that are present in ref_lines. fixme - since there are duplicate lines, we need to check the duplicates and their presence,

    Source code in ties/generator.py
    def _merge_frcmod_section(ref_lines, other_lines):\n    \"\"\"\n    A helper function for merging lines in .frcmod files.\n    Note that the order has to be kept. This is because some lines need to follow other lines.\n    In this case, we exclude lines that are present in ref_lines.\n    fixme - since there are duplicate lines, we need to check the duplicates and their presence,\n    \"\"\"\n    merged_section = copy.copy(ref_lines)\n    for line in other_lines:\n        if line not in ref_lines:\n            merged_section.append(line)\n\n    return merged_section\n
    "},{"location":"reference/generator/#ties.generator.join_frcmod_files","title":"join_frcmod_files","text":"
    join_frcmod_files(f1, f2, output_filepath)\n

    This implementation should be used. Switch to join_frcmod_files2. This version might be removed if the simple approach is fine.

    Source code in ties/generator.py
    def join_frcmod_files(f1, f2, output_filepath):\n    \"\"\"\n    This implementation should be used. Switch to join_frcmod_files2.\n    This version might be removed if the simple approach is fine.\n    \"\"\"\n    # fixme - load f1 and f2\n\n    def get_section(name, rlines):\n        \"\"\"\n        Chips away from the lines until the section is ready\n\n        fixme is there a .frcmod reader in ambertools?\n        http://ambermd.org/FileFormats.php#frcmod\n        \"\"\"\n        section_names = ['MASS', 'BOND', 'ANGLE', 'DIHE', 'IMPROPER', 'NONBON']\n        assert name in rlines.pop().strip()\n\n        section = []\n        while not (len(rlines) == 0 or any(rlines[-1].startswith(sname) for sname in section_names)):\n            nextl = rlines.pop().strip()\n            if nextl == '':\n                continue\n            # depending on the column name, parse differently\n            if name == 'ANGLE':\n                # e.g.\n                # c -cc-na   86.700     123.270   same as c2-cc-na, penalty score=  2.6\n                atom_types = nextl[:8]\n                other = nextl[9:].split()[::-1]\n                # The harmonic force constants for the angle \"ITT\"-\"JTT\"-\n                #                     \"KTT\" in units of kcal/mol/(rad**2) (radians are the\n                #                     traditional unit for angle parameters in force fields).\n                harmonicForceConstant = float(other.pop())\n                # TEQ        The equilibrium bond angle for the above angle in degrees.\n                eq_bond_angle = float(other.pop())\n                # the overall angle\n                section.append([atom_types, harmonicForceConstant, eq_bond_angle])\n            elif name == 'DIHE':\n                # e.g.\n                # ca-ca-cd-cc   1    0.505       180.000           2.000      same as c2-ce-ca-ca, penalty score=229.0\n                atom_types = nextl[:11]\n                other = nextl[11:].split()[::-1]\n                \"\"\"\n                IDIVF      The factor by which the torsional barrier is divided.\n                    Consult Weiner, et al., JACS 106:765 (1984) p. 769 for\n                    details. Basically, the actual torsional potential is\n\n                           (PK/IDIVF) * (1 + cos(PN*phi - PHASE))\n\n                 PK         The barrier height divided by a factor of 2.\n\n                 PHASE      The phase shift angle in the torsional function.\n\n                            The unit is degrees.\n\n                 PN         The periodicity of the torsional barrier.\n                            NOTE: If PN .lt. 0.0 then the torsional potential\n                                  is assumed to have more than one term, and the\n                                  values of the rest of the terms are read from the\n                                  next cards until a positive PN is encountered.  The\n                                  negative value of pn is used only for identifying\n                                  the existence of the next term and only the\n                                  absolute value of PN is kept.\n                \"\"\"\n                IDIVF = float(other.pop())\n                PK = float(other.pop())\n                PHASE = float(other.pop())\n                PN = float(other.pop())\n                section.append([atom_types, IDIVF, PK, PHASE, PN])\n            elif name == 'IMPROPER':\n                # e.g.\n                # cc-o -c -o          1.1          180.0         2.0          Using general improper torsional angle  X- o- c- o, penalty score=  3.0)\n                # ...  IDIVF , PK , PHASE , PN\n                atom_types = nextl[:11]\n                other = nextl[11:].split()[::-1]\n                # fixme - what is going on here? why is not generated this number?\n                # IDIVF = float(other.pop())\n                PK = float(other.pop())\n                PHASE = float(other.pop())\n                PN = float(other.pop())\n                if PN < 0:\n                    raise Exception('Unimplemented - ordering using with negative 0')\n                section.append([atom_types, PK, PHASE, PN])\n            else:\n                section.append(nextl.split())\n        return {name: section}\n\n    def load_frcmod(filepath):\n        # remark line\n        rlines = open(filepath).readlines()[::-1]\n        assert 'Remark' in rlines.pop()\n\n        parsed = OrderedDict()\n        for section_name in ['MASS', 'BOND', 'ANGLE', 'DIHE', 'IMPROPER', 'NONBON']:\n            parsed.update(get_section(section_name, rlines))\n\n        return parsed\n\n    def join_frcmod(left_frc, right_frc):\n        joined = OrderedDict()\n        for left, right in zip(left_frc.items(), right_frc.items()):\n            lname, litems = left\n            rname, ritems = right\n            assert lname == rname\n\n            joined[lname] = copy.copy(litems)\n\n            if lname == 'MASS':\n                if len(litems) > 0 or len(ritems) > 0:\n                    raise Exception('Unimplemented')\n            elif lname == 'BOND':\n                for ritem in ritems:\n                    if len(litems) > 0 or len(ritems) > 0:\n                        if ritem not in joined[lname]:\n                            raise Exception('Unimplemented')\n            # ANGLE, e.g.\n            # c -cc-na   86.700     123.270   same as c2-cc-na, penalty score=  2.6\n            elif lname == 'ANGLE':\n                for ritem in ritems:\n                    # if the item is not in the litems, add it there\n                    # extra the first three terms to determine if it is present\n                    # fixme - note we are ignoring the \"same as\" note\n                    if ritem not in joined[lname]:\n                        joined[lname].append(ritem)\n            elif lname == 'DIHE':\n                for ritem in ritems:\n                    if ritem not in joined[lname]:\n                        joined[lname].append(ritem)\n            elif lname == 'IMPROPER':\n                for ritem in ritems:\n                    if ritem not in joined[lname]:\n                        joined[lname].append(ritem)\n            elif lname == 'NONBON':\n                # if they're empty\n                if not litems and not ritems:\n                    continue\n\n                raise Exception('Unimplemented')\n            else:\n                raise Exception('Unimplemented')\n        return joined\n\n    def write_frcmod(frcmod, filename):\n        with open(filename, 'w') as FOUT:\n            FOUT.write('GENERATED .frcmod by joining two .frcmod files' + os.linesep)\n            for sname, items in frcmod.items():\n                FOUT.write(f'{sname}' + os.linesep)\n                for item in items:\n                    atom_types = item[0]\n                    FOUT.write(atom_types)\n                    numbers = ' \\t'.join([str(n) for n in item[1:]])\n                    FOUT.write(' \\t' + numbers)\n                    FOUT.write(os.linesep)\n                # the ending line\n                FOUT.write(os.linesep)\n\n    left_frc = load_frcmod(f1)\n    right_frc = load_frcmod(f2)\n    joined_frc = join_frcmod(left_frc, right_frc)\n    write_frcmod(joined_frc, output_filepath)\n
    "},{"location":"reference/generator/#ties.generator._correct_fep_tempfactor_single_top","title":"_correct_fep_tempfactor_single_top","text":"
    _correct_fep_tempfactor_single_top(fep_summary, source_pdb_filename, new_pdb_filename)\n

    Single topology version of function correct_fep_tempfactor.

    The left ligand has to be called INI And right FIN

    Source code in ties/generator.py
    def _correct_fep_tempfactor_single_top(fep_summary, source_pdb_filename, new_pdb_filename):\n    \"\"\"\n    Single topology version of function correct_fep_tempfactor.\n\n    The left ligand has to be called INI\n    And right FIN\n    \"\"\"\n    source_sys = parmed.load_file(source_pdb_filename, structure=True)\n    if {'INI', 'FIN'} != {a.residue.name for a in source_sys.atoms}:\n        raise Exception('Missing the resname \"mer\" in the pdb file prepared for fep')\n\n    # dual-topology info\n    # matched atoms are denoted -2 and 2 (morphing into each other)\n    matched_disappearing = list(fep_summary['single_top_matched'].keys())\n    matched_appearing = list( fep_summary['single_top_matched'].values())\n    # disappearing is denoted by -1\n    disappearing_atoms = fep_summary['single_top_disappearing']\n    # appearing is denoted by 1\n    appearing_atoms = fep_summary['single_top_appearing']\n\n    # update the Temp column\n    for atom in source_sys.atoms:\n        # ignore water and ions and non-ligand resname\n        # we only modify the protein, so ignore the ligand resnames\n        # fixme .. why is it called mer, is it tleap?\n        if atom.residue.name not in ['INI', 'FIN']:\n            continue\n\n        # if the atom was \"matched\", meaning present in both ligands (left and right)\n        # then ignore\n        # note: we only use the left ligand\n        if atom.name.upper() in matched_disappearing:\n            atom.bfactor = -2\n        elif atom.name.upper() in matched_appearing:\n            atom.bfactor = 2\n        elif atom.name.upper() in disappearing_atoms:\n            atom.bfactor = -1\n        elif atom.name.upper() in appearing_atoms:\n            # appearing atoms should\n            atom.bfactor = 1\n        else:\n            raise Exception('This should never happen. It has to be one of the cases')\n\n    source_sys.save(new_pdb_filename, use_hetatoms=False)  # , file_format='PDB') - fixme?\n
    "},{"location":"reference/generator/#ties.generator.correct_fep_tempfactor","title":"correct_fep_tempfactor","text":"
    correct_fep_tempfactor(fep_summary, source_pdb_filename, new_pdb_filename, hybrid_topology=False)\n

    fixme - this function does not need to use the file? we have the json information available here.

    Sets the temperature column in the PDB file So that the number reflects the alchemical information Requires by NAMD in order to know which atoms appear (1) and which disappear (-1).

    Source code in ties/generator.py
    def correct_fep_tempfactor(fep_summary, source_pdb_filename, new_pdb_filename, hybrid_topology=False):\n    \"\"\"\n    fixme - this function does not need to use the file?\n    we have the json information available here.\n\n    Sets the temperature column in the PDB file\n    So that the number reflects the alchemical information\n    Requires by NAMD in order to know which atoms\n    appear (1) and which disappear (-1).\n    \"\"\"\n    if hybrid_topology:\n        # delegate correcting fep column in the pdb file\n        return _correct_fep_tempfactor_single_top(fep_summary, source_pdb_filename, new_pdb_filename)\n\n    pmdpdb = parmed.load_file(str(source_pdb_filename), structure=True)\n    if 'HYB' not in {a.residue.name for a in pmdpdb.atoms}:\n        raise Exception('Missing the resname \"HYB\" in the pdb file prepared for fep')\n\n    # dual-topology info\n    matched = list(fep_summary['superimposition']['matched'].keys())\n    appearing_atoms = fep_summary['superimposition']['appearing']\n    disappearing_atoms = fep_summary['superimposition']['disappearing']\n\n    # update the Temp column\n    for atom in pmdpdb.atoms:\n        # ignore water and ions and non-ligand resname\n        # we only modify the protein, so ignore the ligand resnames\n        # fixme .. why is it called mer, is it tleap?\n        if atom.residue.name != 'HYB':\n            continue\n\n        # if the atom was \"matched\", meaning present in both ligands (left and right)\n        # then ignore\n        # note: we only use the left ligand\n        if atom.name in matched:\n            continue\n        elif atom.name in appearing_atoms:\n            # appearing atoms should\n            atom.bfactor = 1\n        elif atom.name in disappearing_atoms:\n            atom.bfactor = -1\n        else:\n            raise Exception('This should never happen. It has to be one of the cases')\n\n    pmdpdb.save(str(new_pdb_filename), use_hetatoms=False, overwrite=True)  # , file_format='PDB') - fixme?\n
    "},{"location":"reference/generator/#ties.generator.get_PBC_coords","title":"get_PBC_coords","text":"
    get_PBC_coords(pdb_file)\n

    Return [x, y, z]

    Source code in ties/generator.py
    def get_PBC_coords(pdb_file):\n    \"\"\"\n    Return [x, y, z]\n    \"\"\"\n    raise Exception('This should not be called PBC coords. Revisit')\n    # u = load(pdb_file)\n    x = np.abs(max(u.atoms.positions[:, 0]) - min(u.atoms.positions[:, 0]))\n    y = np.abs(max(u.atoms.positions[:, 1]) - min(u.atoms.positions[:, 1]))\n    z = np.abs(max(u.atoms.positions[:, 2]) - min(u.atoms.positions[:, 2]))\n    return (x, y, z)\n
    "},{"location":"reference/generator/#ties.generator.extract_PBC_oct_from_tleap_log","title":"extract_PBC_oct_from_tleap_log","text":"
    extract_PBC_oct_from_tleap_log(leap_log)\n

    http://ambermd.org/namd/namd_amber.html Return the 9 numbers for the truncated octahedron unit cell in namd cellBasisVector1 d 0.0 0.0 cellBasisVector2 (-1/3)d (2/3)sqrt(2)d 0.0 cellBasisVector3 (-1/3)d (-1/3)sqrt(2)d (-1/3)sqrt(6)*d

    Source code in ties/generator.py
    def extract_PBC_oct_from_tleap_log(leap_log):\n    \"\"\"\n    http://ambermd.org/namd/namd_amber.html\n    Return the 9 numbers for the truncated octahedron unit cell in namd\n    cellBasisVector1  d         0.0            0.0\n    cellBasisVector2  (-1/3)*d (2/3)sqrt(2)*d  0.0\n    cellBasisVector3  (-1/3)*d (-1/3)sqrt(2)*d (-1/3)sqrt(6)*d\n    \"\"\"\n    leapl_log_lines = open(leap_log).readlines()\n    line_to_extract = \"Total bounding box for atom centers:\"\n    line_of_interest = list(filter(lambda l: line_to_extract in l, leapl_log_lines))\n    d1, d2, d3 = line_of_interest[-1].split(line_to_extract)[1].split()\n    d1, d2, d3 = float(d1), float(d2), float(d3)\n    assert d1 == d2 == d3\n    # scale the d since after minimisation the system turns out to be much smaller?\n    d = d1 * 0.8\n    return {\n        'cbv1': d, 'cbv2': 0, 'cbv3': 0,\n        'cbv4': (1/3.0)*d, 'cbv5': (2/3.0)*np.sqrt(2)*d, 'cbv6': 0,\n        'cbv7': (-1/3.0)*d, 'cbv8': (1/3.0)*np.sqrt(2)*d, 'cbv9': (1/3)*np.sqrt(6)*d,\n    }\n
    "},{"location":"reference/generator/#ties.generator.prepare_antechamber_parmchk2","title":"prepare_antechamber_parmchk2","text":"
    prepare_antechamber_parmchk2(source_script, target_script, net_charge)\n

    Prepare the ambertools scripts. Particularly, create the scritp so that it has the net charge

    "},{"location":"reference/generator/#ties.generator.prepare_antechamber_parmchk2--fixme-run-antechamber-directly-with-the-right-settings-from-here","title":"fixme - run antechamber directly with the right settings from here?","text":""},{"location":"reference/generator/#ties.generator.prepare_antechamber_parmchk2--fixme-check-if-antechamber-has-a-python-interface","title":"fixme - check if antechamber has a python interface?","text":"Source code in ties/generator.py
    def prepare_antechamber_parmchk2(source_script, target_script, net_charge):\n    \"\"\"\n    Prepare the ambertools scripts.\n    Particularly, create the scritp so that it has the net charge\n    # fixme - run antechamber directly with the right settings from here?\n    # fixme - check if antechamber has a python interface?\n    \"\"\"\n    net_charge_set = open(source_script).read().format(net_charge=net_charge)\n    open(target_script, 'w').write(net_charge_set)\n
    "},{"location":"reference/generator/#ties.generator.get_protein_net_charge","title":"get_protein_net_charge","text":"
    get_protein_net_charge(working_dir, protein_file, ambertools_tleap, leap_input_file, prot_ff)\n

    Use automatic ambertools solvation of a single component to determine what is the next charge of the system. This should be replaced with pka/propka or something akin. Note that this is unsuitable for the hybrid ligand: ambertools does not understand a hybrid ligand and might assign the wront net charge.

    Source code in ties/generator.py
    def get_protein_net_charge(working_dir, protein_file, ambertools_tleap, leap_input_file, prot_ff):\n    \"\"\"\n    Use automatic ambertools solvation of a single component to determine what is the next charge of the system.\n    This should be replaced with pka/propka or something akin.\n    Note that this is unsuitable for the hybrid ligand: ambertools does not understand a hybrid ligand\n    and might assign the wront net charge.\n    \"\"\"\n    cwd = working_dir / 'prep' / 'prep_protein_to_find_net_charge'\n    if not cwd.is_dir():\n        cwd.mkdir()\n\n    # copy the protein\n    shutil.copy(working_dir / protein_file, cwd)\n\n    # use ambertools to solvate the protein: set ion numbers to 0 so that they are determined automatically\n    # fixme - consider moving out of the complex\n    leap_in_conf = open(leap_input_file).read()\n    ligand_ff = 'leaprc.gaff' # ignored but must be provided\n    open(cwd / 'solv_prot.in', 'w').write(leap_in_conf.format(protein_ff=prot_ff, ligand_ff=ligand_ff,\n                                                                          protein_file=protein_file))\n\n    log_filename = cwd / \"ties_tleap.log\"\n    with open(log_filename, 'w') as LOG:\n        try:\n            subprocess.run([ambertools_tleap, '-s', '-f', 'solv_prot.in'],\n                           cwd = cwd,\n                           stdout=LOG, stderr=LOG,\n                           check=True, text=True,\n                           timeout=60 * 2  # 2 minutes\n                        )\n        except subprocess.CalledProcessError as E:\n            print('ERROR: tleap could generate a simple topology for the protein to check the number of ions. ')\n            print(f'ERROR: The output was saved in the directory: {cwd}')\n            print(f'ERROR: can be found in the file: {log_filename}')\n            raise E\n\n\n    # read the file to see how many ions were added\n    newsys = parmed.load_file(str(cwd / 'prot_solv.pdb'), structure=True)\n    names = [a.name for a in newsys.atoms]\n    cl = names.count('Cl-')\n    na = names.count('Na+')  \n\n    if cl > na:\n        return cl-na\n    elif cl < na:\n        return -(na-cl)\n\n    return 0\n
    "},{"location":"reference/generator/#ties.generator.prepareFile","title":"prepareFile","text":"
    prepareFile(src, dst, symbolic=True)\n

    Either copies or sets up a relative link between the files. This allows for a quick switch in how the directory structure is organised. Using relative links means that the entire TIES ligand or TIES complex has to be moved together. However, one might want to be able to send a single replica anywhere and execute it independantly (suitable for BOINC).

    @type: 'sym' or 'copy'

    Source code in ties/generator.py
    def prepareFile(src, dst, symbolic=True):\n    \"\"\"\n    Either copies or sets up a relative link between the files.\n    This allows for a quick switch in how the directory structure is organised.\n    Using relative links means that the entire TIES ligand or TIES complex\n    has to be moved together.\n    However, one might want to be able to send a single replica anywhere and\n    execute it independantly (suitable for BOINC).\n\n    @type: 'sym' or 'copy'\n    \"\"\"\n    if symbolic:\n        # note that deleting all the files is intrusive, todo\n        if os.path.isfile(dst):\n            os.remove(dst)\n        os.symlink(src, dst)\n    else:\n        if os.path.isfile(dst):\n            os.remove(dst)\n        shutil.copy(src, dst)\n
    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs","title":"set_coor_from_ref_by_named_pairs","text":"
    set_coor_from_ref_by_named_pairs(mol2_filename, coor_ref_filename, output_filename, left_right_pairs_filename)\n

    Set coordinates but use atom names provided by the user.

    Example of the left_right_pairs_filename content:

    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--flip-the-first-ring","title":"flip the first ring","text":""},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--move-the-first-c-and-its-h","title":"move the first c and its h","text":"

    C32 C18 H34 C19

    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--second-pair","title":"second pair","text":"

    C33 C17

    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--the-actual-matching-pair","title":"the actual matching pair","text":"

    C31 C16 H28 H11

    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--the-second-matching-pair","title":"the second matching pair","text":"

    C30 C15 H29 H12

    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--_1","title":" generator","text":"

    C35 C14

    "},{"location":"reference/generator/#ties.generator.set_coor_from_ref_by_named_pairs--flip-the-other-ring-with-itself","title":"flip the other ring with itself","text":"

    C39 C36 C36 C39 H33 H30 H30 H33 C37 C38 C38 C37 H31 H32 H32 H31

    Source code in ties/generator.py
    def set_coor_from_ref_by_named_pairs(mol2_filename, coor_ref_filename, output_filename, left_right_pairs_filename):\n    \"\"\"\n    Set coordinates but use atom names provided by the user.\n\n    Example of the left_right_pairs_filename content:\n    # flip the first ring\n    # move the first c and its h\n    C32 C18\n    H34 C19\n    # second pair\n    C33 C17\n    # the actual matching pair\n    C31 C16\n    H28 H11\n    # the second matching pair\n    C30 C15\n    H29 H12\n    #\n    C35 C14\n    # flip the other ring with itself\n    C39 C36\n    C36 C39\n    H33 H30\n    H30 H33\n    C37 C38\n    C38 C37\n    H31 H32\n    H32 H31\n    \"\"\"\n    # fixme - check if the names are unique\n\n    # parse \"left_right_pairs_filename\n    # format per line: leftatom_name right_atom_name\n    lines = open(left_right_pairs_filename).read().split(os.linesep)\n    left_right_pairs = (l.split() for l in lines if not l.strip().startswith('#'))\n\n    # load the ref coordinates\n    ref_mol2 = load_mol2_wrapper(coor_ref_filename)\n    # load the .mol2 files with ParmEd and correct the charges\n    static_mol2 = load_mol2_wrapper(mol2_filename)\n    # this is being modified\n    mod_mol2 = load_mol2_wrapper(mol2_filename)\n\n\n    for pair in left_right_pairs:\n        print('find pair', pair)\n        new_pos = False\n        for mol2_atom in static_mol2.atoms:\n            # check if we are assigning from another molecule\n            for ref_atom in ref_mol2.atoms:\n                if mol2_atom.name.upper() == pair[0] and ref_atom.name.upper() == pair[1]:\n                    new_pos = ref_atom.position\n            # check if we are trying to assing coords from the same molecule\n            for another_atom in static_mol2.atoms:\n                if mol2_atom.name.upper() == pair[0] and another_atom.name.upper() == pair[1]:\n                    new_pos = another_atom.position\n\n        if new_pos is False:\n            raise Exception(\"Could not find this pair: \" + str(pair))\n\n        # assign the position to the right atom\n        # find pair[0]\n        found = False\n        for atom in mod_mol2.atoms:\n            if atom.name.upper() == pair[0]:\n                atom.position = new_pos\n                found = True\n                break\n        assert found\n\n\n    # update the mol2 file\n    mod_mol2.atoms.write(output_filename)\n
    "},{"location":"reference/generator/#ties.generator.update_PBC_in_namd_input","title":"update_PBC_in_namd_input","text":"
    update_PBC_in_namd_input(namd_filename, new_pbc_box, structure_filename, constraint_lines='')\n

    fixme - rename this file since it generates the .eq files These are the lines we modify: cellBasisVector1 {cell_x} 0.000 0.000 cellBasisVector2 0.000 {cell_y} 0.000 cellBasisVector3 0.000 0.000 {cell_z}

    With x/y/z replacing the 3 values

    Source code in ties/generator.py
    def update_PBC_in_namd_input(namd_filename, new_pbc_box, structure_filename, constraint_lines=''):\n    \"\"\"\n    fixme - rename this file since it generates the .eq files\n    These are the lines we modify:\n    cellBasisVector1\t{cell_x}  0.000  0.000\n    cellBasisVector2\t 0.000  {cell_y}  0.000\n    cellBasisVector3\t 0.000  0.000 {cell_z}\n\n    With x/y/z replacing the 3 values\n    \"\"\"\n    assert len(new_pbc_box) == 3\n\n    reformatted_namd_in = open(namd_filename).read().format(\n        cell_x=new_pbc_box[0], cell_y=new_pbc_box[1], cell_z=new_pbc_box[2],\n\n        constraints=constraint_lines, output='test_output', structure=structure_filename)\n\n    # write to the file\n    open(namd_filename, 'w').write(reformatted_namd_in)\n
    "},{"location":"reference/generator/#ties.generator.create_constraint_files","title":"create_constraint_files","text":"
    create_constraint_files(original_pdb, output)\n

    :param original_pdb: :param output: :return:

    Source code in ties/generator.py
    def create_constraint_files(original_pdb, output):\n    '''\n\n    :param original_pdb:\n    :param output:\n    :return:\n    '''\n    sys = parmed.load_file(str(original_pdb), structure=True)\n    # for each atom, give the B column the right value\n    for atom in sys.atoms:\n        # ignore water\n        if atom.residue.name in ['WAT', 'Na+', 'TIP3W', 'TIP3', 'HOH', 'SPC', 'TIP4P']:\n            continue\n\n        # set each atom depending on whether it is a H or not\n        if atom.name.upper().startswith('H'):\n            atom.bfactor = 0\n        else:\n            # restrain the heavy atom\n            atom.bfactor = 4\n\n    sys.save(output, use_hetatoms=False, overwrite=True)\n
    "},{"location":"reference/generator/#ties.generator.init_namd_file_min","title":"init_namd_file_min","text":"
    init_namd_file_min(from_dir, to_dir, filename, structure_name, pbc_box, protein)\n

    :param from_dir: :param to_dir: :param filename: :param structure_name: :param pbc_box: :param protein: :return:

    Source code in ties/generator.py
    def init_namd_file_min(from_dir, to_dir, filename, structure_name, pbc_box, protein):\n    '''\n\n    :param from_dir:\n    :param to_dir:\n    :param filename:\n    :param structure_name:\n    :param pbc_box:\n    :param protein:\n    :return:\n    '''\n    if protein is not None:\n        cons = f\"\"\"\nconstraints  on\nconsexp  2\n# use the same file for the position reference and the B column\nconsref  ../build/{structure_name}.pdb ;#need all positions\nconskfile  ../build/cons.pdb\nconskcol  B\n        \"\"\"\n    else:\n        cons = 'constraints  off'\n\n    min_namd_initialised = open(os.path.join(from_dir, filename)).read() \\\n        .format(structure_name=structure_name, constraints=cons, **pbc_box)\n    out_name = 'eq0.conf'\n    open(os.path.join(to_dir, out_name), 'w').write(min_namd_initialised)\n
    "},{"location":"reference/generator/#ties.generator.generate_namd_prod","title":"generate_namd_prod","text":"
    generate_namd_prod(namd_prod, dst_dir, structure_name)\n

    :param namd_prod: :param dst_dir: :param structure_name: :return:

    Source code in ties/generator.py
    def generate_namd_prod(namd_prod, dst_dir, structure_name):\n    '''\n\n    :param namd_prod:\n    :param dst_dir:\n    :param structure_name:\n    :return:\n    '''\n    input_data = open(namd_prod).read()\n    reformatted_namd_in = input_data.format(output='sim1', structure_name=structure_name)\n    open(dst_dir, 'w').write(reformatted_namd_in)\n
    "},{"location":"reference/generator/#ties.generator.generate_namd_eq","title":"generate_namd_eq","text":"
    generate_namd_eq(namd_eq, dst_dir, structure_name, engine, protein)\n

    :param namd_eq: :param dst_dir: :param structure_name: :param engine: :param protein: :return:

    Source code in ties/generator.py
    def generate_namd_eq(namd_eq, dst_dir, structure_name, engine, protein):\n    '''\n\n    :param namd_eq:\n    :param dst_dir:\n    :param structure_name:\n    :param engine:\n    :param protein:\n    :return:\n    '''\n    input_data = open(namd_eq).read()\n    for i in range(1,3):\n\n        if i == 1:\n            run = \"\"\"\nconstraintScaling 1\nrun 10000\n            \"\"\"\n            pressure = ''\n        else:\n            run = \"\"\"\n# protocol - minimization\nset factor 1\nset nall 10\nset n 1\n\nwhile {$n <= $nall} {\n   constraintScaling $factor\n   run 40000\n   set n [expr $n + 1]\n   set factor [expr $factor * 0.5]\n}\n\nconstraintScaling 0\nrun 600000\n            \"\"\"\n            if engine.lower() == 'namd' or engine.lower() == 'namd2':\n                pressure = \"\"\"\nuseGroupPressure      yes ;# needed for 2fs steps\nuseFlexibleCell       no  ;# no for water box, yes for membrane\nuseConstantArea       no  ;# no for water box, yes for membrane\nBerendsenPressure                       on\nBerendsenPressureTarget                 1.0\nBerendsenPressureCompressibility        4.57e-5\nBerendsenPressureRelaxationTime         100\nBerendsenPressureFreq                   2\n                \"\"\"\n            else:\n                pressure = \"\"\"\nuseGroupPressure      yes ;# needed for 2fs steps\nuseFlexibleCell       no  ;# no for water box, yes for membrane\nuseConstantArea       no  ;# no for water box, yes for membrane\nlangevinPiston          on             # Nose-Hoover Langevin piston pressure control\nlangevinPistonTarget  1.01325          # target pressure in bar 1atm = 1.01325bar\nlangevinPistonPeriod  50.0             # oscillation period in fs. correspond to pgamma T=50fs=0.05ps\nlangevinPistonTemp    300              # f=1/T=20.0(pgamma)\nlangevinPistonDecay   25.0             # oscillation decay time. smaller value correspons to larger random\n                                       # forces and increased coupling to the Langevin temp bath.\n                                       # Equall or smaller than piston period\n                \"\"\"\n\n        if protein is not None:\n            cons = f\"\"\"\n        constraints  on\n        consexp  2\n        # use the same file for the position reference and the B column\n        consref  ../build/{structure_name}.pdb ;#need all positions\n        conskfile  ../build/cons.pdb\n        conskcol  B\n                \"\"\"\n        else:\n            cons = 'constraints  off'\n\n        prev_output = 'eq{}'.format(i-1)\n\n        reformatted_namd_in = input_data.format(\n            constraints=cons, output='eq%d' % (i),\n            prev_output=prev_output, structure_name=structure_name, pressure=pressure, run=run)\n\n        next_eq_step_filename = dst_dir / (\"eq%d.conf\" % (i))\n        open(next_eq_step_filename, 'w').write(reformatted_namd_in)\n
    "},{"location":"reference/generator/#ties.generator.redistribute_charges","title":"redistribute_charges","text":"
    redistribute_charges(mda)\n

    Calculate the original charges in the matched component.

    Source code in ties/generator.py
    def redistribute_charges(mda):\n    \"\"\"\n    Calculate the original charges in the matched component.\n    \"\"\"\n\n\n    return\n
    "},{"location":"reference/helpers/","title":" helpers","text":""},{"location":"reference/helpers/#ties.helpers","title":"helpers","text":"

    A list of functions with a clear purpose that does not belong specifically to any of the existing units.

    Classes:

    • ArgparseChecker \u2013

    Functions:

    • get_new_atom_names \u2013

      todo - add unit tests

    • get_atom_names_counter \u2013

      name_counter: a dictionary with atom as the key such as 'N', 'C', etc,

    • parse_frcmod_sections \u2013

      Copied from the previous TIES. It's simpler and this approach must be fine then.

    "},{"location":"reference/helpers/#ties.helpers.ArgparseChecker","title":"ArgparseChecker","text":"

    Methods:

    • str2bool \u2013

      ArgumentParser tool to figure out the bool value

    • logging_lvl \u2013

      ArgumentParser tool to figure out the bool value

    "},{"location":"reference/helpers/#ties.helpers.ArgparseChecker.str2bool","title":"str2bool staticmethod","text":"
    str2bool(v)\n

    ArgumentParser tool to figure out the bool value

    Source code in ties/helpers.py
    @staticmethod\ndef str2bool(v):\n    \"ArgumentParser tool to figure out the bool value\"\n    if isinstance(v, bool):\n        return v\n    if v.lower() in ('yes', 'true', 't', 'y', '1'):\n        return True\n    elif v.lower() in ('no', 'false', 'f', 'n', '0'):\n        return False\n    else:\n        raise argparse.ArgumentTypeError('Boolean value expected.')\n
    "},{"location":"reference/helpers/#ties.helpers.ArgparseChecker.logging_lvl","title":"logging_lvl staticmethod","text":"
    logging_lvl(v)\n

    ArgumentParser tool to figure out the bool value

    Source code in ties/helpers.py
    @staticmethod\ndef logging_lvl(v):\n    \"ArgumentParser tool to figure out the bool value\"\n    logging_levels = {\n        'NOTSET': logging.NOTSET,\n          'DEBUG': logging.DEBUG,\n          'INFO': logging.INFO,\n          'WARNING': logging.WARNING,\n          'ERROR': logging.ERROR,\n          'CRITICAL': logging.CRITICAL,\n          # extras\n           \"ALL\": logging.INFO,\n           \"FALSE\": logging.ERROR\n                      }\n\n    if isinstance(v, bool) and v is True:\n        return logging.WARNING\n    elif isinstance(v, bool) and v is False:\n        # effectively we disable logging until an error happens\n        return logging.ERROR\n    elif v.upper() in logging_levels:\n        return logging_levels[v.upper()]\n    else:\n        raise argparse.ArgumentTypeError('Meaningful logging level expected.')\n
    "},{"location":"reference/helpers/#ties.helpers.get_new_atom_names","title":"get_new_atom_names","text":"
    get_new_atom_names(atoms, name_counter=None)\n

    todo - add unit tests

    @parameter/returns name_counter: a dictionary with atom as the key such as 'N', 'C', etc, the counter keeps track of the last used counter for each name. Empty means that the counting will start from 1. input atoms: mdanalysis atoms

    Source code in ties/helpers.py
    def get_new_atom_names(atoms, name_counter=None):\n    \"\"\"\n    todo - add unit tests\n\n    @parameter/returns name_counter: a dictionary with atom as the key such as 'N', 'C', etc,\n    the counter keeps track of the last used counter for each name.\n    Empty means that the counting will start from 1.\n    input atoms: mdanalysis atoms\n    \"\"\"\n    if name_counter is None:\n        name_counter = {}\n\n    # {new_uniqe_name: old_atom_name}\n    reverse_renaming_map = {}\n\n    for atom in atoms:\n        # count the letters before any digit\n        letter_count = 0\n        for letter in atom.name:\n            if not letter.isalpha():\n                break\n\n            letter_count += 1\n\n        # use max 3 letters from the atom name\n        letter_count = min(letter_count, 3)\n\n        letters = atom.name[:letter_count]\n\n        # how many atoms do we have with these letters? ie C1, C2, C3 -> 3\n        last_used_counter = name_counter.get(letters, 0) + 1\n\n        # rename\n        new_name = letters + str(last_used_counter)\n\n        # if the name is longer than 4 character,\n        # shorten the number of letters\n        if len(new_name) > 4:\n            # the name is too long, use only the first character\n            new_name = letters[:4-len(str(last_used_counter))] + str(last_used_counter)\n\n            # we assume that there is fewer than 1000 atoms with that name\n            assert len(str(last_used_counter)) < 1000\n\n        reverse_renaming_map[new_name] = atom.name\n\n        atom.name = new_name\n\n        # update the counter\n        name_counter[letters] = last_used_counter\n\n    return name_counter, reverse_renaming_map\n
    "},{"location":"reference/helpers/#ties.helpers.get_atom_names_counter","title":"get_atom_names_counter","text":"
    get_atom_names_counter(atoms)\n

    name_counter: a dictionary with atom as the key such as 'N', 'C', etc, the counter keeps track of the last used counter for each name. Ie if there are C1, C2, C3, this will return {'C':3} as the last counter.

    Source code in ties/helpers.py
    def get_atom_names_counter(atoms):\n    \"\"\"\n    name_counter: a dictionary with atom as the key such as 'N', 'C', etc,\n    the counter keeps track of the last used counter for each name.\n    Ie if there are C1, C2, C3, this will return {'C':3} as the last counter.\n    \"\"\"\n    name_counter = {}\n\n    for atom in atoms:\n        # get the first letters that is not a character\n        afterLetters = [i for i, l in enumerate(atom.name) if l.isalpha()][-1] + 1\n\n        atom_name = atom.name[:afterLetters]\n        atom_number = int(atom.name[afterLetters:])\n\n        # we are starting the counter from 0 as we always add 1 later on\n        last_used_counter = name_counter.get(atom_name, 0)\n\n        # update the counter\n        name_counter[atom_name] = max(last_used_counter + 1, atom_number)\n\n    return name_counter\n
    "},{"location":"reference/helpers/#ties.helpers.parse_frcmod_sections","title":"parse_frcmod_sections","text":"
    parse_frcmod_sections(filename)\n

    Copied from the previous TIES. It's simpler and this approach must be fine then.

    Source code in ties/helpers.py
    def parse_frcmod_sections(filename):\n    \"\"\"\n    Copied from the previous TIES. It's simpler and this approach must be fine then.\n    \"\"\"\n    frcmod_info = {}\n    section = 'REMARK'\n\n    with open(filename) as F:\n        for line in F:\n            start_line = line[0:9].strip()\n\n            if start_line in ['MASS', 'BOND', 'IMPROPER',\n                              'NONBON', 'ANGLE', 'DIHE']:\n                section = start_line\n                frcmod_info[section] = []\n            elif line.strip() and section != 'REMARK':\n                frcmod_info[section].append(line)\n\n    return frcmod_info\n
    "},{"location":"reference/ligand/","title":" ligand","text":""},{"location":"reference/ligand/#ties.ligand","title":"ligand","text":"

    Classes:

    • Ligand \u2013

      The ligand helper class. Helps to load and manage the different copies of the ligand file.

    "},{"location":"reference/ligand/#ties.ligand.Ligand","title":"Ligand","text":"
    Ligand(ligand, config=None, save=True)\n

    The ligand helper class. Helps to load and manage the different copies of the ligand file. Specifically, it tracks the different copies of the original input files as it is transformed (e.g. charge assignment).

    :param ligand: ligand filepath :type ligand: string :param config: Optional configuration from which the relevant ligand settings can be used :type config: :class:Config :param save: write a file with unique atom names for further inspection :type save: bool

    Methods:

    • convert_acprep_to_mol2 \u2013

      If the file is not a prep/ac file, this function does not do anything.

    • are_atom_names_correct \u2013

      Checks if atom names:

    • correct_atom_names \u2013

      Ensure that each atom name:

    • antechamber_prepare_mol2 \u2013

      Converts the ligand into a .mol2 format.

    • removeDU_atoms \u2013

      Ambertools antechamber creates sometimes DU dummy atoms.

    • generate_frcmod \u2013

      params

    • overwrite_coordinates_with \u2013

      Load coordinates from another file and overwrite the coordinates in the current file.

    Attributes:

    • renaming_map \u2013

      Otherwise, key: newName, value: oldName.

    Source code in ties/ligand.py
    def __init__(self, ligand, config=None, save=True):\n    \"\"\"Constructor method\n    \"\"\"\n\n    self.save = save\n    # save workplace root\n    self.config = Config() if config is None else config\n    self.config.ligand_files = ligand\n\n    self.original_input = Path(ligand).absolute()\n\n    # internal name without an extension\n    self.internal_name = self.original_input.stem\n\n    # ligand names have to be unique\n    if self.internal_name in Ligand._USED_FILENAMES and self.config.uses_cmd:\n        raise ValueError(f'ERROR: the ligand filename {self.internal_name} is not unique in the list of ligands. ')\n    else:\n        Ligand._USED_FILENAMES.add(self.internal_name)\n\n    # last used representative Path file\n    self.current = self.original_input\n\n    # internal index\n    # TODO - move to config\n    self.index = Ligand.LIG_COUNTER\n    Ligand.LIG_COUNTER += 1\n\n    self._renaming_map = None\n    self.ligand_with_uniq_atom_names = None\n\n    # If .ac format (ambertools, similar to .pdb), convert it to .mol2 using antechamber\n    self.convert_acprep_to_mol2()\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.renaming_map","title":"renaming_map property writable","text":"
    renaming_map\n

    Otherwise, key: newName, value: oldName.

    If None, means no renaming took place.

    "},{"location":"reference/ligand/#ties.ligand.Ligand.convert_acprep_to_mol2","title":"convert_acprep_to_mol2","text":"
    convert_acprep_to_mol2()\n

    If the file is not a prep/ac file, this function does not do anything. Antechamber is called to convert the .prepi/.prep/.ac file into a .mol2 file.

    Returns: the name of the original file, or of it was .prepi, a new filename with .mol2

    Source code in ties/ligand.py
    def convert_acprep_to_mol2(self):\n    \"\"\"\n    If the file is not a prep/ac file, this function does not do anything.\n    Antechamber is called to convert the .prepi/.prep/.ac file into a .mol2 file.\n\n    Returns: the name of the original file, or of it was .prepi, a new filename with .mol2\n    \"\"\"\n\n    if self.current.suffix.lower() not in ('.ac', '.prep'):\n        return\n\n    filetype = {'.ac': 'ac', '.prep': 'prepi'}[self.current.suffix.lower()]\n\n    cwd = self.config.lig_acprep_dir / self.internal_name\n    if not cwd.is_dir():\n        cwd.mkdir(parents=True, exist_ok=True)\n\n    # prepare the .mol2 files with antechamber (ambertools), assign BCC charges if necessary\n    logger.debug(f'Antechamber: converting {filetype} to mol2')\n    new_current = cwd / (self.internal_name + '.mol2')\n\n    log_filename = cwd / \"antechamber_conversion.log\"\n    with open(log_filename, 'w') as LOG:\n        try:\n            subprocess.run([self.config.ambertools_antechamber,\n                            '-i', self.current, '-fi', filetype,\n                            '-o', new_current, '-fo', 'mol2',\n                            '-dr', self.config.antechamber_dr],\n                           stdout=LOG, stderr=LOG,\n                           check=True, text=True,\n                           cwd=cwd, timeout=30)\n        except subprocess.CalledProcessError as E:\n            raise Exception('An error occurred during the antechamber conversion from .ac to .mol2 data type. '\n                            f'The output was saved in the directory: {cwd}'\n                            f'Please see the log file for the exact error information: {log_filename}') from E\n\n    # update\n    self.original_ac = self.current\n    self.current = new_current\n    logger.debug(f'Converted .ac file to .mol2. The location of the new file: {self.current}')\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.are_atom_names_correct","title":"are_atom_names_correct","text":"
    are_atom_names_correct()\n
    Checks if atom names
    • are unique
    • have a correct format \"LettersNumbers\" e.g. C17
    Source code in ties/ligand.py
    def are_atom_names_correct(self):\n    \"\"\"\n    Checks if atom names:\n     - are unique\n     - have a correct format \"LettersNumbers\" e.g. C17\n    \"\"\"\n    ligand = parmed.load_file(str(self.current), structure=True)\n    atom_names = [a.name for a in ligand.atoms]\n\n    are_uniqe = len(set(atom_names)) == len(atom_names)\n\n    return are_uniqe and self._do_atom_names_have_correct_format(atom_names)\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand._do_atom_names_have_correct_format","title":"_do_atom_names_have_correct_format staticmethod","text":"
    _do_atom_names_have_correct_format(names)\n

    Check if the atom name is followed by a number, e.g. \"C15\" Note that the full atom name cannot be more than 4 characters. This is because the PDB format does not allow for more characters which can lead to inconsistencies.

    :param names: a list of atom names :type names: list[str] :return True if they all follow the correct format.

    Source code in ties/ligand.py
    @staticmethod\ndef _do_atom_names_have_correct_format(names):\n    \"\"\"\n    Check if the atom name is followed by a number, e.g. \"C15\"\n    Note that the full atom name cannot be more than 4 characters.\n    This is because the PDB format does not allow for more\n    characters which can lead to inconsistencies.\n\n    :param names: a list of atom names\n    :type names: list[str]\n    :return True if they all follow the correct format.\n    \"\"\"\n    for name in names:\n        # cannot exceed 4 characters\n        if len(name) > 4:\n            return False\n\n        # count letters before any digit\n        letter_count = 0\n        for letter in name:\n            if not letter.isalpha():\n                break\n\n            letter_count += 1\n\n        # at least one character\n        if letter_count == 0:\n            return False\n\n        # extrac the number suffix\n        atom_number = name[letter_count:]\n        try:\n            int(atom_number)\n        except:\n            return False\n\n    return True\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.correct_atom_names","title":"correct_atom_names","text":"
    correct_atom_names()\n
    Ensure that each atom name
    • is unique
    • has letter followed by digits
    • has max 4 characters

    E.g. C17, NX23

    :param self.save: if the path is provided, the updated file will be saved with the unique names and a handle to the new file (ParmEd) will be returned.

    Source code in ties/ligand.py
    def correct_atom_names(self):\n    \"\"\"\n    Ensure that each atom name:\n     - is unique\n     - has letter followed by digits\n     - has max 4 characters\n    E.g. C17, NX23\n\n    :param self.save: if the path is provided, the updated file\n        will be saved with the unique names and a handle to the new file (ParmEd) will be returned.\n    \"\"\"\n    if self.are_atom_names_correct():\n        return\n\n    logger.debug(f'Ligand {self.internal_name} will have its atom names renamed. ')\n\n    ligand = parmed.load_file(str(self.current), structure=True)\n\n    logger.debug(f'Atom names in the molecule ({self.original_input}/{self.internal_name}) are either not unique '\n          f'or do not follow NameDigit format (e.g. C15). Renaming')\n    _, renaming_map = ties.helpers.get_new_atom_names(ligand.atoms)\n    self._renaming_map = renaming_map\n    logger.debug(f'Rename map: {renaming_map}')\n\n    # save the output here\n    os.makedirs(self.config.lig_unique_atom_names_dir, exist_ok=True)\n\n    ligand_with_uniq_atom_names = self.config.lig_unique_atom_names_dir / (self.internal_name + self.current.suffix)\n    if self.save:\n        ligand.save(str(ligand_with_uniq_atom_names))\n\n    self.ligand_with_uniq_atom_names = ligand_with_uniq_atom_names\n    self.parmed = ligand\n    # this object is now represented by the updated ligand\n    self.current = ligand_with_uniq_atom_names\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.antechamber_prepare_mol2","title":"antechamber_prepare_mol2","text":"
    antechamber_prepare_mol2(**kwargs)\n

    Converts the ligand into a .mol2 format.

    BCC charges are generated if missing or requested. It calls antechamber (the charge type -c is not used if user prefers to use their charges). Any DU atoms created in the antechamber call are removed.

    :param atom_type: Atom type bla bla :type atom_type: :param net_charge: :type net_charge: int

    Source code in ties/ligand.py
    def antechamber_prepare_mol2(self, **kwargs):\n    \"\"\"\n    Converts the ligand into a .mol2 format.\n\n    BCC charges are generated if missing or requested.\n    It calls antechamber (the charge type -c is not used if user prefers to use their charges).\n    Any DU atoms created in the antechamber call are removed.\n\n    :param atom_type: Atom type bla bla\n    :type atom_type:\n    :param net_charge:\n    :type net_charge: int\n    \"\"\"\n    self.config.set_configs(**kwargs)\n\n    if self.config.ligands_contain_q or not self.config.antechamber_charge_type:\n        logger.info(f'Antechamber: User-provided atom charges will be reused ({self.current.name})')\n\n    mol2_cwd = self.config.lig_dir / self.internal_name\n\n    # prepare the directory\n    mol2_cwd.mkdir(parents=True, exist_ok=True)\n    mol2_target = mol2_cwd / f'{self.internal_name}.mol2'\n\n    # do not redo if the target file exists\n    if not (mol2_target).is_file():\n        log_filename = mol2_cwd / \"antechamber.log\"\n        with open(log_filename, 'w') as LOG:\n            try:\n                cmd = [self.config.ambertools_antechamber,\n                       '-i', self.current,\n                       '-fi', self.current.suffix[1:],\n                       '-o', mol2_target,\n                       '-fo', 'mol2',\n                       '-at', self.config.ligand_ff_name,\n                       '-nc', str(self.config.ligand_net_charge),\n                       '-dr', str(self.config.antechamber_dr)\n                       ] +  self.config.antechamber_charge_type\n                subprocess.run(cmd,\n                               cwd=mol2_cwd,\n                               stdout=LOG, stderr=LOG,\n                               check=True, text=True,\n                               timeout=60 * 30  # 30 minutes\n                               )\n            except subprocess.CalledProcessError as ProcessError:\n                raise Exception(f'Could not convert the ligand into .mol2 file with antechamber. '\n                                f'See the log and its directory: {log_filename} . '\n                                f'Command used: {\" \".join(map(str, cmd))}') from ProcessError\n        logger.debug(f'Converted {self.original_input} into .mol2, Log: {log_filename}')\n    else:\n        logger.info(f'File {mol2_target} already exists. Skipping. ')\n\n    self.antechamber_mol2 = mol2_target\n    self.current = mol2_target\n\n    # remove any DUMMY DU atoms in the .mol2 atoms\n    self.removeDU_atoms()\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.removeDU_atoms","title":"removeDU_atoms","text":"
    removeDU_atoms()\n

    Ambertools antechamber creates sometimes DU dummy atoms. These are not created when BCC charges are computed from scratch. They are only created if you reuse existing charges. They appear to be a side effect. We remove the dummy atoms therefore.

    Source code in ties/ligand.py
    def removeDU_atoms(self):\n    \"\"\"\n    Ambertools antechamber creates sometimes DU dummy atoms.\n    These are not created when BCC charges are computed from scratch.\n    They are only created if you reuse existing charges.\n    They appear to be a side effect. We remove the dummy atoms therefore.\n    \"\"\"\n    mol2 = parmed.load_file(str(self.current), structure=True)\n    # check if there are any DU atoms\n    has_DU = any(a.type == 'DU' for a in mol2.atoms)\n    if not has_DU:\n        return\n\n    # make a backup copy before (to simplify naming)\n    shutil.move(self.current, self.current.parent / ('lig.beforeRemovingDU' + self.current.suffix))\n\n    # remove DU type atoms and save the file\n    for atom in mol2.atoms:\n        if atom.name != 'DU':\n            continue\n\n        atom.residue.delete_atom(atom)\n    # save the updated molecule\n    mol2.save(str(self.current))\n    logger.debug('Removed dummy atoms with type \"DU\"')\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.generate_frcmod","title":"generate_frcmod","text":"
    generate_frcmod(**kwargs)\n

    params - parmchk2 - atom_type

    Source code in ties/ligand.py
    def generate_frcmod(self, **kwargs):\n    \"\"\"\n        params\n         - parmchk2\n         - atom_type\n    \"\"\"\n    self.config.set_configs(**kwargs)\n\n    logger.debug(f'INFO: frcmod for {self} was computed before. Not repeating.')\n    if hasattr(self, 'frcmod'):\n        return\n\n    # fixme - work on the file handles instaed of the constant stitching\n    logger.debug(f'Parmchk2: generate the .frcmod for {self.internal_name}.mol2')\n\n    # prepare cwd\n    cwd = self.config.lig_frcmod_dir / self.internal_name\n    if not cwd.is_dir():\n        cwd.mkdir(parents=True, exist_ok=True)\n\n    target_frcmod = f'{self.internal_name}.frcmod'\n    log_filename = cwd / \"parmchk2.log\"\n    with open(log_filename, 'w') as LOG:\n        try:\n            subprocess.run([self.config.ambertools_parmchk2,\n                            '-i', self.current,\n                            '-o', target_frcmod,\n                            '-f', 'mol2',\n                            '-s', self.config.ligand_ff_name],\n                           stdout=LOG, stderr=LOG,\n                           check= True, text=True,\n                           cwd= cwd, timeout=20,  # 20 seconds\n                            )\n        except subprocess.CalledProcessError as E:\n            raise Exception(f\"GAFF Error: Could not generate FRCMOD for file: {self.current} . \"\n                            f'See more here: {log_filename}') from E\n\n    logger.debug(f'Parmchk2: created frcmod: {target_frcmod}')\n    self.frcmod = cwd / target_frcmod\n
    "},{"location":"reference/ligand/#ties.ligand.Ligand.overwrite_coordinates_with","title":"overwrite_coordinates_with","text":"
    overwrite_coordinates_with(file, output_file)\n

    Load coordinates from another file and overwrite the coordinates in the current file.

    Source code in ties/ligand.py
    def overwrite_coordinates_with(self, file, output_file):\n    \"\"\"\n    Load coordinates from another file and overwrite the coordinates in the current file.\n    \"\"\"\n\n    # load the current atoms with ParmEd\n    template = parmed.load_file(str(self.current), structure=True)\n\n    # load the file with the coordinates we want to use\n    coords = parmed.load_file(str(file), structure=True)\n\n    # fixme: use the atom names\n    by_atom_name = True\n    by_index = False\n    by_general_atom_type = False\n\n    # mol2_filename will be overwritten!\n    logger.info(f'Writing to {self.current} the coordinates from {file}. ')\n\n    coords_sum = np.sum(coords.atoms.positions)\n\n    if by_atom_name and by_index:\n        raise ValueError('Cannot have both. They are exclusive')\n    elif not by_atom_name and not by_index:\n        raise ValueError('Either option has to be selected.')\n\n    if by_general_atom_type:\n        for mol2_atom in template.atoms:\n            found_match = False\n            for ref_atom in coords.atoms:\n                if element_from_type[mol2_atom.type.upper()] == element_from_type[ref_atom.type.upper()]:\n                    found_match = True\n                    mol2_atom.position = ref_atom.position\n                    break\n            assert found_match, \"Could not find the following atom in the original file: \" + mol2_atom.name\n    if by_atom_name:\n        for mol2_atom in template.atoms:\n            found_match = False\n            for ref_atom in coords.atoms:\n                if mol2_atom.name.upper() == ref_atom.name.upper():\n                    found_match = True\n                    mol2_atom.position = ref_atom.position\n                    break\n            assert found_match, \"Could not find the following atom name across the two files: \" + mol2_atom.name\n    elif by_index:\n        for mol2_atom, ref_atom in zip(template.atoms, coords.atoms):\n            atype = element_from_type[mol2_atom.type.upper()]\n            reftype = element_from_type[ref_atom.type.upper()]\n            if atype != reftype:\n                raise Exception(\n                    f\"The found general type {atype} does not equal to the reference type {reftype} \")\n\n            mol2_atom.position = ref_atom.position\n\n    if np.testing.assert_almost_equal(coords_sum, np.sum(mda_template.atoms.positions), decimal=2):\n        logger.debug('Different positions sums:', coords_sum, np.sum(mda_template.atoms.positions))\n        raise Exception('Copying of the coordinates did not work correctly')\n\n    # save the output file\n    mda_template.atoms.write(output_file)\n
    "},{"location":"reference/ligandmap/","title":" ligandmap","text":""},{"location":"reference/ligandmap/#ties.ligandmap","title":"ligandmap","text":"

    Classes:

    • LigandMap \u2013

      Work on a list of morphs and use their information to generate a each to each map.

    "},{"location":"reference/ligandmap/#ties.ligandmap.LigandMap","title":"LigandMap","text":"
    LigandMap(ligands, morphs)\n

    Work on a list of morphs and use their information to generate a each to each map. This class then uses the information for * clustering, * path finding (traveling salesman, minimum spanning tree) * visualisation, etc.

    Methods:

    • generate_map \u2013

      Use the underlying morphs to extract the each to each cases.

    Source code in ties/ligandmap.py
    def __init__(self, ligands, morphs):\n    self.morphs = morphs\n    self.ligands = ligands\n    # similarity map\n    self.map = None\n    self.map_weights = None\n    self.graph = None\n
    "},{"location":"reference/ligandmap/#ties.ligandmap.LigandMap.generate_map","title":"generate_map","text":"
    generate_map()\n

    Use the underlying morphs to extract the each to each cases.

    Source code in ties/ligandmap.py
    def generate_map(self):\n    \"\"\"\n    Use the underlying morphs to extract the each to each cases.\n    \"\"\"\n    # a simple 2D map of the ligands\n    self.map = [list(range(len(self.ligands))) for l1 in range(len(self.ligands))]\n\n    # weights based on the size of the superimposed topology\n    self.map_weights = numpy.zeros([len(self.ligands), len(self.ligands)])\n    for morph in self.morphs:\n        self.map[morph.ligA.index][morph.ligZ.index] = morph\n        self.map[morph.ligZ.index][morph.ligA.index] = morph\n\n        matched_left, matched_right, disappearing_atoms, appearing_atoms = morph.overlap_fractions()\n        # use the average number of matched fractions in both ligands\n        weight = 1 - (matched_left + matched_right) / 2.0\n        self.map_weights[morph.ligA.index][morph.ligZ.index] = weight\n        self.map_weights[morph.ligZ.index][morph.ligA.index] = weight\n\n        # update also the morph\n        morph.set_distance(weight)\n
    "},{"location":"reference/md/","title":" md","text":""},{"location":"reference/md/#ties.md","title":"md","text":"

    Classes:

    • MD \u2013

      Class a wrapper around TIES_MD API that exposes a simplified interface.

    "},{"location":"reference/md/#ties.md.MD","title":"MD","text":"
    MD(sim_dir, sim_name='complex', fast=False)\n

    Class a wrapper around TIES_MD API that exposes a simplified interface.

    :param sim_dir: str, points to where the simulation is running i.e where the TIES.cfg file is. :param sim_name: str, the prefix to the input param and topo file i.e complex for complex.pdb/prmtop. :param fast: boolean, if True the setting is TIES.cfg will be overwritten with minimal TIES protocol.

    Methods:

    • run \u2013

      Wrapper for TIES_MD.TIES.run()

    • analysis \u2013

      Wrapper for TIES_MD.ties_analysis()

    Source code in ties/md.py
    def __init__(self, sim_dir, sim_name='complex', fast=False):\n    cwd = os.getcwd()\n    self.sim_dir = os.path.join(cwd, sim_dir)\n    self.analysis_dir = os.path.join(self.sim_dir, '..', '..', '..')\n    #This is the main TIES MD object we call it options as the user interacts with this object to change options\n    self.options = TIES(cwd=self.sim_dir, exp_name=sim_name)\n\n    if fast:\n        #modify md to cut as many corners as possible i.e. reps, windows, sim length\n        self.options.total_reps = 3\n        self.options.global_lambdas = [0.00, 0.05, 0.1, 0.3, 0.5, 0.7, 0.9, 0.95, 1.00]\n        self.options.sampling_per_window = 2*unit.nanoseconds()\n\n    self.options.setup()\n
    "},{"location":"reference/md/#ties.md.MD.run","title":"run","text":"
    run()\n

    Wrapper for TIES_MD.TIES.run()

    :return: None

    Source code in ties/md.py
    def run(self):\n    '''\n    Wrapper for TIES_MD.TIES.run()\n\n    :return: None\n    '''\n    self.options.run()\n
    "},{"location":"reference/md/#ties.md.MD.analysis","title":"analysis","text":"
    analysis(legs, analysis_cfg='./analysis.cfg')\n

    Wrapper for TIES_MD.ties_analysis()

    :param legs: list of strings, these are the thermodynamic legs of the simulation i.e. ['lig', 'com']. :param analysis_cfg: str, for what the analysis config file is called.

    :return: None

    Source code in ties/md.py
    def analysis(self, legs, analysis_cfg='./analysis.cfg'):\n    '''\n    Wrapper for TIES_MD.ties_analysis()\n\n    :param legs: list of strings, these are the thermodynamic legs of the simulation i.e. ['lig', 'com'].\n    :param analysis_cfg: str, for what the analysis config file is called.\n\n    :return: None\n    '''\n    os.chdir(self.analysis_dir)\n    if not os.path.exists('exp.dat'):\n        ties_analysis.make_exp(verbose=False)\n\n    #read the experimental data\n    with open('exp.dat') as f:\n        data = f.read()\n    exp_js = json.loads(data)\n\n    ana_cfg = ties_analysis.Config(analysis_cfg)\n    ana_cfg.simulation_legs = legs\n    ana_cfg.exp_data = exp_js\n    ana = ties_analysis.Analysis(ana_cfg)\n    ana.run()\n
    "},{"location":"reference/namd_generator/","title":" namd_generator","text":""},{"location":"reference/namd_generator/#ties.namd_generator","title":"namd_generator","text":"

    Load two ligands, run the topology superimposer, and then using the results, generate the NAMD input files.

    frcmod file format: http://ambermd.org/FileFormats.php#frcmod

    "},{"location":"reference/pair/","title":" pair","text":""},{"location":"reference/pair/#ties.pair","title":"pair","text":"

    Classes:

    • Pair \u2013

      Facilitates the creation of morphs.

    "},{"location":"reference/pair/#ties.pair.Pair","title":"Pair","text":"
    Pair(ligA, ligZ, config=None, **kwargs)\n

    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)\n

    Methods:

    • superimpose \u2013

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

    • set_suptop \u2013

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

    • make_atom_names_unique \u2013

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

    • check_json_file \u2013

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

    • merge_frcmod_files \u2013

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

    • overlap_fractions \u2013

      Calculate the size of the common area.

    Source code in ties/pair.py
    def __init__(self, ligA, ligZ, config=None, **kwargs):\n    \"\"\"\n    Please use the Config class for the documentation of the possible kwargs.\n    Each kwarg is passed to the config class.\n\n    fixme - list all relevant kwargs here\n\n        param ligand_net_charge: integer, net charge of each ligand (has to be the same)\n    \"\"\"\n\n    # create a new config if it is not provided\n    self.config = ties.config.Config() if config is None else config\n\n    # channel all config variables to the config class\n    self.config.set_configs(**kwargs)\n\n    # tell Config about the ligands if necessary\n    if self.config.ligands is None:\n        self.config.ligands = [ligA, ligZ]\n\n    # create ligands if they're just paths\n    if isinstance(ligA, ties.ligand.Ligand):\n        self.ligA = ligA\n    else:\n        self.ligA = ties.ligand.Ligand(ligA, self.config)\n\n    if isinstance(ligZ, ties.ligand.Ligand):\n        self.ligZ = ligZ\n    else:\n        self.ligZ = ties.ligand.Ligand(ligZ, self.config)\n\n    # initialise the handles to the molecules that morph\n    self.current_ligA = self.ligA.current\n    self.current_ligZ = self.ligZ.current\n\n    self.internal_name = f'{self.ligA.internal_name}_{self.ligZ.internal_name}'\n    self.mol2 = None\n    self.pdb = None\n    self.summary = None\n    self.suptop = None\n    self.mda_l1 = None\n    self.mda_l2 = None\n    self.distance = None\n
    "},{"location":"reference/pair/#ties.pair.Pair.superimpose","title":"superimpose","text":"
    superimpose(**kwargs)\n

    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
    def superimpose(self, **kwargs):\n    \"\"\"\n    Please see :class:`Config` class for the documentation of kwargs. The passed kwargs overwrite the config\n    object passed in the constructor.\n\n    fixme - list all relevant kwargs here\n\n    :param use_element_in_superimposition: bool whether the superimposition should rely on the element initially,\n        before refining the results with a more specific check of the atom type.\n    :param manually_matched_atom_pairs:\n    :param manually_mismatched_pairs:\n    :param redistribute_q_over_unmatched:\n    \"\"\"\n    self.config.set_configs(**kwargs)\n\n    # use ParmEd to load the files\n    # fixme - move this to the Morph class instead of this place,\n    # fixme - should not squash all messsages. For example, wrong type file should not be squashed\n    leftlig_atoms, leftlig_bonds, rightlig_atoms, rightlig_bonds, parmed_ligA, parmed_ligZ = \\\n        get_atoms_bonds_from_mol2(self.current_ligA, self.current_ligZ,\n                                  use_general_type=self.config.use_element_in_superimposition)\n    # fixme - manual match should be improved here and allow for a sensible format.\n\n    # in case the atoms were renamed, pass the names via the map renaming map\n    # TODO\n    # ligZ_old_new_atomname_map\n    new_mismatch_names = []\n    for a, z in self.config.manually_mismatched_pairs:\n        new_names = (self.ligA.rev_renaming_map[a], self.ligZ.rev_renaming_map[z])\n        logger.debug(f'Selecting mismatching atoms. The mismatch {(a, z)}) was renamed to {new_names}')\n        new_mismatch_names.append(new_names)\n\n    # assign\n    # fixme - Ideally I would reuse the ParmEd data for this,\n    # ParmEd can use bonds if they are present - fixme\n    # map atom IDs to their objects\n    ligand1_nodes = {}\n    for atomNode in leftlig_atoms:\n        ligand1_nodes[atomNode.id] = atomNode\n    # link them together\n    for nfrom, nto, btype in leftlig_bonds:\n        ligand1_nodes[nfrom].bind_to(ligand1_nodes[nto], btype)\n\n    ligand2_nodes = {}\n    for atomNode in rightlig_atoms:\n        ligand2_nodes[atomNode.id] = atomNode\n    for nfrom, nto, btype in rightlig_bonds:\n        ligand2_nodes[nfrom].bind_to(ligand2_nodes[nto], btype)\n\n    # fixme - this should be moved out of here,\n    #  ideally there would be a function in the main interface for this\n    manual_match = [] if self.config.manually_matched_atom_pairs is None else self.config.manually_matched_atom_pairs\n    starting_node_pairs = []\n    for l_aname, r_aname in manual_match:\n        # find the starting node pairs, ie the manually matched pair(s)\n        found_left_node = None\n        for id, ln in ligand1_nodes.items():\n            if l_aname == ln.name:\n                found_left_node = ln\n        if found_left_node is None:\n            raise ValueError(f'Manual Matching: could not find an atom name: \"{l_aname}\" in the left molecule')\n\n        found_right_node = None\n        for id, ln in ligand2_nodes.items():\n            if r_aname == ln.name:\n                found_right_node = ln\n        if found_right_node is None:\n            raise ValueError(f'Manual Matching: could not find an atom name: \"{r_aname}\" in the right molecule')\n\n        starting_node_pairs.append([found_left_node, found_right_node])\n\n    if starting_node_pairs:\n        logger.debug(f'Starting nodes will be used: {starting_node_pairs}')\n\n    # fixme - simplify to only take the ParmEd as input\n    suptop = superimpose_topologies(ligand1_nodes.values(), ligand2_nodes.values(),\n                                     disjoint_components=self.config.allow_disjoint_components,\n                                     net_charge_filter=True,\n                                     pair_charge_atol=self.config.atom_pair_q_atol,\n                                     net_charge_threshold=self.config.net_charge_threshold,\n                                     redistribute_charges_over_unmatched=self.config.redistribute_q_over_unmatched,\n                                     ignore_charges_completely=self.config.ignore_charges_completely,\n                                     ignore_bond_types=True,\n                                     ignore_coords=False,\n                                     align_molecules=self.config.align_molecules_using_mcs,\n                                     use_general_type=self.config.use_element_in_superimposition,\n                                     # fixme - not the same ... use_element_in_superimposition,\n                                     use_only_element=False,\n                                     check_atom_names_unique=True,  # fixme - remove?\n                                     starting_pairs_heuristics=self.config.starting_pairs_heuristics,  # fixme - add to config\n                                     force_mismatch=new_mismatch_names,\n                                     starting_node_pairs=starting_node_pairs,\n                                     parmed_ligA=parmed_ligA, parmed_ligZ=parmed_ligZ,\n                                     starting_pair_seed=self.config.superimposition_starting_pair,\n                                     config=self.config)\n\n    self.set_suptop(suptop, parmed_ligA, parmed_ligZ)\n    # attach the used config to the suptop\n\n    if suptop is not None:\n        suptop.config = self.config\n        # attach the morph to the suptop\n        suptop.morph = self\n\n    return suptop\n
    "},{"location":"reference/pair/#ties.pair.Pair.set_suptop","title":"set_suptop","text":"
    set_suptop(suptop, parmed_ligA, parmed_ligZ)\n

    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
    def set_suptop(self, suptop, parmed_ligA, parmed_ligZ):\n    \"\"\"\n    Attach a SuperimposedTopology object along with the ParmEd objects for the ligA and ligZ.\n\n    :param suptop: :class:`SuperimposedTopology`\n    :param parmed_ligA: An ParmEd for the ligA\n    :param parmed_ligZ: An ParmEd for the ligZ\n    \"\"\"\n    self.suptop = suptop\n    self.parmed_ligA = parmed_ligA\n    self.parmed_ligZ = parmed_ligZ\n
    "},{"location":"reference/pair/#ties.pair.Pair.make_atom_names_unique","title":"make_atom_names_unique","text":"
    make_atom_names_unique(out_ligA_filename=None, out_ligZ_filename=None, save=True)\n

    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
    def make_atom_names_unique(self, out_ligA_filename=None, out_ligZ_filename=None, save=True):\n    \"\"\"\n    Ensure that each that atoms across the two ligands have unique names.\n\n    While renaming atoms, start with the element (C, N, ..) followed by\n     the count so far (e.g. C1, C2, N1).\n\n    Resnames are set to \"INI\" and \"FIN\", this is useful for the hybrid dual topology.\n\n    :param out_ligA_filename: The new filenames for the ligands with renamed atoms. If None, the default\n        naming convention is used.\n    :type out_ligA_filename: string or bool\n    :param out_ligZ_filename: The new filenames for the ligands with renamed atoms. If None, the default\n        naming convention is used.\n    :type out_ligZ_filename: string or bool\n    :param save: Whether to save to the disk the ligands after renaming the atoms\n    :type save: bool\n    \"\"\"\n\n    # The A ligand is a template for the renaming\n    self.ligA.correct_atom_names()\n\n    # load both ligands\n    left = parmed.load_file(str(self.ligA.current), structure=True)\n    right = parmed.load_file(str(self.ligZ.current), structure=True)\n\n    common_atom_names = {a.name for a in right.atoms}.intersection({a.name for a in left.atoms})\n    atom_names_overlap = len(common_atom_names) > 0\n\n    if atom_names_overlap or not self.ligZ.are_atom_names_correct():\n        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. ')\n        if atom_names_overlap:\n            logger.debug(f'Common atom names: {common_atom_names}')\n        name_counter_L_nodes = ties.helpers.get_atom_names_counter(left.atoms)\n        _, renaming_map = ties.helpers.get_new_atom_names(right.atoms, name_counter=name_counter_L_nodes)\n        self.ligZ.renaming_map = renaming_map\n\n    # rename the residue names to INI and FIN\n    for atom in left.atoms:\n        atom.residue = 'INI'\n    for atom in right.atoms:\n        atom.residue = 'FIN'\n\n    # fixme - instead of using the save parameter, have a method pair.save(filename1, filename2) and\n    #  call it when necessary.\n    # prepare the destination directory\n    if not save:\n        return\n\n    if out_ligA_filename is None:\n        cwd = self.config.pair_unique_atom_names_dir / f'{self.ligA.internal_name}_{self.ligZ.internal_name}'\n        cwd.mkdir(parents=True, exist_ok=True)\n\n        self.current_ligA = cwd / (self.ligA.internal_name + '.mol2')\n        self.current_ligZ = cwd / (self.ligZ.internal_name + '.mol2')\n    else:\n        self.current_ligA = out_ligA_filename\n        self.current_ligZ = out_ligZ_filename\n\n    # save the updated atom names\n    left.save(str(self.current_ligA))\n    right.save(str(self.current_ligZ))\n
    "},{"location":"reference/pair/#ties.pair.Pair.check_json_file","title":"check_json_file","text":"
    check_json_file()\n

    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
    def check_json_file(self):\n    \"\"\"\n    Performance optimisation in case TIES is rerun again. Return the first matched atoms which\n    can be used as a seed for the superimposition.\n\n    :return: If the superimposition was computed before, and the .json file is available,\n        gets one of the matched atoms.\n    :rtype: [(ligA_atom, ligZ_atom)]\n    \"\"\"\n    matching_json = self.config.workdir / f'fep_{self.ligA.internal_name}_{self.ligZ.internal_name}.json'\n    if not matching_json.is_file():\n        return None\n\n    return [list(json.load(matching_json.open())['matched'].items())[0]]\n
    "},{"location":"reference/pair/#ties.pair.Pair.merge_frcmod_files","title":"merge_frcmod_files","text":"
    merge_frcmod_files(ligcom=None)\n

    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
    def merge_frcmod_files(self, ligcom=None):\n    \"\"\"\n    Merges the .frcmod files generated for each ligand separately, simply by adding them together.\n\n    The duplication has no effect on the final generated topology parm7 top file.\n\n    We are also testing the .frcmod here with the user's force field in order to check if\n    the merge works correctly.\n\n    :param ligcom: Either \"lig\" if only ligands are present, or \"com\" if the complex is present.\n        Helps with the directory structure.\n    :type ligcom: string \"lig\" or \"com\"\n    \"\"\"\n    ambertools_tleap = self.config.ambertools_tleap\n    ambertools_script_dir = self.config.ambertools_script_dir\n    if self.config.protein is None:\n        protein_ff = None\n    else:\n        protein_ff = self.config.protein_ff\n\n    ligand_ff = self.config.ligand_ff\n\n    frcmod_info1 = ties.helpers.parse_frcmod_sections(self.ligA.frcmod)\n    frcmod_info2 = ties.helpers.parse_frcmod_sections(self.ligZ.frcmod)\n\n    cwd = self.config.workdir\n\n    # fixme: use the provided cwd here, otherwise this will not work if the wrong cwd is used\n    # have some conf module instead of this\n    if ligcom:\n        morph_frcmod = cwd / f'ties-{self.ligA.internal_name}-{self.ligZ.internal_name}' / ligcom / 'build' / 'hybrid.frcmod'\n    else:\n        # fixme - clean up\n        morph_frcmod = cwd / f'ties-{self.ligA.internal_name}-{self.ligZ.internal_name}' / 'build' / 'hybrid.frcmod'\n    morph_frcmod.parent.mkdir(parents=True, exist_ok=True)\n    with open(morph_frcmod, 'w') as FOUT:\n        FOUT.write('merged frcmod\\n')\n\n        for section in ['MASS', 'BOND', 'ANGLE',\n                        'DIHE', 'IMPROPER', 'NONBON']:\n            section_lines = frcmod_info1[section] + frcmod_info2[section]\n            FOUT.write('{0:s}\\n'.format(section))\n            for line in section_lines:\n                FOUT.write('{0:s}'.format(line))\n            FOUT.write('\\n')\n\n        FOUT.write('\\n\\n')\n\n    # this is our current frcmod file\n    self.frcmod = morph_frcmod\n\n    # as part of the .frcmod writing\n    # insert dummy angles/dihedrals if a morph .frcmod requires\n    # new terms between the appearing/disappearing atoms\n    # this is a trick to make sure tleap has everything it needs to generate the .top file\n    correction_introduced = self._check_hybrid_frcmod(ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff)\n    if correction_introduced:\n        # move the .frcmod which turned out to be insufficient according to the test\n        shutil.move(morph_frcmod, str(self.frcmod) + '.uncorrected' )\n        # now copy in place the corrected version\n        shutil.copy(self.frcmod, morph_frcmod)\n
    "},{"location":"reference/pair/#ties.pair.Pair._check_hybrid_frcmod","title":"_check_hybrid_frcmod","text":"
    _check_hybrid_frcmod(ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff)\n

    Check that the output library can be used to create a valid amber topology. Add missing terms with no force to pass the topology creation. Returns the corrected .frcmod content, otherwise throws an exception.

    Source code in ties/pair.py
    def _check_hybrid_frcmod(self, ambertools_tleap, ambertools_script_dir, protein_ff, ligand_ff):\n    \"\"\"\n    Check that the output library can be used to create a valid amber topology.\n    Add missing terms with no force to pass the topology creation.\n    Returns the corrected .frcmod content, otherwise throws an exception.\n    \"\"\"\n    # prepare the working directory\n    cwd = self.config.pair_morphfrmocs_tests_dir / self.internal_name\n    if not cwd.is_dir():\n        cwd.mkdir(parents=True, exist_ok=True)\n\n    if protein_ff is None:\n        protein_ff = '# no protein ff needed'\n    else:\n        protein_ff = 'source ' + protein_ff\n\n    # prepare the superimposed .mol2 file if needed\n    if not hasattr(self.suptop, 'mol2'):\n        self.suptop.write_mol2()\n\n    # prepare tleap input\n    leap_in_test = 'leap_test_morph.in'\n    leap_in_conf = open(ambertools_script_dir / leap_in_test).read()\n    open(cwd / leap_in_test, 'w').write(leap_in_conf.format(\n                            mol2=os.path.relpath(self.suptop.mol2, cwd),\n                            frcmod=os.path.relpath(self.frcmod, cwd),\n                            protein_ff=protein_ff, ligand_ff=ligand_ff))\n\n    # attempt generating the .top\n    logger.debug('Create amber7 topology .top')\n    try:\n        tleap_process = subprocess.run([ambertools_tleap, '-s', '-f', leap_in_test],\n                                       cwd=cwd, text=True, timeout=20,\n                                       capture_output=True, check=True)\n    except subprocess.CalledProcessError as err:\n        raise Exception(\n            f'ERROR: Testing the topology with tleap broke. Return code: {err.returncode} '\n            f'ERROR: Ambertools output: {err.stdout}') from err\n\n    # save stdout and stderr\n    open(cwd / 'tleap_scan_check.log', 'w').write(tleap_process.stdout + tleap_process.stderr)\n\n    if 'Errors = 0' in tleap_process.stdout:\n        logger.debug('Test hybrid .frcmod: OK, no dummy angle/dihedrals inserted.')\n        return False\n\n    # extract the missing angles/dihedrals\n    missing_bonds = set()\n    missing_angles = []\n    missing_dihedrals = []\n    for line in tleap_process.stdout.splitlines():\n        if \"Could not find bond parameter for:\" in line:\n            bond = line.split(':')[-1].strip()\n            missing_bonds.add(bond)\n        elif \"Could not find angle parameter:\" in line or \\\n                \"Could not find angle parameter for atom types:\" in line:\n            cols = line.split(':')\n            angle = cols[-1].strip()\n            if angle not in missing_angles:\n                missing_angles.append(angle)\n        elif \"No torsion terms for\" in line:\n            cols = line.split()\n            torsion = cols[-1].strip()\n            if torsion not in missing_dihedrals:\n                missing_dihedrals.append(torsion)\n\n    modified_hybrid_frcmod = cwd / f'{self.internal_name}_corrected.frcmod'\n    if missing_angles or missing_dihedrals:\n        logger.debug('Adding dummy bonds+angles+dihedrals to frcmod to generate .top')\n        # read the original frcmod\n        frcmod_lines = open(self.frcmod).readlines()\n        # overwriting the .frcmod with dummy angles/dihedrals\n        with open(modified_hybrid_frcmod, 'w') as NEW_FRCMOD:\n            for line in frcmod_lines:\n                NEW_FRCMOD.write(line)\n                if 'BOND' in line:\n                    for bond  in missing_bonds:\n                        dummy_bond = f'{bond:<14}0  180  \\t\\t# Dummy bond\\n'\n                        NEW_FRCMOD.write(dummy_bond)\n                        logger.debug(f'Added dummy bond: \"{dummy_bond}\"')\n                if 'ANGLE' in line:\n                    for angle in missing_angles:\n                        dummy_angle = f'{angle:<14}0  120.010  \\t\\t# Dummy angle\\n'\n                        NEW_FRCMOD.write(dummy_angle)\n                        logger.debug(f'Added dummy angle: \"{dummy_angle}\"')\n                if 'DIHE' in line:\n                    for dihedral in missing_dihedrals:\n                        dummy_dihedral = f'{dihedral:<14}1  0.00  180.000  2.000   \\t\\t# Dummy dihedrals\\n'\n                        NEW_FRCMOD.write(dummy_dihedral)\n                        logger.debug(f'Added dummy dihedral: \"{dummy_dihedral}\"')\n\n        # update our tleap input test to use the corrected file\n        leap_in_test_corrected = cwd / 'leap_test_morph_corrected.in'\n        open(leap_in_test_corrected, 'w').write(leap_in_conf.format(\n                            mol2=os.path.relpath(self.suptop.mol2, cwd),\n                            frcmod=os.path.relpath(modified_hybrid_frcmod, cwd),\n                            protein_ff=protein_ff, ligand_ff=ligand_ff))\n\n        # verify that adding the dummy angles/dihedrals worked\n        tleap_process = subprocess.run([ambertools_tleap, '-s', '-f', leap_in_test_corrected],\n                                       cwd=cwd, text=True, timeout=60 * 10, capture_output=True, check=True)\n\n        if not \"Errors = 0\" in tleap_process.stdout:\n            raise Exception('ERROR: Could not generate the .top file after adding dummy angles/dihedrals')\n\n\n    logger.debug('Morph .frcmod after the insertion of dummy angle/dihedrals: OK')\n    # set this .frcmod as the correct one now,\n    self.frcmod_before_correction = self.frcmod\n    self.frcmod = modified_hybrid_frcmod\n    return True\n
    "},{"location":"reference/pair/#ties.pair.Pair.overlap_fractions","title":"overlap_fractions","text":"
    overlap_fractions()\n

    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
    def overlap_fractions(self):\n    \"\"\"\n    Calculate the size of the common area.\n\n    :return: Four decimals capturing: 1) the fraction of the common size with respect to the ligA topology,\n        2) the fraction of the common size with respect to the ligZ topology,\n        3) the percentage of the disappearing atoms in the disappearing molecule\n        4) the percentage of the appearing atoms  in the appearing molecule\n    :rtype: [float, float, float, float]\n    \"\"\"\n\n    if self.suptop is None:\n        return 0, 0, float('inf'), float('inf')\n    else:\n        mcs_size = len(self.suptop.matched_pairs)\n\n    matched_fraction_left = mcs_size / float(len(self.suptop.top1))\n    matched_fraction_right = mcs_size / float(len(self.suptop.top2))\n    disappearing_atoms_fraction = (len(self.suptop.top1) - mcs_size) \\\n                               / float(len(self.suptop.top1)) * 100\n    appearing_atoms_fraction = (len(self.suptop.top2) - mcs_size) \\\n                               / float(len(self.suptop.top2)) * 100\n\n    return matched_fraction_left, matched_fraction_right, disappearing_atoms_fraction, appearing_atoms_fraction\n
    "},{"location":"reference/protein/","title":" protein","text":""},{"location":"reference/protein/#ties.protein","title":"protein","text":"

    Classes:

    • Protein \u2013

      A helper tool for the protein file. It calculates the number of ions needed to neutralise it

    "},{"location":"reference/protein/#ties.protein.Protein","title":"Protein","text":"
    Protein(filename=None, config=None)\n

    A helper tool for the protein file. It calculates the number of ions needed to neutralise it (using ambertools for now).

    :param filename: filepath to the protein :type filename: string :param config: Optional configuration for the protein :type config: :class:Config

    Methods:

    • get_path \u2013

      Get a path to the protein.

    Source code in ties/protein.py
    def __init__(self, filename=None, config=None):\n    if filename is None and config is None:\n        raise Exception('Protein filename is not passed and the config file is missing. ')\n\n    self.config = Config() if config is None else config\n\n    if filename is None:\n        if config.protein is None:\n            raise Exception('Could not find the protein in the config object. ')\n        self.file = config.protein\n    elif filename is not None:\n        self.file = filename\n        # update the config\n        config.protein = filename\n\n    # fixme - check if the file exists at this point, throw an exception otherwise\n\n    # calculate the charges of the protein (using ambertools)\n    # fixme - turn this into a method? stage2: use propka or some other tool, not this workaround\n    self.protein_net_charge = ties.generator.get_protein_net_charge(config.workdir, config.protein.absolute(),\n                                                               config.ambertools_tleap, config.tleap_check_protein,\n                                                               config.protein_ff)\n\n    logger.info(f'Protein net charge: {self.protein_net_charge}')\n
    "},{"location":"reference/protein/#ties.protein.Protein.get_path","title":"get_path","text":"
    get_path()\n

    Get a path to the protein.

    :return: the protein filename :rtype: string

    Source code in ties/protein.py
    def get_path(self):\n    \"\"\"\n    Get a path to the protein.\n\n    :return: the protein filename\n    :rtype: string\n    \"\"\"\n    return self.file\n
    "},{"location":"reference/topology_superimposer/","title":" topology_superimposer","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer","title":"topology_superimposer","text":"

    The main module responsible for the superimposition.

    Classes:

    • Atom \u2013
    • AtomPair \u2013

      An atom pair for networkx.

    • SuperimposedTopology \u2013

      SuperimposedTopology contains in the minimal case two sets of nodes S1 and S2, which

    Functions:

    • get_largest \u2013

      return a list of largest solutions

    • long_merge \u2013

      Carry out a merge and apply all checks.

    • merge_compatible_suptops \u2013

      Imagine mapping of two carbons C1 and C2 to another pair of carbons C1' and C2'.

    • merge_compatible_suptops_faster \u2013

      :param pairing_suptop:

    • superimpose_topologies \u2013

      The main function that manages the entire process.

    • extract_best_suptop \u2013

      Assumes that any merging possible already took place.

    • is_mirror_of_one \u2013

      \"Mirror\" in the sense that it is an alternative topological way to traverse the molecule.

    • generate_nxg_from_list \u2013

      Helper function. Generates a graph from a list of atoms

    • get_starting_configurations \u2013
      Minimise the number of starting configurations to optimise the process speed.\n
    • get_atoms_bonds_from_mol2 \u2013

      Use Parmed to load the files.

    • assign_coords_from_pdb \u2013

      Match the atoms from the ParmEd object based on a .pdb file

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.Atom","title":"Atom","text":"
    Atom(name, atom_type, charge=0, use_general_type=False)\n

    Methods:

    • eq \u2013

      Check if the atoms are of the same type and have a charge within the given absolute tolerance.

    • united_eq \u2013

      Like .eq, but treat the atoms as united atoms.

    Attributes:

    • united_charge \u2013

      United atom charge: summed charges of this atom and the bonded hydrogens.

    Source code in ties/topology_superimposer.py
    def __init__(self, name, atom_type, charge=0, use_general_type=False):\n    self._original_name = None\n\n    self._id = None\n    self.name = name\n    self._original_name = name.upper()\n    self.type = atom_type\n\n    self._resname = None\n    self.charge = charge\n    self._original_charge = charge\n\n    self.resid = None\n    self.bonds:Bonds = Bonds()\n    self.use_general_type = use_general_type\n    self.hash_value = None\n\n    self._unique_counter = Atom.counter\n    Atom.counter += 1\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.Atom.united_charge","title":"united_charge property","text":"
    united_charge\n

    United atom charge: summed charges of this atom and the bonded hydrogens.

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.Atom.eq","title":"eq","text":"
    eq(atom, atol=0)\n

    Check if the atoms are of the same type and have a charge within the given absolute tolerance.

    Source code in ties/topology_superimposer.py
    def eq(self, atom, atol=0):\n    \"\"\"\n    Check if the atoms are of the same type and have a charge within the given absolute tolerance.\n    \"\"\"\n    if self.type == atom.type and np.isclose(self.charge, atom.charge, atol=atol):\n        return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.Atom.united_eq","title":"united_eq","text":"
    united_eq(atom, atol=0)\n

    Like .eq, but treat the atoms as united atoms. Check if the atoms have the same atom type, and if if their charges are within the absolute tolerance. If the atoms have hydrogens, add up the attached hydrogens and use a united atom representation.

    Source code in ties/topology_superimposer.py
    def united_eq(self, atom, atol=0):\n    \"\"\"\n    Like .eq, but treat the atoms as united atoms.\n    Check if the atoms have the same atom type, and\n    if if their charges are within the absolute tolerance.\n    If the atoms have hydrogens, add up the attached hydrogens and use a united atom representation.\n    \"\"\"\n    if self.type != atom.type:\n        return False\n\n    if not np.isclose(self.united_charge, atom.united_charge, atol=atol):\n        return False\n\n    return True\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.AtomPair","title":"AtomPair","text":"
    AtomPair(left_node, right_node)\n

    An atom pair for networkx.

    Source code in ties/topology_superimposer.py
    def __init__(self, left_node, right_node):\n    self.left_atom = left_node\n    self.right_atom = right_node\n    # generate the hash value for this match\n    self.hash_value = self._gen_hash()\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology","title":"SuperimposedTopology","text":"
    SuperimposedTopology(topology1=None, topology2=None, parmed_ligA=None, parmed_ligZ=None)\n

    SuperimposedTopology contains in the minimal case two sets of nodes S1 and S2, which are paired together and represent a strongly connected component.

    However, it can also represent the symmetrical versions that were superimposed.

    Methods:

    • mcs_score \u2013

      Raturn a ratio of the superimposed atoms to the number of all atoms.

    • write_metadata \u2013

      Writes a .json file with a summary of which atoms are classified as appearing, disappearing

    • write_pdb \u2013

      param filename: name or a filepath of the new file. If None, standard preconfigured pattern will be used.

    • write_mol2 \u2013

      param filename: str location where the .mol2 file should be saved.

    • get_single_topology_region \u2013

      Return: matched atoms (even if they were unmatched for any reason)

    • get_single_topology_app \u2013

      fixme - called app but gives both app and dis

    • ringring \u2013

      Rings can only be matched to rings.

    • is_or_was_matched \u2013

      A helper function. For whatever reasons atoms get discarded.

    • get_unmatched_atoms \u2013

      Find the atoms in both topologies which were unmatched and return them.

    • get_unique_atom_count \u2013

      Requires that the .assign_atoms_ids() was called.

    • align_ligands_using_mcs \u2013

      Align the two ligands using the MCS (Maximum Common Substructure).

    • rm_matched_pairs_with_different_bonds \u2013

      Scan the matched pairs. Assume you have three pairs

    • get_dual_topology_bonds \u2013

      Get the bonds between all the atoms.

    • largest_cc_survives \u2013

      CC - Connected Component.

    • assign_atoms_ids \u2013

      Assign an ID to each pair A1-B1. This means that if we request an atom ID

    • get_appearing_atoms \u2013
    • get_disappearing_atoms \u2013
    • remove_lonely_hydrogens \u2013

      You could also remove the hydrogens when you correct charges.

    • match_gaff2_nondirectional_bonds \u2013

      If needed, swap cc-cd with cd-cc.

    • get_net_charge \u2013

      Calculate the net charge difference across

    • get_matched_with_diff_q \u2013

      Returns a list of matched atom pairs that have a different q,

    • apply_net_charge_filter \u2013

      Averaging the charges across paired atoms introduced inequalities.

    • remove_attached_hydrogens \u2013

      The node_pair to which these hydrogens are attached was removed.

    • find_lowest_rmsd_mirror \u2013

      Walk through the different mirrors and out of all options select the one

    • is_subgraph_of_global_top \u2013

      Check if after superimposition, one graph is a subgraph of another

    • rmsd \u2013

      For each pair take the distance, and then get rmsd, so root(mean(square(deviation)))

    • link_pairs \u2013

      This helps take care of the bonds.

    • find_mirror_choices \u2013

      For each pair (A1, B1) find all the other options in the mirrors where (A1, B2)

    • add_alternative_mapping \u2013

      This means that there is another way to traverse and overlap the two molecules,

    • correct_for_coordinates \u2013

      Use the coordinates of the atoms, to figure out which symmetries are the correct ones.

    • enforce_no_partial_rings \u2013

      http://www.alchemistry.org/wiki/Constructing_a_Pathway_of_Intermediate_States

    • get_topology_similarity_score \u2013

      Having the superimposed A(Left) and B(Right), score the match.

    • unmatch_pairs_with_different_charges \u2013

      Removes the matched pairs where atom charges are more different

    • is_consistent_with \u2013

      Conditions:

    • get_circles \u2013

      Return circles found in the matched pairs.

    • get_original_circles \u2013

      Return the original circles present in the input topologies.

    • cycle_spans_multiple_cycles \u2013

      What is the circle is shared?

    • merge \u2013

      Absorb the other suptop by adding all the node pairs that are not present

    • validate_charges \u2013

      Check the original charges:

    • redistribute_charges \u2013

      After the match is made and the user commits to the superimposed topology,

    • contains_same_atoms_symmetric \u2013

      The atoms can be paired differently, but they are the same.

    • is_subgraph_of \u2013

      Checks if this superimposed topology is a subgraph of another superimposed topology.

    • subgraph_relationship \u2013

      Return

    • is_mirror_of \u2013

      this is a naive check

    • eq \u2013

      Check if the superimposed topology is \"the same\". This means that every pair has a corresponding pair in the

    • toJSON \u2013

      \"

    Source code in ties/topology_superimposer.py
    def __init__(self, topology1=None, topology2=None, parmed_ligA=None, parmed_ligZ=None):\n    self.set_parmeds(parmed_ligA, parmed_ligZ)\n\n    \"\"\"\n    @superimposed_nodes : a set of pairs of nodes that matched together\n    \"\"\"\n    matched_pairs = []\n\n    # TEST: with the list of matching nodes, check if each node was used only once,\n    # the number of unique nodes should be equivalent to 2*len(common_pairs)\n    all_matched_nodes = []\n    [all_matched_nodes.extend(list(pair)) for pair in matched_pairs]\n    assert len(matched_pairs) * 2 == len(all_matched_nodes)\n\n    # fixme don't allow for initiating with matching pairs, it's not used anyway\n\n    # todo convert to nx? some other graph theory package?\n    matched_pairs.sort(key=lambda pair: pair[0].name)\n    self.matched_pairs = matched_pairs\n    self.top1 = topology1\n    self.top2 = topology2\n    # create graph representation for both in networkx library, initially to track the number of cycles\n    # fixme\n\n    self.mirrors = []\n    self.alternative_mappings = []\n    # this is a set of all nodes rather than their pairs\n    self.nodes = set(all_matched_nodes)\n    self.nodes_added_log = []\n\n    self.internal_ids = None\n    self.unique_atom_count = 0\n    self.matched_pairs_bonds = {}\n\n    # options\n    # Ambertools ignores the bonds when creating the .prmtop from the hybrid.mol2 file,\n    # so for now we can ignore the bond types\n    self.ignore_bond_types = True\n\n    # removed because\n    # fixme - make this into a list\n    self._removed_pairs_with_charge_difference = []    # atom-atom charge decided by qtol\n    self._removed_because_disjointed_cc = []    # disjointed segment\n    self._removed_due_to_net_charge = []\n    self._removed_because_unmatched_rings = []\n    self._removed_because_diff_bonds = []  # the atoms pair uses a different bond\n\n    # save the cycles in the left and right molecules\n    if self.top1 is not None and self.top2 is not None:\n        self._init_nonoverlapping_cycles()\n\n    self.id = SuperimposedTopology.COUNTER\n    SuperimposedTopology.COUNTER += 1\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology--fixme-should-check-first-if-atomname-is-unique","title":"fixme - should check first if atomName is unique","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology--fixme-should-check-first-if-atomname-is-unique","title":"fixme - should check first if atomName is unique","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.mcs_score","title":"mcs_score","text":"
    mcs_score()\n

    Raturn a ratio of the superimposed atoms to the number of all atoms. Specifically, (superimposed_atoms_number * 2) / (atoms_number_ligandA + atoms_number_ligandB) :return:

    Source code in ties/topology_superimposer.py
    def mcs_score(self):\n    \"\"\"\n    Raturn a ratio of the superimposed atoms to the number of all atoms.\n    Specifically, (superimposed_atoms_number * 2) / (atoms_number_ligandA + atoms_number_ligandB)\n    :return:\n    \"\"\"\n    return (len(self.matched_pairs) * 2) / (len(self.top1) + len(self.top2))\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.write_metadata","title":"write_metadata","text":"
    write_metadata(filename=None)\n

    Writes a .json file with a summary of which atoms are classified as appearing, disappearing as well as all other metadata relevant to this superimposition/hybrid. TODO add information: - config class in general -- relative paths to ligand 1, ligand 2 (the latest copies, ie renamed etc) -- general settings used - pair? bonds? these can be restractured, so not necessary?

    param filename: a location where the metadata should be saved\n
    Source code in ties/topology_superimposer.py
    def write_metadata(self, filename=None):\n    \"\"\"\n    Writes a .json file with a summary of which atoms are classified as appearing, disappearing\n    as well as all other metadata relevant to this superimposition/hybrid.\n    TODO add information:\n     - config class in general\n     -- relative paths to ligand 1, ligand 2 (the latest copies, ie renamed etc)\n     -- general settings used\n     - pair? bonds? these can be restractured, so not necessary?\n\n        param filename: a location where the metadata should be saved\n    \"\"\"\n\n    # store at the root for now\n    # fixme - should either be created or generated API\n    if filename is None:\n        matching_json = self.config.workdir / f'meta_{self.morph.ligA.internal_name}_{self.morph.ligZ.internal_name}.json'\n    else:\n        matching_json = pathlib.Path(filename)\n\n    matching_json.parent.mkdir(parents=True, exist_ok=True)\n\n    json.dump(self.toJSON(), open(matching_json, 'w'))\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.write_pdb","title":"write_pdb","text":"
    write_pdb(filename=None)\n

    param filename: name or a filepath of the new file. If None, standard preconfigured pattern will be used.

    Source code in ties/topology_superimposer.py
    def write_pdb(self, filename=None):\n    \"\"\"\n        param filename: name or a filepath of the new file. If None, standard preconfigured pattern will be used.\n    \"\"\"\n    if filename is None:\n        morph_pdb_path = self.config.workdir / f'{self.morph.ligA.internal_name}_{self.morph.ligZ.internal_name}_morph.pdb'\n    else:\n        morph_pdb_path = filename\n\n    # def write_morph_top_pdb(filepath, mda_l1, mda_l2, suptop, hybrid_single_dual_top=False):\n    if self.config.use_hybrid_single_dual_top:\n        # the NAMD hybrid single dual topology\n        # rename the ligand on the left to INI\n        # and the ligand on the right to END\n\n        # make a copy of the suptop here to ensure that the modifications won't affect it\n        st = copy.copy(self)\n\n        # first, set all the matched pairs to -2 and 2 (single topology)\n        # regardless of how they were mismatched\n        raise NotImplementedError('Cannot yet write hybrid single dual topology .pdb file')\n\n        # then, set the different atoms to -1 and 1 (dual topology)\n\n        # save in a single PDB file\n        # Note that the atoms from left to right\n        # in the single topology region have to\n        # be separated by the same number\n        # fixme - make a check for that\n        return\n    # fixme - find another library that can handle writing to a PDB file, MDAnalysis\n    # save the ligand with all the appropriate atomic positions, write it using the pdb format\n    # pdb file format: http://www.wwpdb.org/documentation/file-format-content/format33/sect9.html#ATOM\n    # write a dual .pdb file\n    with open(morph_pdb_path, 'w') as FOUT:\n        for atom in self.parmed_ligA.atoms:\n            \"\"\"\n            There is only one forcefield which is shared across the two topologies. \n            Basically, we need to check whether the atom is in both topologies. \n            If that is the case, then the atom should have the same name, and therefore appear only once. \n            However, if there is a new atom, it should be specfically be outlined \n            that it is 1) new and 2) the right type\n            \"\"\"\n            # write all the atoms if they are matched, that's the common part\n            # note that ParmEd does not have the information on a residue ID\n            REMAINS = 0\n            if self.contains_left_atom(atom.idx):\n                line = f\"ATOM  {atom.idx:>5d} {atom.name:>4s} {atom.residue.name:>3s}  \" \\\n                       f\"{1:>4d}    \" \\\n                       f\"{atom.xx:>8.3f}{atom.xy:>8.3f}{atom.xz:>8.3f}\" \\\n                       f\"{1.0:>6.2f}{REMAINS:>6.2f}\" + (' ' * 11) + \\\n                       '  ' + '  ' + '\\n'\n                FOUT.write(line)\n            else:\n                # this atom was not found, this means it disappears, so we should update the\n                DISAPPEARING_ATOM = -1.0\n                line = f\"ATOM  {atom.idx:>5d} {atom.name:>4s} {atom.residue.name:>3s}  \" \\\n                       f\"{1:>4d}    \" \\\n                       f\"{atom.xx:>8.3f}{atom.xy:>8.3f}{atom.xz:>8.3f}\" \\\n                       f\"{1.0:>6.2f}{DISAPPEARING_ATOM:>6.2f}\" + \\\n                       (' ' * 11) + \\\n                       '  ' + '  ' + '\\n'\n                FOUT.write(line)\n        # add atoms from the right topology,\n        # which are going to be created\n        for atom in self.parmed_ligZ.atoms:\n            if not self.contains_right_atom(atom.idx):\n                APPEARING_ATOM = 1.0\n                line = f\"ATOM  {atom.idx:>5d} {atom.name:>4s} {atom.residue.name:>3s}  \" \\\n                       f\"{1:>4d}    \" \\\n                       f\"{atom.xx:>8.3f}{atom.xy:>8.3f}{atom.xz:>8.3f}\" \\\n                       f\"{1.0:>6.2f}{APPEARING_ATOM:>6.2f}\" + \\\n                       (' ' * 11) + \\\n                       '  ' + '  ' + '\\n'\n                FOUT.write(line)\n    self.pdb = morph_pdb_path\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.write_mol2","title":"write_mol2","text":"
    write_mol2(filename=None, use_left_charges=True, use_left_coords=True)\n

    param filename: str location where the .mol2 file should be saved.

    Source code in ties/topology_superimposer.py
    def write_mol2(self, filename=None, use_left_charges=True, use_left_coords=True):\n    \"\"\"\n        param filename: str location where the .mol2 file should be saved.\n    \"\"\"\n    if filename is None:\n        hybrid_mol2 = self.config.workdir / f'{self.morph.ligA.internal_name}_{self.morph.ligZ.internal_name}_morph.mol2'\n    else:\n        hybrid_mol2 = filename\n\n    # fixme - make this as a method of suptop as well\n    # recreate the mol2 file that is merged and contains the correct atoms from both\n    # mol2 format: http://chemyang.ccnu.edu.cn/ccb/server/AIMMS/mol2.pdf\n    # fixme - build this molecule using the MDAnalysis builder instead of the current approach\n    # however, MDAnalysis currently cannot convert pdb into mol2? ...\n    # where the formatting is done manually\n    with open(hybrid_mol2, 'w') as FOUT:\n        bonds = self.get_dual_topology_bonds()\n\n        FOUT.write('@<TRIPOS>MOLECULE ' + os.linesep)\n        # name of the molecule\n        FOUT.write('HYB ' + os.linesep)\n        # num_atoms [num_bonds [num_subst [num_feat [num_sets]]]]\n        # fixme this is tricky\n        FOUT.write(f'{self.get_unique_atom_count():d} '\n                   f'{len(bonds):d}' + os.linesep)\n        # mole type\n        FOUT.write('SMALL ' + os.linesep)\n        # charge_type\n        FOUT.write('NO_CHARGES ' + os.linesep)\n        FOUT.write(os.linesep)\n\n        # write the atoms\n        FOUT.write('@<TRIPOS>ATOM ' + os.linesep)\n        # atom_id atom_name x y z atom_type [subst_id [subst_name [charge [status_bit]]]]\n        # e.g.\n        #       1 O4           3.6010   -50.1310     7.2170 o          1 L39      -0.815300\n\n        # so from the two topologies all the atoms are needed and they need to have a different atom_id\n        # so we might need to name the atom_id for them, other details are however pretty much the same\n        # the importance of atom_name is difficult to estimate\n\n        # we are going to assign IDs in the superimposed topology in order to track which atoms have IDs\n        # and which don't\n\n        # fixme - for writing, modify things to achieve the desired output\n        # note - we are modifying in place our atoms\n        for left, right in self.matched_pairs:\n            logger.debug(\n                f'Aligned {left.original_name} id {left.id} with {right.original_name} id {right.id}')\n            if not use_left_charges:\n                left.charge = right.charge\n            if not use_left_coords:\n                left.position = right.position\n\n        subst_id = 1  # resid basically\n        # write all the atoms that were matched first with their IDs\n        # prepare all the atoms, note that we use primarily the left ligand naming\n        all_atoms = [left for left, right in self.matched_pairs] + self.get_unmatched_atoms()\n        unmatched_atoms = self.get_unmatched_atoms()\n        # reorder the list according to the ID\n        all_atoms.sort(key=lambda atom: self.get_generated_atom_id(atom))\n\n        resname = 'HYB'\n        for atom in all_atoms:\n            FOUT.write(f'{self.get_generated_atom_id(atom)} {atom.name} '\n                       f'{atom.position[0]:.4f} {atom.position[1]:.4f} {atom.position[2]:.4f} '\n                       f'{atom.type.lower()} {subst_id} {resname} {atom.charge:.6f} {os.linesep}')\n\n        FOUT.write(os.linesep)\n\n        # write bonds\n        FOUT.write('@<TRIPOS>BOND ' + os.linesep)\n\n        # we have to list every bond:\n        # 1) all the bonds between the paired atoms, so that part is easy\n        # 2) bonds which link the disappearing atoms, and their connection to the paired atoms\n        # 3) bonds which link the appearing atoms, and their connections to the paired atoms\n\n        bond_counter = 1\n        list(bonds)\n        for bond_from_id, bond_to_id, bond_type in sorted(list(bonds), key=lambda b: b[:2]):\n            # Bond Line Format:\n            # bond_id origin_atom_id target_atom_id bond_type [status_bits]\n            FOUT.write(f'{bond_counter} {bond_from_id} {bond_to_id} {bond_type}' + os.linesep)\n            bond_counter += 1\n\n    self.mol2 = hybrid_mol2\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._init_nonoverlapping_cycles","title":"_init_nonoverlapping_cycles","text":"
    _init_nonoverlapping_cycles()\n

    Compile the cycles separately for the left and right molecule. Then, across the cycles, remove the nodes that join rings (double rings).

    Source code in ties/topology_superimposer.py
    def _init_nonoverlapping_cycles(self):\n    \"\"\"\n    Compile the cycles separately for the left and right molecule.\n    Then, across the cycles, remove the nodes that join rings (double rings).\n    \"\"\"\n    l_cycles, r_cycles = self.get_original_circles()\n    # remove any nodes that are shared between two cycles\n    for c1, c2 in itertools.combinations(l_cycles, r=2):\n        common = c1.intersection(c2)\n        for atom in common:\n            c1.remove(atom)\n            c2.remove(atom)\n\n    # same for r_cycles\n    for c1, c2 in itertools.combinations(r_cycles, r=2):\n        common = c1.intersection(c2)\n        for atom in common:\n            c1.remove(atom)\n            c2.remove(atom)\n\n    self._nonoverlapping_l_cycles = l_cycles\n    self._nonoverlapping_r_cycles = r_cycles\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_single_topology_region","title":"get_single_topology_region","text":"
    get_single_topology_region()\n

    Return: matched atoms (even if they were unmatched for any reason)

    Source code in ties/topology_superimposer.py
    def get_single_topology_region(self):\n    \"\"\"\n    Return: matched atoms (even if they were unmatched for any reason)\n    \"\"\"\n    # strip the pairs of the exact information about the charge differences\n    removed_pairs_with_charge_difference = [(n1, n2) for (n1, n2), q_diff in\n                                            self._removed_pairs_with_charge_difference]\n\n    # fixme: this should not work with disjointed cc and others?\n    unpaired = self._removed_because_disjointed_cc + self._removed_due_to_net_charge + \\\n        removed_pairs_with_charge_difference\n\n    return self.matched_pairs + unpaired\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_single_topology_app","title":"get_single_topology_app","text":"
    get_single_topology_app()\n

    fixme - called app but gives both app and dis get the appearing and disappearing region in the hybrid single topology use the single topology region and classify all other atoms not in it as either appearing or disappearing

    Source code in ties/topology_superimposer.py
    def get_single_topology_app(self):\n    \"\"\"\n    fixme - called app but gives both app and dis\n    get the appearing and disappearing region in the hybrid single topology\n    use the single topology region and classify all other atoms not in it\n    as either appearing or disappearing\n    \"\"\"\n    single_top_area = self.get_single_topology_region()\n\n    # turn it into a set\n    single_top_set = set()\n    for left, right in single_top_area:\n        single_top_set.add(left)\n        single_top_set.add(right)\n\n    # these unmatched atoms could be due to charge etc.\n    # so they historically refer to the dual-topology\n    unmatched_app = self.get_appearing_atoms()\n    app = {a for a in unmatched_app if a not in single_top_set}\n    unmatched_dis = self.get_disappearing_atoms()\n    dis = {a for a in unmatched_dis if a not in single_top_set}\n\n    return app, dis\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.ringring","title":"ringring","text":"
    ringring()\n

    Rings can only be matched to rings.

    Source code in ties/topology_superimposer.py
    def ringring(self):\n    \"\"\"\n    Rings can only be matched to rings.\n    \"\"\"\n    l_circles, r_circles = self.get_original_circles()\n    removed_h = []\n    ringring_removed = []\n    for l, r in self.matched_pairs[::-1]:\n        if (l, r) in removed_h:\n            continue\n\n        l_ring = any([l in c for c in l_circles])\n        r_ring = any([r in c for c in r_circles])\n        if l_ring + r_ring == 1:\n            removed_h.extend(self.remove_attached_hydrogens((l, r)))\n            self.remove_node_pair((l, r))\n            ringring_removed.append((l,r))\n\n    if ringring_removed:\n        logger.debug(f'(ST{self.id}) Ring only matches ring filter, removed: {ringring_removed} with hydrogens {removed_h}')\n    return ringring_removed, removed_h\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.is_or_was_matched","title":"is_or_was_matched","text":"
    is_or_was_matched(atom_name1, atom_name2)\n

    A helper function. For whatever reasons atoms get discarded. E.g. they had a different charge, or were part of the disjointed component, etc. This function simply checks if the most original match was made between the two atoms. It helps with verifying the original matching.

    Source code in ties/topology_superimposer.py
    def is_or_was_matched(self, atom_name1, atom_name2):\n    \"\"\"\n    A helper function. For whatever reasons atoms get discarded.\n    E.g. they had a different charge, or were part of the disjointed component, etc.\n    This function simply checks if the most original match was made between the two atoms.\n    It helps with verifying the original matching.\n    \"\"\"\n    if self.contains_atom_name_pair(atom_name1, atom_name2):\n        return True\n\n    # check if it was unmatched\n    unmatched_lists = [\n                        self._removed_because_disjointed_cc,\n                        # ignore the charges in this list\n                        [pair for pair, q in self._removed_due_to_net_charge],\n                        [pair for pair, q in self._removed_pairs_with_charge_difference]\n                       ]\n    for unmatched_list in unmatched_lists:\n        for atom1, atom2 in unmatched_list:\n            if atom1.name == atom_name1 and atom2.name == atom_name2:\n                return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_unmatched_atoms","title":"get_unmatched_atoms","text":"
    get_unmatched_atoms()\n

    Find the atoms in both topologies which were unmatched and return them. These are both, appearing and disappearing.

    Note that some atoms were removed due to charges.

    Source code in ties/topology_superimposer.py
    def get_unmatched_atoms(self):\n    \"\"\"\n    Find the atoms in both topologies which were unmatched and return them.\n    These are both, appearing and disappearing.\n\n    Note that some atoms were removed due to charges.\n    \"\"\"\n    unmatched_atoms = []\n    for node in self.top1:\n        if not self.contains_node(node):\n            unmatched_atoms.append(node)\n\n    for node in self.top2:\n        if not self.contains_node(node):\n            unmatched_atoms.append(node)\n\n    return unmatched_atoms\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_unique_atom_count","title":"get_unique_atom_count","text":"
    get_unique_atom_count()\n

    Requires that the .assign_atoms_ids() was called. This should be rewritten. But basically, it needs to count each matched pair as one atom, and the appearing and disappearing atoms separately.

    Source code in ties/topology_superimposer.py
    def get_unique_atom_count(self):\n    \"\"\"\n    Requires that the .assign_atoms_ids() was called.\n    This should be rewritten. But basically, it needs to count each matched pair as one atom,\n    and the appearing and disappearing atoms separately.\n    \"\"\"\n    return self.unique_atom_count\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.align_ligands_using_mcs","title":"align_ligands_using_mcs","text":"
    align_ligands_using_mcs(overwrite_original=False)\n

    Align the two ligands using the MCS (Maximum Common Substructure). The ligA here is the reference (docked) to which the ligZ is aligned.

    :param overwrite_original: After aligning by MCS, update the internal coordinates which will be saved to a file at the end. :type overwrite_original: bool

    Source code in ties/topology_superimposer.py
    def align_ligands_using_mcs(self, overwrite_original=False):\n    \"\"\"\n    Align the two ligands using the MCS (Maximum Common Substructure).\n    The ligA here is the reference (docked) to which the ligZ is aligned.\n\n    :param overwrite_original: After aligning by MCS, update the internal coordinates\n        which will be saved to a file at the end.\n    :type overwrite_original: bool\n    \"\"\"\n\n    if self.mda_ligA is None or self.mda_ligB is None:\n        # todo comment\n        return self.rmsd()\n\n    ligA = self.mda_ligA\n    ligB = self.mda_ligB\n\n    # back up\n    ligA_original_positions = ligA.atoms.positions[:]\n    ligB_original_positions = ligB.atoms.positions[:]\n\n    # select the atoms for the MCS,\n    # the following uses 0-based indexing\n    mcs_ligA_ids = [left.id for left, right in self.matched_pairs]\n    mcs_ligB_ids = [right.id for left, right in self.matched_pairs]\n\n    ligA_fragment = ligA.atoms[mcs_ligA_ids]\n    ligB_fragment = ligB.atoms[mcs_ligB_ids]\n\n    # move all to the origin of the fragment\n    ligA_mcs_centre = ligA_fragment.centroid()\n    ligA.atoms.translate(-ligA_mcs_centre)\n    ligB.atoms.translate(-ligB_fragment.centroid())\n\n    rotation_matrix, rmsd = MDAnalysis.analysis.align.rotation_matrix(ligB_fragment.positions, ligA_fragment.positions)\n\n    # apply the rotation to\n    ligB.atoms.rotate(rotation_matrix)\n    # move back to ligA\n    ligB.atoms.translate(ligA_mcs_centre)\n\n    # save the superimposed coordinates\n    ligB_sup = self.mda_ligB.atoms.positions[:]\n\n    # restore the MDAnalysis positions (\"working copy\")\n    # in theory you do not need to do this every time\n    self.mda_ligA.atoms.positions = ligA_original_positions\n    self.mda_ligB.atoms.positions = ligB_original_positions\n\n    if not overwrite_original:\n        # return the RMSD of the superimposed matched pairs only\n        return rmsd\n\n    # update the atoms with the mapping done via IDs\n    logger.debug(f'Aligned by MCS with the RMSD value {rmsd}')\n\n    # use the aligned coordinates\n    self.parmed_ligZ.coordinates = ligB_sup\n\n    # ideally this would now be done with MDAnalysis which can now write .mol2\n    # overwrite the internal atom positions with the final generated alignment\n    for parmed_atom in self.parmed_ligZ.atoms:\n        found = False\n        for atom in self.top2:\n            if parmed_atom.idx == atom.id:\n                atom.position = parmed_atom.xx, parmed_atom.xy, parmed_atom.xz\n                found = True\n                break\n        assert found\n\n    return rmsd\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.rm_matched_pairs_with_different_bonds","title":"rm_matched_pairs_with_different_bonds","text":"
    rm_matched_pairs_with_different_bonds()\n

    Scan the matched pairs. Assume you have three pairs A-B=C with the double bond on the right side, and the alternative bonds A=B-C remove all A, B and C pairs because of the different bonds Remove them by finding that A-B is not A=B, and B=C is not B-C

    return: the list of removed pairs

    Source code in ties/topology_superimposer.py
    def rm_matched_pairs_with_different_bonds(self):\n    \"\"\"\n    Scan the matched pairs. Assume you have three pairs\n    A-B=C with the double bond on the right side,\n    and the alternative bonds\n    A=B-C remove all A, B and C pairs because of the different bonds\n    Remove them by finding that A-B is not A=B, and B=C is not B-C\n\n    return: the list of removed pairs\n    \"\"\"\n\n    # extract the bonds for the matched molecules first\n    removed_pairs = []\n    for from_pair, bonded_pair_list in list(self.matched_pairs_bonds.items())[::-1]:\n        for bonded_pair, bond_type in bonded_pair_list:\n            # ignore if this combination was already checked\n            if bonded_pair in removed_pairs and from_pair in removed_pairs:\n                continue\n\n            if bond_type[0] != bond_type[1]:\n                # resolve this, remove the bonded pair from the matched atoms\n                if from_pair not in removed_pairs:\n                    self.remove_node_pair(from_pair)\n                    removed_pairs.append(from_pair)\n                if bonded_pair not in removed_pairs:\n                    self.remove_node_pair(bonded_pair)\n                    removed_pairs.append(bonded_pair)\n\n                # keep the history\n                self._removed_because_diff_bonds.append((from_pair, bonded_pair))\n\n    return removed_pairs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_dual_topology_bonds","title":"get_dual_topology_bonds","text":"
    get_dual_topology_bonds()\n

    Get the bonds between all the atoms. Use the atom IDs for the bonds.

    Source code in ties/topology_superimposer.py
    def get_dual_topology_bonds(self):\n    \"\"\"\n    Get the bonds between all the atoms.\n    Use the atom IDs for the bonds.\n    \"\"\"\n    assert self.top1 is not None and self.top2 is not None\n    # fixme - check if the atoms IDs have been generated\n    assert self.internal_ids is not None\n\n    # extract the bonds for the matched molecules first\n    bonds = set()\n    for from_pair, bonded_pair_list in self.matched_pairs_bonds.items():\n        from_pair_id = self.get_generated_atom_id(from_pair)\n        for bonded_pair, bond_type in bonded_pair_list:\n            if not self.ignore_bond_types:\n                if bond_type[0] != bond_type[1]:\n                    logger.error(f'ERROR: bond types do not match, even though they apply to the same atoms')\n                    logger.error(f'ERROR: left bond is \"{bond_type[0]}\" and right bond is \"{bond_type[1]}\"')\n                    logger.error(f'ERROR: the bonded atoms are {bonded_pair}')\n                    raise Exception('The bond types do not correspond to each other')\n            # every bonded pair has to be in the topology\n            assert bonded_pair in self.matched_pairs\n            to_pair_id = self.get_generated_atom_id(bonded_pair)\n            # before adding them to bonds, check if they are not already there\n            bond_sorted = sorted([from_pair_id, to_pair_id])\n            bond_sorted.append(bond_type[0])\n            bonds.add(tuple(bond_sorted))\n\n    # extract the bond information from the unmatched\n    unmatched_atoms = self.get_unmatched_atoms()\n    # for every atom, check to which \"pair\" the bond connects,\n    # and use that pair's ID to make the link\n\n    # several iterations of walking through the atoms,\n    # this is to ensure that we remove each atom one by one\n    # e.g. imagine this PAIR-SingleA1-SingleA2-SingleA3\n    # so only the first SingleA1 is connected to a pair,\n    # so the first iteration would take care of that,\n    # the next iteration would connect SingleA2 to SingleA1, etc\n    # first, remove the atoms that are connected to pairs\n    for atom in unmatched_atoms:\n        for bond in atom.bonds:\n            unmatched_atom_id = self.get_generated_atom_id(atom)\n            # check if the unmatched atom is bonded to any pair\n            pair = self.find_pair_with_atom(bond.atom)\n            if pair is not None:\n                # this atom is bound to a pair, so add the bond to the pair\n                pair_id = self.get_generated_atom_id(pair[0])\n                # add the bond between the atom and the pair\n                bond_sorted = sorted([unmatched_atom_id, pair_id])\n                bond_sorted.append(bond.type)\n                bonds.add(tuple(bond_sorted))\n            else:\n                # it is not directly linked to a matched pair,\n                # simply add this missing bond to whatever atom it is bound\n                another_unmatched_atom_id = self.get_generated_atom_id(bond.atom)\n                bond_sorted = sorted([unmatched_atom_id, another_unmatched_atom_id])\n                bond_sorted.append(bond.type)\n                bonds.add(tuple(bond_sorted))\n\n    # fixme - what about circles etc? these bonds\n    # that form circles should probably be added while checking if the circles make sense etc\n    # also, rather than checking if it is a circle, we could check if the new linked atom,\n    # is in a pair to which the new pair refers (the same rule that is used currently)\n    return bonds\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.largest_cc_survives","title":"largest_cc_survives","text":"
    largest_cc_survives(verbose=True)\n

    CC - Connected Component.

    Removes any disjoint components. Only the largest CC will be left. In the case of of equal length CCs, an arbitrary is chosen.

    How: Generates the graph where each pair is a single node, connecting the nodes if the bonds exist. Uses then networkx to find CCs.

    Source code in ties/topology_superimposer.py
    def largest_cc_survives(self, verbose=True):\n    \"\"\"\n    CC - Connected Component.\n\n    Removes any disjoint components. Only the largest CC will be left.\n    In the case of of equal length CCs, an arbitrary is chosen.\n\n    How:\n    Generates the graph where each pair is a single node, connecting the nodes if the bonds exist.\n    Uses then networkx to find CCs.\n    \"\"\"\n\n    if len(self) == 0:\n        return self, []\n\n    def lookup_up(pairs, tuple_pair):\n        for pair in pairs:\n            if pair.is_pair(tuple_pair):\n                return pair\n\n        raise Exception('Did not find the AtomPair')\n\n    g = nx.Graph()\n    atom_pairs = []\n    for pair in self.matched_pairs:\n        ap = AtomPair(pair[0], pair[1])\n        atom_pairs.append(ap)\n        g.add_node(ap)\n\n    # connect the atom pairs\n    for pair_from, pair_list in self.matched_pairs_bonds.items():\n        # lookup the corresponding atom pairs\n        ap_from = lookup_up(atom_pairs, pair_from)\n        for tuple_pair, bond_type in pair_list:\n            ap_to = lookup_up(atom_pairs, tuple_pair)\n            g.add_edge(ap_from, ap_to)\n\n    # check for connected components (CC)\n    remove_ccs = []\n    ccs = [g.subgraph(cc).copy() for cc in nx.connected_components(g)]\n    largest_cc = max([len(cc) for cc in ccs])\n\n    # there are disjoint fragments, remove the smaller one\n    for cc in ccs[::-1]:\n        # remove the cc if it smaller than the largest component\n        if len(cc) < largest_cc:\n            remove_ccs.append(cc)\n            ccs.remove(cc)\n\n    # remove the cc that have a smaller number of heavy atoms\n    largest_heavy_atom_cc = max([len([p for p in cc.nodes() if p.is_heavy_atom()])\n                                                    for cc in ccs])\n    for cc in ccs[::-1]:\n        if len([p for p in cc if p.is_heavy_atom()]) < largest_heavy_atom_cc:\n            if verbose:\n                logger.debug('Found CC that had fewer heavy atoms. Removing. ')\n            remove_ccs.append(cc)\n            ccs.remove(cc)\n\n    # remove the cc that has a smaller number of rings\n    largest_cycle_num = max([len(nx.cycle_basis(cc)) for cc in ccs])\n    for cc in ccs[::-1]:\n        if len(nx.cycle_basis(cc)) < largest_cycle_num:\n            if verbose:\n                logger.debug('Found CC that had fewer cycles. Removing. ')\n            remove_ccs.append(cc)\n            ccs.remove(cc)\n\n    # remove cc that has a smaller number of heavy atoms across rings\n    most_heavy_atoms_in_cycles = 0\n    for cc in ccs[::-1]:\n        # count the heavy atoms across the cycles\n        heavy_atom_counter = 0\n        for cycle in nx.cycle_basis(cc):\n            for a in cycle:\n                if a.is_heavy_atom():\n                    heavy_atom_counter += 1\n        if heavy_atom_counter > most_heavy_atoms_in_cycles:\n            most_heavy_atoms_in_cycles = heavy_atom_counter\n\n    for cc in ccs[::-1]:\n        # count the heavy atoms across the cycles\n        heavy_atom_counter = 0\n        for cycle in nx.cycle_basis(cc):\n            for a in cycle:\n                if a.is_heavy_atom():\n                    heavy_atom_counter += 1\n\n        if heavy_atom_counter < most_heavy_atoms_in_cycles:\n            if verbose:\n                logger.debug('Found CC that had fewer heavy atoms in cycles. Removing. ')\n            remove_ccs.append(cc)\n            ccs.remove(cc)\n\n    if len(ccs) > 1:\n        # there are equally large CCs\n        if verbose:\n            logger.debug(\"The Connected Components are equally large! Picking the first one\")\n        for cc in ccs[1:]:\n            remove_ccs.append(cc)\n            ccs.remove(cc)\n\n    assert len(ccs) == 1, \"At this point there should be left only one main component\"\n\n    # remove the worse cc\n    for cc in remove_ccs:\n        for atom_pair in cc:\n            atom_tuple = (atom_pair.left_atom, atom_pair.right_atom)\n            self.remove_node_pair(atom_tuple)\n            self._removed_because_disjointed_cc.append(atom_tuple)\n\n    return largest_cc, remove_ccs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.assign_atoms_ids","title":"assign_atoms_ids","text":"
    assign_atoms_ids(id_start=1)\n

    Assign an ID to each pair A1-B1. This means that if we request an atom ID for A1 or B1 it will be the same.

    Then assign different IDs for the other atoms

    Source code in ties/topology_superimposer.py
    def assign_atoms_ids(self, id_start=1):\n    \"\"\"\n    Assign an ID to each pair A1-B1. This means that if we request an atom ID\n    for A1 or B1 it will be the same.\n\n    Then assign different IDs for the other atoms\n    \"\"\"\n    self.internal_ids = {}\n    id_counter = id_start\n    # for each pair assign an ID\n    for left_atom, right_atom in self.matched_pairs:\n        self.internal_ids[left_atom] = id_counter\n        self.internal_ids[right_atom] = id_counter\n        # make it possible to look up the atom ID with a pair\n        self.internal_ids[(left_atom, right_atom)] = id_counter\n\n        id_counter += 1\n        self.unique_atom_count += 1\n\n    # for each atom that was not mapped to any other atom,\n    # but is still in the topology, generate an ID for it\n\n    # find the not mapped atoms in the left topology and assign them an atom ID\n    for node in self.top1:\n        # check if this node was matched\n        if not self.contains_node(node):\n            self.internal_ids[node] = id_counter\n            id_counter += 1\n            self.unique_atom_count += 1\n\n    # find the not mapped atoms in the right topology and assign them an atom ID\n    for node in self.top2:\n        # check if this node was matched\n        if not self.contains_node(node):\n            self.internal_ids[node] = id_counter\n            id_counter += 1\n            self.unique_atom_count += 1\n\n    # return the last atom\n    return id_counter\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_appearing_atoms","title":"get_appearing_atoms","text":"
    get_appearing_atoms()\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_appearing_atoms--fixme-should-check-first-if-atomname-is-unique","title":"fixme - should check first if atomName is unique","text":"

    Return a list of appearing atoms (atomName) which are the atoms that are

    Source code in ties/topology_superimposer.py
    def get_appearing_atoms(self):\n    \"\"\"\n    # fixme - should check first if atomName is unique\n    Return a list of appearing atoms (atomName) which are the\n    atoms that are\n    \"\"\"\n    unmatched = []\n    for top2_atom in self.top2:\n        is_matched = False\n        for _, matched_right_ligand_atom in self.matched_pairs:\n            if top2_atom is matched_right_ligand_atom:\n                is_matched = True\n                break\n        if not is_matched:\n            unmatched.append(top2_atom)\n\n    return unmatched\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_disappearing_atoms","title":"get_disappearing_atoms","text":"
    get_disappearing_atoms()\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_disappearing_atoms--fixme-should-check-first-if-atomname-is-unique","title":"fixme - should check first if atomName is unique","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_disappearing_atoms--fixme-update-to-using-the-node-set","title":"fixme - update to using the node set","text":"

    Return a list of appearing atoms (atomName) which are the atoms that are found in the topology, and that are not present in the matched_pairs

    Source code in ties/topology_superimposer.py
    def get_disappearing_atoms(self):\n    \"\"\"\n    # fixme - should check first if atomName is unique\n    # fixme - update to using the node set\n    Return a list of appearing atoms (atomName) which are the\n    atoms that are found in the topology, and that\n    are not present in the matched_pairs\n    \"\"\"\n    unmatched = []\n    for top1_atom in self.top1:\n        is_matched = False\n        for matched_left_ligand_atom, _ in self.matched_pairs:\n            if top1_atom is matched_left_ligand_atom:\n                is_matched = True\n                break\n        if not is_matched:\n            unmatched.append(top1_atom)\n\n    return unmatched\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.remove_lonely_hydrogens","title":"remove_lonely_hydrogens","text":"
    remove_lonely_hydrogens()\n

    You could also remove the hydrogens when you correct charges.

    Source code in ties/topology_superimposer.py
    def remove_lonely_hydrogens(self):\n    \"\"\"\n    You could also remove the hydrogens when you correct charges.\n    \"\"\"\n    logger.error('ERROR: function used that was not verified. It can create errors. '\n          'Please verify that the code works first.')\n    # in order to see any hydrogens that are by themselves, we check for any connection\n    removed_pairs = []\n    for A1, B1 in self.matched_pairs:\n        # fixme - assumes hydrogens start their names with H*\n        if not A1.name.upper().startswith('H'):\n            continue\n\n        # check if any of the bonded atoms can be found in this sup top\n        if not self.contains_any(A1.bonds) or not self.contains_node(B1.bonds):\n            # we appear disconnected, remove us\n            pass\n        for bonded_atom in A1.bonds:\n            assert not bonded_atom.name.upper().startswith('H')\n            if self.contains_node(bonded_atom):\n                continue\n\n    return removed_pairs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.match_gaff2_nondirectional_bonds","title":"match_gaff2_nondirectional_bonds","text":"
    match_gaff2_nondirectional_bonds()\n

    If needed, swap cc-cd with cd-cc. If two pairs are linked: (CC/CD) - (CD/CC), replace them according to the left side: (CC/CC) - (CD/CD). Apply this rule to all other pairs in Table I (b) at http://ambermd.org/antechamber/gaff.html

    These two define where the double bond is in a ring. GAFF decides on which one is cc or cd depending on the arbitrary atom order. This intervention we ensure that we do not remove atoms based on that arbitrary order.

    This method is idempotent.

    Source code in ties/topology_superimposer.py
    def match_gaff2_nondirectional_bonds(self):\n    \"\"\"\n    If needed, swap cc-cd with cd-cc.\n    If two pairs are linked: (CC/CD) - (CD/CC),\n    replace them according to the left side: (CC/CC) - (CD/CD).\n    Apply this rule to all other pairs in Table I (b) at http://ambermd.org/antechamber/gaff.html\n\n    These two define where the double bond is in a ring.\n    GAFF decides on which one is cc or cd depending on the arbitrary atom order.\n    This intervention we ensure that we do not remove atoms based on that arbitrary order.\n\n    This method is idempotent.\n    \"\"\"\n    nondirectionals = ({'CC', 'CD'}, {'CE', 'CF'}, {'CP', 'CQ'},\n                         {'PC', 'PD'}, {'PE', 'PF'},\n                         {'NC', 'ND'})\n\n    for no_direction_pair in nondirectionals:\n        corrected_pairs = []\n        for A1, A2 in self.matched_pairs:\n            # check if it is the right combination\n            if not {A1.type, A2.type} == no_direction_pair or (A1, A2) in corrected_pairs:\n                continue\n\n            # ignore if they are already the same\n            if A2.type == A1.type:\n                continue\n\n            # fixme - temporary solution\n            # fixme - do we want to check if we are in a ring?\n            # for now we are simply rewriting the types here so that it passes the \"specific atom type\" checks later\n            # ie so that later CC-CC and CD-CD are compared\n            # fixme - check if .type is used when writing the final output.\n            A2.type = A1.type\n            logger.debug(f'Arbitrary atom type correction. '\n                  f'Right atom type {A2.type} (in {A2}) overwritten with left atom type {A1.type} (in {A1}). ')\n\n            corrected_pairs.append((A1, A2))\n\n    return 0\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_net_charge","title":"get_net_charge","text":"
    get_net_charge()\n

    Calculate the net charge difference across the matched pairs.

    Source code in ties/topology_superimposer.py
    def get_net_charge(self):\n    \"\"\"\n    Calculate the net charge difference across\n    the matched pairs.\n    \"\"\"\n    net_charge = sum(n1.charge - n2.charge for n1, n2 in self.matched_pairs)\n    return net_charge\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_matched_with_diff_q","title":"get_matched_with_diff_q","text":"
    get_matched_with_diff_q()\n

    Returns a list of matched atom pairs that have a different q, sorted in the descending order (the first pair has the largest q diff).

    Source code in ties/topology_superimposer.py
    def get_matched_with_diff_q(self):\n    \"\"\"\n    Returns a list of matched atom pairs that have a different q,\n    sorted in the descending order (the first pair has the largest q diff).\n    \"\"\"\n    diff_q = [(n1, n2) for n1, n2 in self.matched_pairs if np.abs(n1.united_charge - n2.united_charge) > 0]\n    return sorted(diff_q, key=lambda p: abs(p[0].united_charge - p[1].united_charge), reverse=True)\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.apply_net_charge_filter","title":"apply_net_charge_filter","text":"
    apply_net_charge_filter(net_charge_threshold)\n

    Averaging the charges across paired atoms introduced inequalities. Check if the sum of the inequalities in charges is below net_charge. If not, remove pairs until that net_charge is met. Which pairs are removed depends on the approach. Greedy removal of the pairs with the highest difference can create disjoint blocks which creates issues in themselves.

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.apply_net_charge_filter--specifically-create-copies-for-each-strategy-here-and-try-a-couple-of-them","title":"Specifically, create copies for each strategy here and try a couple of them.","text":"

    Returns: a new suptop where the net_charge_threshold is enforced.

    Source code in ties/topology_superimposer.py
    def apply_net_charge_filter(self, net_charge_threshold):\n    \"\"\"\n    Averaging the charges across paired atoms introduced inequalities.\n    Check if the sum of the inequalities in charges is below net_charge.\n    If not, remove pairs until that net_charge is met.\n    Which pairs are removed depends on the approach.\n    Greedy removal of the pairs with the highest difference\n    can create disjoint blocks which creates issues in themselves.\n\n    # Specifically, create copies for each strategy here and try a couple of them.\n    Returns: a new suptop where the net_charge_threshold is enforced.\n    \"\"\"\n\n    approaches = ['greedy', 'terminal_alch_linked', 'terminal', 'alch_linked', 'leftovers', 'smart']\n    rm_disjoint_at_each_step = [True, False]\n\n    # best configuration info\n    best_approach = None\n    suptop_size = -1\n    rm_disjoint_each_step_conf = False\n\n    # try all confs\n    for rm_disjoint_each_step in rm_disjoint_at_each_step:\n        for approach in approaches:\n            # make a shallow copy of the suptop\n            next_approach = copy.copy(self)\n            # first overall\n            if rm_disjoint_each_step:\n                next_approach.largest_cc_survives(verbose=False)\n\n            # try the strategy\n            while np.abs(next_approach.get_net_charge()) > net_charge_threshold:\n\n                best_candidate_with_h = next_approach._smart_netqtol_pair_picker(approach)\n                for pair in best_candidate_with_h:\n                    next_approach.remove_node_pair(pair)\n\n                if rm_disjoint_each_step:\n                    next_approach.largest_cc_survives(verbose=False)\n\n            # regardless of whether the continuous disjoint removal is being tried or not,\n            # it will be applied at the end\n            # so apply it here at the end in order to make this comparison equivalent\n            next_approach.largest_cc_survives(verbose=False)\n\n            if len(next_approach) > suptop_size:\n                suptop_size = len(next_approach)\n                best_approach = approach\n                rm_disjoint_each_step_conf = rm_disjoint_each_step\n\n    # apply the best strategy to this suptop\n    logger.debug(f'Pair removal strategy (q net tol): {best_approach} with disjoint CC removed at each step: {rm_disjoint_each_step_conf}')\n    logger.debug(f'To meet q net tol: {best_approach}')\n\n    total_diff = 0\n    if rm_disjoint_each_step_conf:\n        self.largest_cc_survives()\n    while np.abs(self.get_net_charge()) > net_charge_threshold:\n        best_candidate_with_h = self._smart_netqtol_pair_picker(best_approach)\n\n        # remove them\n        for pair in best_candidate_with_h:\n            self.remove_node_pair(pair)\n            diff_q_pairs = abs(pair[0].united_charge - pair[1].united_charge)\n            # add to the list of removed because of the net charge\n            self._removed_due_to_net_charge.append([pair, diff_q_pairs])\n            total_diff += diff_q_pairs\n\n        if rm_disjoint_each_step_conf:\n            self.largest_cc_survives()\n\n    return total_diff\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._smart_netqtol_pair_picker","title":"_smart_netqtol_pair_picker","text":"
    _smart_netqtol_pair_picker(strategy)\n

    The appearing and disappearing alchemical region have their cumulative q different by more than the netq (0.1 typically). Find the next pair with q imbalance that contributes to it. Instead of using the greedy strategy: - avoid bottleneck atoms (the removed atoms split the molecule into smaller parts) - use atoms that are close to the already mutating site

    @param strategy: 'greedy', 'terminal_alch_linked', 'terminal', 'alch_linked', 'leftovers', 'smart'

    Source code in ties/topology_superimposer.py
    def _smart_netqtol_pair_picker(self, strategy):\n    \"\"\"\n    The appearing and disappearing alchemical region have their\n    cumulative q different by more than the netq (0.1 typically).\n    Find the next pair with q imbalance that contributes to it.\n    Instead of using the greedy strategy:\n      - avoid bottleneck atoms (the removed atoms split the molecule into smaller parts)\n      - use atoms that are close to the already mutating site\n\n    @param strategy: 'greedy', 'terminal_alch_linked', 'terminal', 'alch_linked', 'leftovers', 'smart'\n    \"\"\"\n    # get pairs with different charges\n    diff_q_pairs = self.get_matched_with_diff_q()\n    if len(diff_q_pairs) == 0:\n        raise Exception('Did not find any pairs with a different q even though the net tol is not met? ')\n\n    # sort the pairs into categories\n    # use 5 pairs with the largest difference\n    diff_sorted = self._sort_pairs_into_categories_qnettol(diff_q_pairs, best_cases_num=5)\n\n    if strategy == 'smart':\n        # get the most promising category\n        for cat in diff_sorted.keys():\n            if diff_sorted[cat]:\n                category = diff_sorted[cat]\n                break\n        # remove the first pair\n        # fixme - double check this\n        return category[0]\n\n    # allow removal of pairs even if the differences are small\n    diff_sorted = self._sort_pairs_into_categories_qnettol(diff_q_pairs, best_cases_num=len(self))\n\n    # for other strategies, take the key directly, but only if there is one\n    if diff_sorted[strategy]:\n        pairs_in_category = diff_sorted[strategy]\n    else:\n        # if there is no option in that category, revert to greedy\n        pairs_in_category = diff_sorted['greedy']\n    return pairs_in_category[0]\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._sort_pairs_into_categories_qnettol","title":"_sort_pairs_into_categories_qnettol","text":"
    _sort_pairs_into_categories_qnettol(pairs, best_cases_num=6)\n

    This is a helper function which sorts matched pairs with different charges into categories, which are: - terminal_alch_linked - terminal: at most one heavy atom bonded - alch_linked: at least one bond to the alchemical region - leftovers: not terminal or alch_linked, - low_diff

    Returns: Ordered Dictionary

    Source code in ties/topology_superimposer.py
    def _sort_pairs_into_categories_qnettol(self, pairs, best_cases_num=6):\n    \"\"\"\n    This is a helper function which sorts\n    matched pairs with different charges into categories, which are:\n     - terminal_alch_linked\n     - terminal: at most one heavy atom bonded\n     - alch_linked: at least one bond to the alchemical region\n     - leftovers: not terminal or alch_linked,\n     - low_diff\n\n    Returns: Ordered Dictionary\n    \"\"\"\n\n    sorted_categories = OrderedDict()\n    sorted_categories['terminal_alch_linked'] = []\n    sorted_categories['terminal'] = []\n    sorted_categories['alch_linked'] = []\n    sorted_categories['greedy'] = []\n    sorted_categories['leftovers'] = []\n    sorted_categories['low_diff'] = []\n\n    app_atoms = self.get_appearing_atoms()\n    dis_atoms = self.get_disappearing_atoms()\n\n    # fixme: maybe use a threshold rather than a number of cases?\n    for pair in pairs[:best_cases_num]:\n        # ignore hydrogens on their own\n        # if pair[0].element == 'H':\n        #     continue\n\n        neighbours = [p for p, bonds in self.matched_pairs_bonds[pair]]\n\n        # ignore hydrogens in these connections (non-consequential)\n        hydrogens = [(a, b) for a, b in neighbours if a.element == 'H']\n        heavy = [(a, b) for a, b in neighbours if a.element != 'H']\n\n        # attach the hydrogens to be removed as well\n        to_remove = [pair] + hydrogens\n\n        sorted_categories['greedy'].append(to_remove)\n\n        # check if the current pair is linked to the alchemical region\n        linked_to_alchemical = False\n        for bond in pair[0].bonds:\n            if bond.atom in dis_atoms:\n                linked_to_alchemical = True\n        for bond in pair[1].bonds:\n            if bond.atom in app_atoms:\n                linked_to_alchemical = True\n\n        if len(heavy) == 1 and linked_to_alchemical:\n            sorted_categories['terminal_alch_linked'].append(to_remove)\n        if len(heavy) == 1:\n            sorted_categories['terminal'].append(to_remove)\n        if linked_to_alchemical:\n            sorted_categories['alch_linked'].append(to_remove)\n        if len(heavy) != 1 and not linked_to_alchemical:\n            sorted_categories['leftovers'].append(to_remove)\n\n    # carry out for the pairs that have a smaller Q diff\n    for pair in pairs[best_cases_num:]:\n        neighbours = [p for p, bonds in self.matched_pairs_bonds[pair]]\n        # consider the attached hydrogens\n        hydrogens = [(a, b) for a, b in neighbours if a.element == 'H']\n        # attach the hydrogens to be removed as well\n        to_remove = [pair] + hydrogens\n\n        sorted_categories['low_diff'].append(to_remove)\n\n    return sorted_categories\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.remove_attached_hydrogens","title":"remove_attached_hydrogens","text":"
    remove_attached_hydrogens(node_pair)\n

    The node_pair to which these hydrogens are attached was removed. Remove the dangling hydrogens.

    Check if these hydrogen are matched/superimposed. If that is the case. Remove the pairs.

    Note that if the hydrogens are paired and attached to node_pairA, they have to be attached to node_pairB, as a rule of being a match.

    Source code in ties/topology_superimposer.py
    def remove_attached_hydrogens(self, node_pair):\n    \"\"\"\n    The node_pair to which these hydrogens are attached was removed.\n    Remove the dangling hydrogens.\n\n    Check if these hydrogen are matched/superimposed. If that is the case. Remove the pairs.\n\n    Note that if the hydrogens are paired and attached to node_pairA,\n    they have to be attached to node_pairB, as a rule of being a match.\n    \"\"\"\n\n    # skip if no hydrogens found\n    if node_pair not in self.matched_pairs_bonds:\n        logger.debug('No dangling hydrogens')\n        return []\n\n    attached_pairs = self.matched_pairs_bonds[node_pair]\n\n    removed_pairs = []\n    for pair, bond_types in list(attached_pairs):\n        # ignore non hydrogens\n        if not pair[0].element == 'H':\n            continue\n\n        self.remove_node_pair(pair)\n        logger.debug(f'Removed dangling hydrogen pair: {pair}')\n        removed_pairs.append(pair)\n    return removed_pairs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.find_lowest_rmsd_mirror","title":"find_lowest_rmsd_mirror","text":"
    find_lowest_rmsd_mirror()\n

    Walk through the different mirrors and out of all options select the one that has the lowest RMSD. This way we increase the chance of getting a better match. However, long term it will be necessary to use the dihedrals to ensure that we match the atoms better.

    Source code in ties/topology_superimposer.py
    def find_lowest_rmsd_mirror(self):\n    \"\"\"\n    Walk through the different mirrors and out of all options select the one\n    that has the lowest RMSD. This way we increase the chance of getting a better match.\n    However, long term it will be necessary to use the dihedrals to ensure that we match\n    the atoms better.\n    \"\"\"\n    # fixme - you have to also take into account the \"weird / other symmetries\" besides mirrors\n    winner = self\n    lowest_rmsd = self.rmsd()\n    for mirror in self.mirrors:\n        mirror_rmsd = mirror.rmsd()\n        if mirror_rmsd < lowest_rmsd:\n            lowest_rmsd = mirror_rmsd\n            winner = mirror\n\n    if self is winner:\n        # False here means that it is not a mirror\n        return lowest_rmsd, self, False\n    else:\n        return lowest_rmsd, winner, True\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.is_subgraph_of_global_top","title":"is_subgraph_of_global_top","text":"
    is_subgraph_of_global_top()\n

    Check if after superimposition, one graph is a subgraph of another :return:

    Source code in ties/topology_superimposer.py
    def is_subgraph_of_global_top(self):\n    \"\"\"\n    Check if after superimposition, one graph is a subgraph of another\n    :return:\n    \"\"\"\n    # check if one topology is a subgraph of another topology\n    if len(self.matched_pairs) == len(self.top1) or len(self.matched_pairs) == len(self.top2):\n        return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.rmsd","title":"rmsd","text":"
    rmsd()\n

    For each pair take the distance, and then get rmsd, so root(mean(square(deviation)))

    Source code in ties/topology_superimposer.py
    def rmsd(self):\n    \"\"\"\n    For each pair take the distance, and then get rmsd, so root(mean(square(deviation)))\n    \"\"\"\n\n    assert len(self.matched_pairs) > 0\n\n    dsts = []\n    for atomA, atomB in self.matched_pairs:\n        dst = np.sqrt(np.sum(np.square((atomA.position - atomB.position))))\n        dsts.append(dst)\n    return np.sqrt(np.mean(np.square(dsts)))\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.link_pairs","title":"link_pairs","text":"
    link_pairs(from_pair, pairs)\n

    This helps take care of the bonds.

    Source code in ties/topology_superimposer.py
    def link_pairs(self, from_pair, pairs):\n    \"\"\"\n    This helps take care of the bonds.\n    \"\"\"\n    assert from_pair in self.matched_pairs_bonds\n    for pair, bond_types in pairs:\n        # the parent pair should have its list of pairs\n        assert pair in self.matched_pairs_bonds, f'not found pair {pair}'\n\n        # link X-Y\n        self.matched_pairs_bonds[from_pair].add((pair, bond_types))\n        # link Y-X\n        self.matched_pairs_bonds[pair].add((from_pair, bond_types))\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.find_mirror_choices","title":"find_mirror_choices","text":"
    find_mirror_choices()\n

    For each pair (A1, B1) find all the other options in the mirrors where (A1, B2)

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.find_mirror_choices--ie-ignore-x-b1-search-if-we-repair-from-a-to-b-then-b-to-a-should-be-repaired-too","title":"ie Ignore (X, B1) search, if we repair from A to B, then B to A should be repaired too","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.find_mirror_choices--fixme-is-this-still-necessary-if-we-are-traversing-all-paths","title":"fixme - is this still necessary if we are traversing all paths?","text":"Source code in ties/topology_superimposer.py
    def find_mirror_choices(self):\n    \"\"\"\n    For each pair (A1, B1) find all the other options in the mirrors where (A1, B2)\n    # ie Ignore (X, B1) search, if we repair from A to B, then B to A should be repaired too\n\n    # fixme - is this still necessary if we are traversing all paths?\n    \"\"\"\n    choices = {}\n    for A1, B1 in self.matched_pairs:\n        options_for_a1 = []\n        for mirror in self.mirrors:\n            for A2, B2 in mirror.matched_pairs:\n                if A1 is A2 and B1 is not B2:\n                    options_for_a1.append(B2)\n\n        if options_for_a1:\n            options_for_a1.insert(0, B1)\n            choices[A1] = options_for_a1\n\n    return choices\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.add_alternative_mapping","title":"add_alternative_mapping","text":"
    add_alternative_mapping(weird_symmetry)\n

    This means that there is another way to traverse and overlap the two molecules, but that the self is better (e.g. lower rmsd) than the other one

    Source code in ties/topology_superimposer.py
    def add_alternative_mapping(self, weird_symmetry):\n    \"\"\"\n    This means that there is another way to traverse and overlap the two molecules,\n    but that the self is better (e.g. lower rmsd) than the other one\n    \"\"\"\n    self.alternative_mappings.append(weird_symmetry)\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.correct_for_coordinates","title":"correct_for_coordinates","text":"
    correct_for_coordinates()\n

    Use the coordinates of the atoms, to figure out which symmetries are the correct ones. Rearrange so that the overall topology represents the one that has appropriate coordinates, whereas all the mirrors represent the other poor matches.

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.correct_for_coordinates--fixme-ensure-that-each-node-is-used-only-once-at-the-end","title":"fixme - ensure that each node is used only once at the end","text":"Source code in ties/topology_superimposer.py
    def correct_for_coordinates(self):\n    \"\"\"\n    Use the coordinates of the atoms, to figure out which symmetries are the correct ones.\n    Rearrange so that the overall topology represents the one that has appropriate coordinates,\n    whereas all the mirrors represent the other poor matches.\n\n    # fixme - ensure that each node is used only once at the end\n    \"\"\"\n\n    # check if you have coordinates\n    # fixme - rn we have it, check\n\n    # superimpose the coordinates, ensure a good match\n    # fixme - this was done before, so let's leave this way for now\n\n    # fixme - consider putting this conf as a mirror, and then modifying this\n\n    # check which are preferable for each of the mirrors\n    # we have to match mirrors to each other, ie say we have (O1=O3) and (O2=O4)\n    # we should find the mirror matching (O1=O4) and (O2=O3)\n    # so note that we have a closure here: All 4 atoms are used in both cases, and each time are paired differently.\n    # So this is how we defined the mirror - and therefore we can reduce this issue to the minimal mirrors.\n    # fixme - is this a cycle? O1-O3-O2-O4-O1\n    # Let's try to define a chain: O1 =O3, and O1 =O4, and O2 is =O3 or =O4\n    # So we have to define how to find O1 matching to different parts, and then decide\n    choices_mapping = self.find_mirror_choices()\n\n    # fixme - rewrite this method to eliminate one by one the hydrogens that fit in perfectly,\n    # some of them will have a plural significant match, while others might be hazy,\n    # so we have to eliminate them one by one, searching the best matches and then eliminating them\n\n    removed_nodes = set()\n    for A1, choices in choices_mapping.items():\n        # remove the old tuple\n        # fixme - not sure if this is the right way to go,\n        # but we break all the rules when applying this simplistic strategy\n        self.remove_node_pair((A1, choices[0]))\n        removed_nodes.add(A1)\n        removed_nodes.add(choices[0])\n\n    shortest_dsts = []\n\n    added_nodes = set()\n\n    # better matches\n    # for each atom that mismatches, scan all molecules and find the best match and eliminate it\n    blacklisted_bxs = []\n    for _ in range(len(choices_mapping)):\n        # fixme - optimisation of this could be such that if they two atoms are within 0.2A or something\n        # then they are straight away fixed\n        closest_dst = 9999999\n        closest_a1 = None\n        closest_bx = None\n        for A1, choices in choices_mapping.items():\n            # so we have several choices for A1, and now naively we are taking the one that is closest, and\n            # assuming the superimposition is easy, this would work\n\n            # FIXME - you cannot use simply distances, if for A1 and A2 the best is BX, then BX there should be\n            # rules for that\n            for BX in choices:\n                if BX in blacklisted_bxs:\n                    continue\n                # use the distance_array because of PBC correction and speed\n                a1_bx_dst = np.sqrt(np.sum(np.square(A1.position-BX.position)))\n                if a1_bx_dst < closest_dst:\n                    closest_dst = a1_bx_dst\n                    closest_bx = BX\n                    closest_a1 = A1\n\n        # across all the possible choices, found the best match now:\n        blacklisted_bxs.append(closest_bx)\n        shortest_dsts.append(closest_dst)\n        logger.debug(f'{closest_a1.name} is matching best with {closest_bx.name}')\n\n        # remove the old tuple and insert the new one\n        self.add_node_pair((closest_a1, closest_bx))\n        added_nodes.add(closest_a1)\n        added_nodes.add(closest_bx)\n        # remove from consideration\n        del choices_mapping[closest_a1]\n        # blacklist\n\n    # fixme - check that the added and the removed nodes are the same set\n    assert removed_nodes == added_nodes\n\n    # this is the corrected region score (there might not be any)\n    if len(shortest_dsts) != 0:\n        avg_dst = np.mean(shortest_dsts)\n    else:\n        # fixme\n        avg_dst = 0\n\n    return avg_dst\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.enforce_no_partial_rings","title":"enforce_no_partial_rings","text":"
    enforce_no_partial_rings()\n

    http://www.alchemistry.org/wiki/Constructing_a_Pathway_of_Intermediate_States It is the opening or closing of the rings that is an issue. This means that if any atom on a ring disappears, it breaks the ring, and therefore the entire ring should be removed and appeared again.

    If any atom is removed, it should check if it affects other rings, therefore cascading removing further rings.

    Source code in ties/topology_superimposer.py
    def enforce_no_partial_rings(self):\n    \"\"\"\n    http://www.alchemistry.org/wiki/Constructing_a_Pathway_of_Intermediate_States\n    It is the opening or closing of the rings that is an issue.\n    This means that if any atom on a ring disappears, it breaks the ring,\n    and therefore the entire ring should be removed and appeared again.\n\n    If any atom is removed, it should check if it affects other rings,\n    therefore cascading removing further rings.\n    \"\"\"\n    MAX_CIRCLE_SIZE = 7\n\n    # get circles in the original ligands\n    l_circles, r_circles = self.get_original_circles()\n    l_matched_circles, r_matched_circles = self.get_circles()\n\n    # right now we are filtering out circles that are larger than 7 atoms,\n    l_circles = list(filter(lambda c: len(c) <= MAX_CIRCLE_SIZE, l_circles))\n    r_circles = list(filter(lambda c: len(c) <= MAX_CIRCLE_SIZE, r_circles))\n    l_matched_circles = list(filter(lambda c: len(c) <= MAX_CIRCLE_SIZE, l_matched_circles))\n    r_matched_circles = list(filter(lambda c: len(c) <= MAX_CIRCLE_SIZE, r_matched_circles))\n\n    # first, see which matched circles eliminate themselves (simply matched circles)\n    correct_circles = []\n    for l_matched_circle in l_matched_circles[::-1]:\n        for r_matched_circle in r_matched_circles[::-1]:\n            if self.are_matched_sets(l_matched_circle, r_matched_circle):\n                # These two circles fully overlap, so they are fine\n                l_matched_circles.remove(l_matched_circle)\n                r_matched_circles.remove(r_matched_circle)\n                # update the original circles\n                l_circles.remove(l_matched_circle)\n                r_circles.remove(r_matched_circle)\n                correct_circles.append((l_matched_circle, r_matched_circle))\n\n    # at this point, we should not have any matched circles, in either R and L\n    # this is because we do not allow one ligand to have a matched circle, while another ligand not\n    assert len(l_matched_circles) == len(r_matched_circles) == 0\n\n    while True:\n        # so now we have to work with the original rings which have not been overlapped,\n        # these most likely means that there are mutations preventing it from overlapping\n        l_removed_pairs = self._remove_unmatched_ring_atoms(l_circles)\n        r_removed_pairs = self._remove_unmatched_ring_atoms(r_circles)\n\n        for l_circle, r_circle in correct_circles:\n            # checked if any removed atom affected any of the correct circles\n            affected_l_circle = any(l_atom in l_circle for l_atom, r_atom in l_removed_pairs)\n            affected_r_circle = any(r_atom in r_circle for l_atom, r_atom in r_removed_pairs)\n            # add the circle to be disassembled\n            if affected_l_circle or affected_r_circle:\n                l_circles.append(l_circle)\n                r_circles.append(r_circle)\n\n        if len(l_removed_pairs) == len(r_removed_pairs) == 0:\n            break\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._remove_unmatched_ring_atoms","title":"_remove_unmatched_ring_atoms","text":"
    _remove_unmatched_ring_atoms(circles)\n

    A helper function. Removes pairs with the given atoms.

    The removed atoms are classified as unmatched_rings.

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._remove_unmatched_ring_atoms--parameters","title":"Parameters","text":"

    circles : list A list of iterables. Each atom in a circle, if matched, is removed together with the corresponding atom from the suptop. The user should ensure that the rings/circles are partial

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._remove_unmatched_ring_atoms--returns","title":"Returns","text":"

    removed : bool True if any atom was removed. False otherwise.

    Source code in ties/topology_superimposer.py
    def _remove_unmatched_ring_atoms(self, circles):\n    \"\"\"\n    A helper function. Removes pairs with the given atoms.\n\n    The removed atoms are classified as unmatched_rings.\n\n    Parameters\n    ----------\n    circles : list\n        A list of iterables. Each atom in a circle, if matched, is removed together with\n        the corresponding atom from the suptop.\n        The user should ensure that the rings/circles are partial\n\n    Returns\n    -------\n    removed : bool\n        True if any atom was removed. False otherwise.\n    \"\"\"\n    removed_pairs = []\n    for circle in circles:\n        for unmatched_ring_atom in circle:\n            # find if the ring has a match\n            if self.contains_node(unmatched_ring_atom):\n                # remove the pair from matched\n                pair = self.get_pair_with_atom(unmatched_ring_atom)\n                self.remove_node_pair(pair)\n                self._removed_because_unmatched_rings.append(pair)\n                removed_pairs.append(pair)\n    return removed_pairs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_topology_similarity_score","title":"get_topology_similarity_score","text":"
    get_topology_similarity_score()\n

    Having the superimposed A(Left) and B(Right), score the match. This is a rather naive approach. It compares A-B match by checking if any of the node X and X' in A and B have a bond to another node Y that is not present in A-B, but that is directly reachable from X and X' in a similar way. We ignore the charge of Y and focus here only on the topology.

    For every \"external bond\" from the component we try to see if topologically it scores well. So for any matched pair, we extend the topology and the score is equal to the size of such an component. Then we do this for all other matching nodes and sum the score.

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_topology_similarity_score--fixme-maybe-you-should-use-the-entire-graphs-in-order-to-see-if-this-is-good-or-not","title":"fixme - maybe you should use the entire graphs in order to see if this is good or not?","text":"

    so the simpler approach is to ignore charges for a second to only understand the relative place in the topology, in other words, the question is, how similar are two nodes A and B vs A and C? let's traverse A and B together, and then A and C together, and while doing that, ignore the charges. In this case, A and B could get together 20 parts, whereas A and C traverses together 22 parts, meaning that topologically, it is a more suitable one, because it closer corresponds to the actual atom. Note that this approach has problem: - you can imagine A and B traversing where B is in a completely wrong global place, but it happens to have a bigger part common to A, than C which globally is correct. Answer to this: at the same time, ideally B would be excluded, because it should have been already matched to another topology.

    Alternative approach: take into consideration other components and the distance from this component to them. Specifically, allows mismatches

    FIXME - allow flexible mismatches. Meaning if someone mutates one bonded atom, then it might be noticed that

    Source code in ties/topology_superimposer.py
    def get_topology_similarity_score(self):\n    \"\"\"\n    Having the superimposed A(Left) and B(Right), score the match.\n    This is a rather naive approach. It compares A-B match by checking\n    if any of the node X and X' in A and B have a bond to another node Y that is\n    not present in A-B, but that is directly reachable from X and X' in a similar way.\n    We ignore the charge of Y and focus here only on the topology.\n\n    For every \"external bond\" from the component we try to see if topologically it scores well.\n    So for any matched pair, we extend the topology and the score is equal to the size of\n    such an component. Then we do this for all other matching nodes and sum the score.\n\n    # fixme - maybe you should use the entire graphs in order to see if this is good or not?\n    so the simpler approach is to ignore charges for a second to only understand the relative place in the topology,\n    in other words, the question is, how similar are two nodes A and B vs A and C? let's traverse A and B together,\n    and then A and C together, and while doing that, ignore the charges. In this case, A and B could\n    get together 20 parts, whereas A and C traverses together 22 parts, meaning that topologically,\n    it is a more suitable one, because it closer corresponds to the actual atom.\n    Note that this approach has problem:\n    - you can imagine A and B traversing where B is in a completely wrong global place, but it\n    happens to have a bigger part common to A, than C which globally is correct. Answer to this:\n    at the same time, ideally B would be excluded, because it should have been already matched to another\n    topology.\n\n    Alternative approach: take into consideration other components and the distance from this component\n    to them. Specifically, allows mismatches\n\n    FIXME - allow flexible mismatches. Meaning if someone mutates one bonded atom, then it might be noticed\n    that\n    \"\"\"\n    overall_score = 0\n    for node_a, node_b in self.matched_pairs:\n        # for every neighbour in Left\n        for a_bond in node_a.bonds:\n            # if this bonded atom is present in this superimposed topology (or component), ignore\n            # fixme - surely this can be done better, you could have \"contains this atom or something\"\n            in_this_sup_top = False\n            for other_a, _ in self.matched_pairs:\n                if a_bond.atom == other_a:\n                    in_this_sup_top = True\n                    break\n            if in_this_sup_top:\n                continue\n\n            # a candidate is found that could make the node_a and node_b more similar,\n            # so check if it is also present in node_b,\n            # ignore the charges to focus only on the topology and put aside the parameterisation\n            for b_bond in node_b.bonds:\n                # fixme - what if the atom is mutated into a different atom? we have to be able\n                # to relies on other measures than just this one, here the situation is that the topology\n                # is enough to answer the question (because only charges were modified),\n                # however, this gets more tricky\n                # fixme - hardcoded\n                score = len(_overlay(a_bond.atom, b_bond.atom))\n\n                # this is a purely topology based score, the bigger the overlap the better the match\n                overall_score += score\n\n            # check if the neighbour points to any node X that is not used in Left,\n\n            # if node_b leads to the same node X\n    return overall_score\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.unmatch_pairs_with_different_charges","title":"unmatch_pairs_with_different_charges","text":"
    unmatch_pairs_with_different_charges(atol)\n

    Removes the matched pairs where atom charges are more different than the provided absolute tolerance atol (units in Electrons).

    remove_dangling_h: After removing any pair it also removes any bound hydrogen(s).

    Source code in ties/topology_superimposer.py
    def unmatch_pairs_with_different_charges(self, atol):\n    \"\"\"\n    Removes the matched pairs where atom charges are more different\n    than the provided absolute tolerance atol (units in Electrons).\n\n    remove_dangling_h: After removing any pair it also removes any bound hydrogen(s).\n    \"\"\"\n    removed_hydrogen_pairs = []\n    for node1, node2 in self.matched_pairs[::-1]:\n        if node1.united_eq(node2, atol=atol) or (node1, node2) in removed_hydrogen_pairs:\n            continue\n\n        # remove this pair\n        # use full logging for this kind of information\n        # print('Q: removing nodes', (node1, node2)) # to do - consider making this into a logging feature\n        self.remove_node_pair((node1, node2))\n\n        # keep track of the removed atoms due to the charge\n        self._removed_pairs_with_charge_difference.append(\n            ((node1, node2), math.fabs(node2.united_charge - node1.united_charge)))\n\n        # Removed functionality: remove the dangling hydrogens\n        removed_h_pairs = self.remove_attached_hydrogens((node1, node2))\n        removed_hydrogen_pairs.extend(removed_h_pairs)\n        for h_pair in removed_h_pairs:\n            self._removed_pairs_with_charge_difference.append(\n                (h_pair, 'dangling'))\n\n    # sort the removed in a descending order\n    self._removed_pairs_with_charge_difference.sort(key=lambda x: x[1], reverse=True)\n\n    return self._removed_pairs_with_charge_difference\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.is_consistent_with","title":"is_consistent_with","text":"
    is_consistent_with(suptop)\n
    Conditions
    • There should be a minimal overlap of at least 1 node.
    • There is no pair (Na=Nb) in this sup top such that (Na=Nc) or (Nb=Nc) for some Nc in the other suptop.
    • The number of cycles in this suptop and the other suptop must be the same (?removing for now, fixme)
    • merging cannot lead to new cycles?? (fixme). What is the reasoning behind this? I mean, I guess the assumption is that, if the cycles were compatible, they would be created during the search, rather than now while merging. ??
    Source code in ties/topology_superimposer.py
    def is_consistent_with(self, suptop):\n    \"\"\"\n    Conditions:\n        - There should be a minimal overlap of at least 1 node.\n        - There is no pair (Na=Nb) in this sup top such that (Na=Nc) or (Nb=Nc) for some Nc in the other suptop.\n        - The number of cycles in this suptop and the other suptop must be the same (?removing for now, fixme)\n        - merging cannot lead to new cycles?? (fixme). What is the reasoning behind this?\n            I mean, I guess the assumption is that, if the cycles were compatible,\n            they would be created during the search, rather than now while merging. ??\n    \"\"\"\n\n    # confirm that there is no mismatches, ie (A=B) in suptop1 and (A=C) in suptop2 where (C!=B)\n    for st1Na, st1Nb in self.matched_pairs:\n        for st2Na, st2Nb in suptop.matched_pairs:\n            if (st1Na is st2Na) and not (st1Nb is st2Nb) or (st1Nb is st2Nb) and not (st1Na is st2Na):\n                return False\n\n    # ensure there is at least one common pair\n    if self.count_common_node_pairs(suptop) == 0:\n        return False\n\n    # why do we need this?\n    # if not self.is_consistent_cycles(suptop):\n    #     return False\n\n    return True\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._rename_ligand","title":"_rename_ligand staticmethod","text":"
    _rename_ligand(atoms, name_counter=None)\n

    name_counter: a dictionary with atom as the key such as 'N', 'C', etc, the counter keeps track of the last used counter for each name. Empty means that the counting will start from 1.

    Source code in ties/topology_superimposer.py
    @staticmethod\ndef _rename_ligand(atoms, name_counter=None):\n    \"\"\"\n    name_counter: a dictionary with atom as the key such as 'N', 'C', etc,\n    the counter keeps track of the last used counter for each name.\n    Empty means that the counting will start from 1.\n    \"\"\"\n    if name_counter is None:\n        name_counter = {}\n\n    for atom in atoms:\n        # get the first letters that is not a character\n        after_letters = [i for i, l in enumerate(atom.name) if l.isalpha()][-1] + 1\n\n        atom_name = atom.name[:after_letters]\n        last_used_counter = name_counter.get(atom_name, 0)\n\n        # rename\n        last_used_counter += 1\n        new_atom_name = atom_name + str(last_used_counter)\n        logger.info(f'Renaming {atom.name} to {new_atom_name}')\n        atom.name = new_atom_name\n\n        # update the counter\n        name_counter[atom_name] = last_used_counter\n\n    return name_counter\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._get_atom_names_counter","title":"_get_atom_names_counter staticmethod","text":"
    _get_atom_names_counter(atoms)\n

    name_counter: a dictionary with atom as the key such as 'N', 'C', etc, the counter keeps track of the last used counter for each name. Ie if there are C1, C2, C3, this will return {'C':3} as the last counter.

    Source code in ties/topology_superimposer.py
    @staticmethod\ndef _get_atom_names_counter(atoms):\n    \"\"\"\n    name_counter: a dictionary with atom as the key such as 'N', 'C', etc,\n    the counter keeps track of the last used counter for each name.\n    Ie if there are C1, C2, C3, this will return {'C':3} as the last counter.\n    \"\"\"\n    name_counter = {}\n\n    for atom in atoms:\n        # get the first letters that is not a character\n        after_letters = [i for i, l in enumerate(atom.name) if l.isalpha()][-1] + 1\n\n        atom_name = atom.name[:after_letters]\n        atom_number = int(atom.name[after_letters:])\n        last_used_counter = name_counter.get(atom_name, 0)\n\n        # update the counter\n        name_counter[atom_name] = max(last_used_counter, atom_number)\n\n    return name_counter\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_circles","title":"get_circles","text":"
    get_circles()\n

    Return circles found in the matched pairs.

    Source code in ties/topology_superimposer.py
    def get_circles(self):\n    \"\"\"\n    Return circles found in the matched pairs.\n    \"\"\"\n    gl, gr = self.get_nx_graphs()\n    gl_circles = [set(circle) for circle in nx.cycle_basis(gl)]\n    gr_circles = [set(circle) for circle in nx.cycle_basis(gr)]\n    return gl_circles, gr_circles\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.get_original_circles","title":"get_original_circles","text":"
    get_original_circles()\n

    Return the original circles present in the input topologies.

    Source code in ties/topology_superimposer.py
    def get_original_circles(self):\n    \"\"\"\n    Return the original circles present in the input topologies.\n    \"\"\"\n    # create a circles\n    l_original = self._get_original_circle(self.top1)\n    r_original = self._get_original_circle(self.top2)\n\n    l_circles = [set(circle) for circle in nx.cycle_basis(l_original)]\n    r_circles = [set(circle) for circle in nx.cycle_basis(r_original)]\n    return l_circles, r_circles\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology._get_original_circle","title":"_get_original_circle","text":"
    _get_original_circle(atom_list)\n

    Create a networkx circle out of the list atom_list - list of AtomNode

    Source code in ties/topology_superimposer.py
    def _get_original_circle(self, atom_list):\n    \"\"\"Create a networkx circle out of the list\n    atom_list - list of AtomNode\n    \"\"\"\n    g = nx.Graph()\n    # add each node\n    for atom in atom_list:\n        g.add_node(atom)\n\n    # add all the edges\n    for atom in atom_list:\n        # add the edges from nA\n        for other in atom_list:\n            if atom.bound_to(other):\n                g.add_edge(atom, other)\n\n    return g\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.cycle_spans_multiple_cycles","title":"cycle_spans_multiple_cycles","text":"
    cycle_spans_multiple_cycles()\n

    What is the circle is shared? We are using cycles which excluded atoms that join different rings. fixme - could this lead to a special case?

    Source code in ties/topology_superimposer.py
    def cycle_spans_multiple_cycles(self):\n    # This filter checks whether a newly created suptop cycle spans multiple cycles\n    # this is one of the filters (#106)\n    # fixme - should this be applied whenever we work with more than 1 cycle?\n    # it checks whether any cycles in the left molecule,\n    # is paired with more than one cycle in the right molecule\n    \"\"\"\n    What is the circle is shared?\n    We are using cycles which excluded atoms that join different rings.\n    fixme - could this lead to a special case?\n    \"\"\"\n\n    for l_cycle in self._nonoverlapping_l_cycles:\n        overlap_counter = 0\n        for r_cycle in self._nonoverlapping_r_cycles:\n            # check if the cycles overlap\n            if self._cycles_overlap(l_cycle, r_cycle):\n                overlap_counter += 1\n\n        if overlap_counter > 1:\n            return True\n\n    for r_cycle in self._nonoverlapping_r_cycles:\n        overlap_counter = 0\n        for l_cycle in self._nonoverlapping_l_cycles:\n            # check if the cycles overlap\n            if self._cycles_overlap(l_cycle, r_cycle):\n                overlap_counter += 1\n\n        if overlap_counter > 1:\n            return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.merge","title":"merge","text":"
    merge(suptop)\n

    Absorb the other suptop by adding all the node pairs that are not present in the current sup top.

    WARNING: ensure that the other suptop is consistent with this sup top.

    Source code in ties/topology_superimposer.py
    def merge(self, suptop):\n    \"\"\"\n    Absorb the other suptop by adding all the node pairs that are not present\n    in the current sup top.\n\n    WARNING: ensure that the other suptop is consistent with this sup top.\n    \"\"\"\n    # assert self.is_consistent_with(suptop)\n\n    # print(\"About the merge two sup tops\")\n    # self.print_summary()\n    # other_suptop.print_summary()\n\n    merged_pairs = []\n    for pair in suptop.matched_pairs:\n        # check if this pair is present\n        if not self.contains(pair):\n            n1, n2 = pair\n            if self.contains_node(n1) or self.contains_node(n2):\n                raise Exception('already uses that node')\n            # pass the bonded pairs here\n            self.add_node_pair(pair)\n            merged_pairs.append(pair)\n    # after adding all the nodes, now add the bonds\n    for pair in merged_pairs:\n        # add the connections\n        bonded_pairs = suptop.matched_pairs_bonds[pair]\n        assert len(bonded_pairs) > 0\n        self.link_pairs(pair, bonded_pairs)\n\n    # removed from the \"merged\" the ones that agree, so it contains only the new stuff\n    # to make it easier to read\n    self.nodes_added_log.append((\"merged with\", merged_pairs))\n\n    # check for duplication, fixme - temporary\n    return merged_pairs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.validate_charges","title":"validate_charges staticmethod","text":"
    validate_charges(atom_list_l, atom_list_right)\n

    Check the original charges: - ensure that the total charge of L and R are integers - ensure that they are equal to the same integer

    Source code in ties/topology_superimposer.py
    @staticmethod\ndef validate_charges(atom_list_l, atom_list_right):\n    \"\"\"\n    Check the original charges:\n    - ensure that the total charge of L and R are integers\n    - ensure that they are equal to the same integer\n    \"\"\"\n    whole_left_charge = sum(a.charge for a in atom_list_l)\n    np.testing.assert_almost_equal(whole_left_charge, round(whole_left_charge), decimal=2,\n                                   err_msg=f'left charges are not integral. Expected {round(whole_left_charge)}'\n                                           f' but found {whole_left_charge}')\n\n    whole_right_charge = sum(a.charge for a in atom_list_right)\n    np.testing.assert_almost_equal(whole_right_charge, round(whole_right_charge), decimal=2,\n                                   err_msg=f'right charges are not integral. Expected {round(whole_right_charge)}'\n                                           f' but found {whole_right_charge}'\n                                   )\n    # same integer\n    np.testing.assert_almost_equal(whole_left_charge, whole_right_charge, decimal=2)\n\n    return round(whole_left_charge)\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.redistribute_charges","title":"redistribute_charges","text":"
    redistribute_charges()\n

    After the match is made and the user commits to the superimposed topology, the charges can be revised. We calculate the average charges between every match, and check how that affects the rest of the molecule (the unmatched atoms). Then, we distribute the charges to the unmatched atoms to get the net charge as a whole number/integer.

    This function should be called after removing the matches for whatever reason. ie at the end of anything that could modify the atom pairing.

    Source code in ties/topology_superimposer.py
    def redistribute_charges(self):\n    \"\"\"\n    After the match is made and the user commits to the superimposed topology,\n    the charges can be revised.\n    We calculate the average charges between every match, and check how that affects\n    the rest of the molecule (the unmatched atoms).\n    Then, we distribute the charges to the unmatched atoms to get\n    the net charge as a whole number/integer.\n\n    This function should be called after removing the matches for whatever reason.\n    ie at the end of anything that could modify the atom pairing.\n    \"\"\"\n\n    SuperimposedTopology.validate_charges(self.top1, self.top2)\n\n    # find the integral net charge of the molecule\n    net_charge = round(sum(a.charge for a in self.top1))\n    net_charge_test = round(sum(a.charge for a in self.top2))\n    if net_charge != net_charge_test:\n        raise Exception('The internally computed net charges of the molecules are different')\n    # fixme - use the one passed by the user?\n    logger.info(f'Internally computed net charge: {net_charge}')\n\n    # the total charge in the matched region before the changes\n    matched_total_charge_l = sum(left.charge for left, right in self.matched_pairs)\n    matched_total_charge_r = sum(right.charge for left, right in self.matched_pairs)\n\n    # get the unmatched atoms in Left and Right\n    l_unmatched = self.get_disappearing_atoms()\n    r_unmatched = self.get_appearing_atoms()\n\n    init_q_dis = sum(a.charge for a in l_unmatched)\n    init_q_app = sum(a.charge for a in r_unmatched)\n    logger.debug(f'Initial cumulative charge of the appearing={init_q_app:.6f}, disappearing={init_q_dis:.6f} '\n          f'alchemical regions')\n\n    # average the charges between matched atoms in the joint area of the dual topology\n    total_charge_matched = 0    # represents the net charge of the joint area minus molecule charge\n    for left, right in self.matched_pairs:\n        avg_charge = (left.charge + right.charge) / 2.0\n        # write the new charge\n        left.charge = right.charge = avg_charge\n        total_charge_matched += avg_charge\n    # total_partial_charge_matched e.g. -0.9 (partial charges) - -1 (net molecule charge) = 0.1\n    total_partial_charge_matched = total_charge_matched - net_charge\n    logger.debug(f'Total partial charge in the joint area = {total_partial_charge_matched:.6f}')\n\n    # calculate what the correction should be in the alchemical regions\n    r_delta_charge_total = - (total_partial_charge_matched + init_q_app)\n    l_delta_charge_total = - (total_partial_charge_matched + init_q_dis)\n    logger.debug(f'Total charge imbalance to be distributed in '\n          f'dis={l_delta_charge_total:.6f} and app={r_delta_charge_total:.6f}')\n\n    if len(l_unmatched) == 0 and l_delta_charge_total != 0:\n        logger.error('----------------------------------------------------------------------------------------------')\n        logger.error('ERROR? AFTER AVERAGING CHARGES, THERE ARE NO UNMATCHED ATOMS TO ASSIGN THE CHARGE TO: '\n              'left ligand.')\n        logger.error('----------------------------------------------------------------------------------------------')\n    if len(r_unmatched) == 0 and r_delta_charge_total != 0:\n        logger.error('----------------------------------------------------------------------------------------------')\n        logger.error('ERROR? AFTER AVERAGING CHARGES, THERE ARE NO UNMATCHED ATOMS TO ASSIGN THE CHARGE TO: '\n              'right ligand. ')\n        logger.error('----------------------------------------------------------------------------------------------')\n\n    # distribute the charges over the alchemical regions\n    if len(l_unmatched) != 0:\n        l_delta_per_atom = float(l_delta_charge_total) / len(l_unmatched)\n    else:\n        # fixme - no unmatching atoms, so there should be no charge to redistribute\n        l_delta_per_atom = 0\n\n    if len(r_unmatched) != 0:\n        r_delta_per_atom = float(r_delta_charge_total) / len(r_unmatched)\n    else:\n        r_delta_per_atom = 0\n        # fixme - no matching atoms, so there should be no charge to redistribute\n    logger.debug(f'Charge imbalance per atom in dis={l_delta_per_atom:.6f} and app={r_delta_per_atom:.6f}')\n\n    # redistribute that delta q over the atoms in the left and right molecule\n    for atom in l_unmatched:\n        atom.charge += l_delta_per_atom\n    for atom in r_unmatched:\n        atom.charge += r_delta_per_atom\n\n    # check if the appearing atoms and the disappearing atoms have the same net charge\n    dis_q_sum = sum(a.charge for a in l_unmatched)\n    app_q_sum = sum(a.charge for a in r_unmatched)\n    logger.debug(f'Final cumulative charge of the appearing={app_q_sum:.6f}, disappearing={dis_q_sum:.6f} '\n          f'alchemical regions')\n    if not np.isclose(dis_q_sum, app_q_sum):\n        logger.error('The partial charges in app/dis region are not equal to each other. ')\n        raise Exception('The alchemical region in app/dis do not have equal partial charges.')\n\n    # note that we are really modifying right now the original nodes.\n    SuperimposedTopology.validate_charges(self.top1, self.top2)\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.contains_same_atoms_symmetric","title":"contains_same_atoms_symmetric","text":"
    contains_same_atoms_symmetric(other_sup_top)\n

    The atoms can be paired differently, but they are the same.

    Source code in ties/topology_superimposer.py
    def contains_same_atoms_symmetric(self, other_sup_top):\n    \"\"\"\n    The atoms can be paired differently, but they are the same.\n    \"\"\"\n    if len(self.nodes.symmetric_difference(other_sup_top.nodes)) == 0:\n        return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.is_subgraph_of","title":"is_subgraph_of","text":"
    is_subgraph_of(other_sup_top)\n

    Checks if this superimposed topology is a subgraph of another superimposed topology. Or if any mirror topology is a subgraph.

    Source code in ties/topology_superimposer.py
    def is_subgraph_of(self, other_sup_top):\n    \"\"\"\n    Checks if this superimposed topology is a subgraph of another superimposed topology.\n    Or if any mirror topology is a subgraph.\n    \"\"\"\n    # subgraph cannot be equivalent self.eq, it is only proper subgraph (ie proper subset)\n    if len(self.matched_pairs) >= len(other_sup_top.matched_pairs):\n        return False\n\n    # self is smaller, so it might be a subgraph\n    if other_sup_top.contains_all(self):\n        return True\n\n    # self is not a subgraph, but it could be a subgraph of one of the mirrors\n    for mirror in self.mirrors:\n        if other_sup_top.contains_all(mirror):\n            return True\n\n    # other is bigger than self, but not a subgraph of self\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.subgraph_relationship","title":"subgraph_relationship","text":"
    subgraph_relationship(other_sup_top)\n

    Return 1 if self is a supergraph of other, -1 if self is a subgraph of other 0 if they have the same number of elements (regardless of what the nodes are)

    Source code in ties/topology_superimposer.py
    def subgraph_relationship(self, other_sup_top):\n    \"\"\"\n    Return\n    1 if self is a supergraph of other,\n    -1 if self is a subgraph of other\n    0 if they have the same number of elements (regardless of what the nodes are)\n    \"\"\"\n    if len(self.matched_pairs) == len(other_sup_top.matched_pairs):\n        return 0\n\n    if len(self.matched_pairs) > len(other_sup_top.matched_pairs):\n        # self is bigger than other,\n        # check if self contains all nodes in other\n        if self.contains_all(other_sup_top):\n            return 1\n\n        # other is not a subgraph, but check the mirrors if any of them are\n        for mirror in self.mirrors:\n            if mirror.contains_all(other_sup_top):\n                return 1\n\n        # other is smaller but not a subgraph of this graph or any of its mirrors\n        return 0\n\n    if len(self.matched_pairs) < len(other_sup_top.matched_pairs):\n        # other is bigger, so self might be a subgraph\n        # check if other contains all nodes in self\n        if other_sup_top.contains_all(self):\n            return -1\n\n        # self is not a subgraph, but it could be a subgraph of one of the mirrors\n        for mirror in self.mirrors:\n            if other_sup_top.contains_all(mirror):\n                return -1\n\n        # other is bigger than self, but it is not a subgraph\n        return 0\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.is_mirror_of","title":"is_mirror_of","text":"
    is_mirror_of(other_sup_top)\n

    this is a naive check fixme - check if the found superimposed topology is the same (ie the same matches), what then?

    some of the superimposed topologies represent symmetrical matches, for example, imagine T1A and T1B is a symmetrical version of T2A and T2B, this means that - the number of nodes in T1A, T1B, T2A, and T2B is the same - all the nodes in T1A are in T2A, - all the nodes in T1B are in T2B

    Source code in ties/topology_superimposer.py
    def is_mirror_of(self, other_sup_top):\n    \"\"\"\n    this is a naive check\n    fixme - check if the found superimposed topology is the same (ie the same matches), what then?\n\n    some of the superimposed topologies represent symmetrical matches,\n    for example, imagine T1A and T1B is a symmetrical version of T2A and T2B,\n    this means that\n     - the number of nodes in T1A, T1B, T2A, and T2B is the same\n     - all the nodes in T1A are in T2A,\n     - all the nodes in T1B are in T2B\n    \"\"\"\n\n    if len(self.matched_pairs) != len(other_sup_top.matched_pairs):\n        return False\n\n    if self.contains_same_atoms_symmetric(other_sup_top):\n        return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.eq","title":"eq","text":"
    eq(sup_top)\n

    Check if the superimposed topology is \"the same\". This means that every pair has a corresponding pair in the other topology (but possibly in a different order)

    Source code in ties/topology_superimposer.py
    def eq(self, sup_top):\n    \"\"\"\n    Check if the superimposed topology is \"the same\". This means that every pair has a corresponding pair in the\n    other topology (but possibly in a different order)\n    \"\"\"\n    # fixme - should replace this with networkx\n    if len(self) != len(sup_top):\n        return False\n\n    for pair in self.matched_pairs:\n        # find for every pair the matching pair\n        if not sup_top.contains(pair):\n            return False\n\n    return True\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.SuperimposedTopology.toJSON","title":"toJSON","text":"
    toJSON()\n

    \" Extract all the important information and return a json string.

    Source code in ties/topology_superimposer.py
    def toJSON(self):\n    \"\"\"\"\n        Extract all the important information and return a json string.\n    \"\"\"\n    summary = {\n        # metadata\n        # renamed atoms, new name : old name\n        'renamed_atoms': {\n            'start_ligand': {a.name: a.original_name for a in self.top1},\n            'end_ligand': {a.name: a.original_name for a in self.top2},\n        },\n        # the dual topology information\n        'superimposition': {\n            'matched': {str(n1): str(n2) for n1, n2 in self.matched_pairs},\n            'appearing': list(map(str, self.get_appearing_atoms())),\n            'disappearing': list(map(str, self.get_disappearing_atoms())),\n            'removed': { # because of:\n                # replace atoms with their names\n                'net_charge': [((a1.name, a2.name), d) for (a1, a2), d in self._removed_due_to_net_charge],\n                'pair_q': [((a1.name, a2.name), d) for (a1, a2), d in self._removed_pairs_with_charge_difference],\n                'disjointed': [(a1.name, a2.name) for a1, a2 in self._removed_because_disjointed_cc],\n                'bonds': [((a1.name, a2.name), d) for (a1, a2), d in self._removed_because_diff_bonds],\n                'unmatched_rings': [((a1.name, a2.name), d) for (a1, a2), d in self._removed_because_unmatched_rings],\n            },\n            'charges_delta': {\n                'start_ligand': {a.name: a.charge - a._original_charge for a in self.top1 if a._original_charge != a.charge},\n                'end_ligand': {a.name: a.charge - a._original_charge for a in self.top2 if a._original_charge != a.charge}\n            }\n        },\n        'config': self.config.get_serializable(),\n        'internal': 'atoms' # fixme\n    }\n    return summary\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.get_largest","title":"get_largest","text":"
    get_largest(lists)\n

    return a list of largest solutions

    Source code in ties/topology_superimposer.py
    def get_largest(lists):\n    \"\"\"\n    return a list of largest solutions\n    \"\"\"\n    solution_sizes = [len(st) for st in lists]\n    largest_sol_size = max(solution_sizes)\n    return list(filter(lambda st: len(st) == largest_sol_size, lists))\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.long_merge","title":"long_merge","text":"
    long_merge(suptop1, suptop2)\n

    Carry out a merge and apply all checks. Merge suptop2 into suptop1.

    Source code in ties/topology_superimposer.py
    def long_merge(suptop1, suptop2):\n    \"\"\"\n    Carry out a merge and apply all checks.\n    Merge suptop2 into suptop1.\n\n    \"\"\"\n    if suptop1 is suptop2:\n        return suptop1\n\n    if suptop1.eq(suptop2):\n        log(\"Merge: the two are the equal. Ignoring\")\n        return suptop1\n\n    if suptop2.is_subgraph_of(suptop1):\n        log(\"Merge: this is already a superset. Ignoring\")\n        return suptop1\n\n    # check if the two are consistent\n    # ie there is no clashes\n    if not suptop1.is_consistent_with(suptop2):\n        log(\"Merge: cannot merge - not consistent\")\n        return -1\n\n    # fixme - this can be removed because it is now taken care of in the other functions?\n    # g1, g2 = suptop1.getNxGraphs()\n    # assert len(nx.cycle_basis(g1)) == len(nx.cycle_basis(g2))\n    # g3, g4 = suptop2.getNxGraphs()\n    # assert len(nx.cycle_basis(g3)) == len(nx.cycle_basis(g4))\n    #\n    # assert suptop1.sameCircleNumber()\n    newly_added_pairs = suptop1.merge(suptop2)\n\n    # if not suptop1.sameCircleNumber():\n    #     raise Exception('something off')\n    # # remove sol2 from the solutions:\n    # all_solutions.remove(sol2)\n    return newly_added_pairs\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.merge_compatible_suptops","title":"merge_compatible_suptops","text":"
    merge_compatible_suptops(suptops)\n

    Imagine mapping of two carbons C1 and C2 to another pair of carbons C1' and C2'. If C1 was mapped to C1', and C2 to C2', and each craeted a suptop, then we have to join the two suptops.

    fixme - appears to be doing too many combinations Consider using a queue. Add the new combinations here rather than restarting again and again. You could keep a list of \"combinations\" in a queue, and each time you make a new element,

    Source code in ties/topology_superimposer.py
    def merge_compatible_suptops(suptops):\n    \"\"\"\n    Imagine mapping of two carbons C1 and C2 to another pair of carbons C1' and C2'.\n    If C1 was mapped to C1', and C2 to C2', and each craeted a suptop, then we have to join the two suptops.\n\n    fixme - appears to be doing too many combinations\n    Consider using a queue. Add the new combinations here rather than restarting again and again.\n    You could keep a list of \"combinations\" in a queue, and each time you make a new element,\n\n    \"\"\"\n\n    if len(suptops) == 1:\n        return suptops\n\n    # consier simplifying in case of \"2\"\n\n    # keep track of which suptops have been used to build a bigger one\n    # these can be likely later discarded\n    ingredients = {}\n    excluded = []\n    while True:\n        any_new_suptop = False\n        for st1, st2 in itertools.combinations(suptops, r=2):\n            if {st1, st2} in excluded:\n                continue\n\n            if st1 in ingredients.get(st2, []) or st2 in ingredients.get(st1, []):\n                continue\n\n            if st1.is_subgraph_of(st2) or st2.is_subgraph_of(st1):\n                continue\n\n            # fixme - verify this one\n            if st1.eq(st2):\n                continue\n\n            # check if the two suptops are compatible\n            elif st1.is_consistent_with(st2):\n                # merge them!\n                large_suptop = copy.copy(st1)\n                # add both the pairs and the bonds that are not present in st1\n                large_suptop.merge(st2)\n                suptops.append(large_suptop)\n\n                ingredients[large_suptop] = {st1, st2}.union(ingredients.get(st1, set())).union(ingredients.get(st2, set()))\n                excluded.append({st1, st2})\n\n                # break\n                any_new_suptop = True\n\n        if not any_new_suptop:\n            break\n\n    # flatten\n    all_ingredients = list(itertools.chain(*ingredients.values()))\n\n    # return the larger suptops, but not the constituents\n    new_suptops = [st for st in suptops if st not in all_ingredients]\n    return new_suptops\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.merge_compatible_suptops_faster","title":"merge_compatible_suptops_faster","text":"
    merge_compatible_suptops_faster(pairing_suptop: Dict, min_bonds: int)\n

    :param pairing_suptop: :param min_bonds: if the End molecule at this point has only two bonds, they can be mapped to two other bonds in the start molecule. :return:

    Source code in ties/topology_superimposer.py
    def merge_compatible_suptops_faster(pairing_suptop: Dict, min_bonds: int):\n    \"\"\"\n\n    :param pairing_suptop:\n    :param min_bonds: if the End molecule at this point has only two bonds, they can be mapped to two other bonds\n        in the start molecule.\n    :return:\n    \"\"\"\n\n    if len(pairing_suptop) == 1:\n        return [pairing_suptop.popitem()[1]]\n\n    # any to any\n    all_pairings = list(itertools.combinations(pairing_suptop.keys(), r=min_bonds))\n\n    selected_pairings = []\n    for pairings in all_pairings:\n        n = set()\n        for pairing in pairings:\n            n.add(pairing[0])\n            n.add(pairing[1])\n        #\n        if 2 * len(pairings) == len(n):\n            selected_pairings.append(pairings)\n\n    # attempt to combine the different traversals\n    built_topologies = []\n    for mapping in selected_pairings:\n        # mapping the different bonds to different bonds\n\n        # check if the suptops are consistent with each other\n        if not are_consistent_topologies([pairing_suptop[key] for key in mapping]):\n            continue\n\n        # merge them!\n        large_suptop = copy.copy(pairing_suptop[mapping[0]])\n        for next_map in mapping[1:]:\n            next_suptop = pairing_suptop[next_map]\n\n            # add both the pairs and the bonds that are not present in st1\n            large_suptop.merge(next_suptop)\n\n        built_topologies.append(large_suptop)\n\n    return built_topologies\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer._overlay","title":"_overlay","text":"
    _overlay(n1, n2, parent_n1, parent_n2, bond_types, suptop, ignore_coords=False, use_element_type=True, exact_coords_cue=False)\n

    Jointly and recursively traverse the molecule while building up the suptop.

    If n1 and n2 are the same, we will be traversing through both graphs, marking the jointly travelled areas.

    Return the topology of the common substructure between the two molecules.

    n1 from the left molecule, n2 from the right molecule

    Source code in ties/topology_superimposer.py
    def _overlay(n1, n2, parent_n1, parent_n2, bond_types, suptop, ignore_coords=False, use_element_type=True,\n             exact_coords_cue=False):\n    \"\"\"\n    Jointly and recursively traverse the molecule while building up the suptop.\n\n    If n1 and n2 are the same, we will be traversing through both graphs, marking the jointly travelled areas.\n\n    Return the topology of the common substructure between the two molecules.\n\n    *n1 from the left molecule,\n    *n2 from the right molecule\n    \"\"\"\n\n    # ignore if either of the nodes is part of the suptop\n    if suptop.contains_node(n1) or suptop.contains_node(n2):\n        return None\n\n    if use_element_type and not n1.same_element(n2):\n        return None\n\n    # make more specific, ie if \"use_specific_type\"\n    if not use_element_type and not n1.same_type(n2):\n        return None\n\n    # Check for cycles\n    # if a new cycle is created by adding this node,\n    # then the cycle should be present in both, left and right ligand\n    safe = True\n    # if n1 is linked with node in suptop other than parent\n    for b1 in n1.bonds:\n        # if this bound atom is not a parent and is already a suptop\n        if b1.atom != parent_n1 and suptop.contains_node(b1.atom):\n            safe = False  # n1 forms cycle, now need to check n2\n            for b2 in n2.bonds:\n                if b2.atom != parent_n2 and suptop.contains_node(b2.atom):\n                    # b2 forms cycle, now need to check it's the same in both\n                    if suptop.contains((b1.atom, b2.atom)):\n                        safe = True\n                        break\n            if not safe:  # only n1 forms a cycle\n                break\n    if not safe:  # either only n1 forms cycle or both do but different cycles\n        return None\n\n    # now the same for any remaining unchecked bonds in n2\n    safe = True\n    for b2 in n2.bonds:\n        if b2.atom != parent_n2 and suptop.contains_node(b2.atom):\n            safe = False\n            for b1 in n1.bonds:\n                if b1.atom != parent_n1 and suptop.contains_node(b1.atom):\n                    if suptop.contains((b1.atom, b2.atom)):\n                        safe = True\n                        break\n            if not safe:\n                break\n    if not safe:\n        return None\n\n    # check if the cycle spans multiple cycles present in the left and right molecule,\n    if suptop.cycle_spans_multiple_cycles():\n        logger.debug('Found a cycle spanning multiple cycles')\n        return None\n\n    logger.debug(f\"Adding {(n1, n2)} to suptop.matched_pairs\")\n\n    # all looks good, create a new copy for this suptop\n    suptop = copy.copy(suptop)\n\n    # append both nodes as a pair to ensure that we keep track of the mapping\n    # having both nodes appended also ensure that we do not revisit/read neither n1 and n2\n    suptop.add_node_pair((n1, n2))\n    if not (parent_n1 is parent_n2 is None):\n        # fixme - adding a node pair should automatically take care of the bond, maybe using inner data?\n        # fixme why is this link different than a normal link?\n        suptop.link_with_parent((n1, n2), (parent_n1, parent_n2), bond_types)\n\n    # the extra bonds are legitimate\n    # so let's make sure they are added\n    # fixme: add function get_bonds_without_parent? or maybe make them \"subtractable\" even without the type\n    # for this it would be enough that the bonds is an object too, it will make it more managable\n    # bookkeeping? Ideally adding \"add_node_pair\" would take care of this\n    for n1_bonded in n1.bonds:\n        # ignore left parent\n        if n1_bonded.atom is parent_n1:\n            continue\n        for n2_bonded in n2.bonds:\n            # ignore right parent\n            if n2_bonded.atom is parent_n2:\n                continue\n\n            # if the pair exists, add a bond between the two pairs\n            if suptop.contains((n1_bonded.atom, n2_bonded.atom)):\n                # fixme: this linking of pairs should also be corrected\n                # 1) add \"pair\" as an object rather than a tuple (n1, n2)\n                # 2) this always has to happen, ie it is impossible to find (n1, n2)\n                # ie make it into a more sensible method,\n                # fixme: this does not link pairs?\n                suptop.link_pairs((n1, n2),\n                                  [((n1_bonded.atom, n2_bonded.atom), (n1_bonded.type, n2_bonded.type)), ])\n\n    # fixme: sort so that heavy atoms go first\n    p1_bonds = n1.bonds.without(parent_n1)\n    p2_bonds = n2.bonds.without(parent_n2)\n    candidate_pairings = list(itertools.product(p1_bonds, p2_bonds))\n\n    # check if any of the pairs have exactly the same location, use that as a hidden signal\n    # it is possible at this stage to use predetermine the distances\n    # and trick it to use the ones that have exactly the same distances,\n    # and treat that as a signal\n    # now the issue here is that someone might \"predetermine\" one part, ia CA1 mapping to CB1 rathern than CB2\n    # but if CA1 and CA2 is present, and CA2 is not matched to CB2 in a predetermined manner, than CB2 should not be deleted\n    # so we have to delete only the offers where CA1 = CB2 which would not be correct to pursue\n    if exact_coords_cue:\n        predetermined = {a1: a2 for a1, a2 in candidate_pairings if np.array_equal(a1.atom.position, a2.atom.position)}\n        predetermined.update(zip(list(predetermined.values()), list(predetermined.keys())))\n\n        # skip atom pairings that have been predetermined for other atoms\n        for n1_bond, n2_bond in candidate_pairings:\n            if n1_bond in predetermined or n2 in predetermined:\n                if predetermined[n1_bond] != n2_bond or predetermined[n2_bond] != n1_bond:\n                    candidate_pairings.remove((n1_bond, n2_bond))\n\n    # but they will be considered as a group\n    larger_suptops = []\n    pairing_and_suptop = {}\n    for n1_bond, n2_bond in candidate_pairings:\n        # fixme - ideally we would allow other typing than just the chemical element\n        if n1_bond.atom.element is not n2_bond.atom.element:\n            continue\n\n        logger.debug(f'sampling {n1_bond}, {n2_bond}')\n\n        # create a copy of the sup_top to allow for different traversals\n        # fixme: note that you could just send bonds, and that would have both parent etc with a bit of work\n        larger_suptop = _overlay(n1_bond.atom, n2_bond.atom,\n                                  parent_n1=n1, parent_n2=n2,\n                                  bond_types=(n1_bond.type, n2_bond.type),\n                                  suptop=suptop,\n                                  ignore_coords=ignore_coords,\n                                  use_element_type=use_element_type,\n                                  exact_coords_cue=exact_coords_cue)\n\n        if larger_suptop is not None:\n            larger_suptops.append(larger_suptop)\n            pairing_and_suptop[(n1_bond, n2_bond)] = larger_suptop\n\n    # todo\n    # check for \"predetermined\" atoms. Ie if they have the same coordinates,\n    # then that's the path to take, rather than a competing path??\n\n    # nothing further grown out of this suptop, so it is final\n    if not larger_suptops:\n        return suptop\n\n    # fixme: compare every two pairs of returned suptops, if they are compatible, join them\n    # fixme - note that we are repeating this partly below\n    # it also removes subgraph suptops\n    #all_solutions = merge_compatible_suptops(larger_suptops)\n    all_solutions = merge_compatible_suptops_faster(pairing_and_suptop, min(len(p1_bonds), len(p2_bonds)))\n\n    # if you couldn't merge any solutions, return the largest one\n    if not all_solutions:\n        all_solutions = list(pairing_and_suptop.values())\n\n    # sort in the descending order\n    all_solutions.sort(key=lambda st: len(st), reverse=True)\n    for sol1, sol2 in itertools.combinations(all_solutions, r=2):\n        if sol1.eq(sol2):\n            logger.debug(f\"Found the same solution and removing, solution: {sol1.matched_pairs}\")\n            if sol2 in all_solutions:\n                all_solutions.remove(sol2)\n\n    best_suptop = extract_best_suptop(all_solutions, ignore_coords)\n    return best_suptop\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.superimpose_topologies","title":"superimpose_topologies","text":"
    superimpose_topologies(top1_nodes, top2_nodes, pair_charge_atol=0.1, use_charges=True, use_coords=True, starting_node_pairs=None, force_mismatch=None, disjoint_components=False, net_charge_filter=True, net_charge_threshold=0.1, redistribute_charges_over_unmatched=True, parmed_ligA=None, parmed_ligZ=None, align_molecules=True, partial_rings_allowed=True, ignore_charges_completely=False, ignore_bond_types=True, ignore_coords=False, use_general_type=True, use_only_element=False, check_atom_names_unique=True, starting_pairs_heuristics=True, starting_pair_seed=None, config=None)\n

    The main function that manages the entire process.

    TODO: - check if each molecule topology is connected

    Source code in ties/topology_superimposer.py
    def superimpose_topologies(top1_nodes,\n                           top2_nodes,\n                           pair_charge_atol=0.1,\n                           use_charges=True,\n                           use_coords=True,\n                           starting_node_pairs=None,\n                           force_mismatch=None,\n                           disjoint_components=False,\n                           net_charge_filter=True,\n                           net_charge_threshold=0.1,\n                           redistribute_charges_over_unmatched=True,\n                           parmed_ligA=None,\n                           parmed_ligZ=None,\n                           align_molecules=True,\n                           partial_rings_allowed=True,\n                           ignore_charges_completely=False,\n                           ignore_bond_types=True,\n                           ignore_coords=False,\n                           use_general_type=True,\n                           use_only_element=False,\n                           check_atom_names_unique=True,\n                           starting_pairs_heuristics=True,\n                           starting_pair_seed=None,\n                           config=None):\n    \"\"\"\n    The main function that manages the entire process.\n\n    TODO:\n    - check if each molecule topology is connected\n    \"\"\"\n\n    if not ignore_charges_completely:\n        whole_charge = SuperimposedTopology.validate_charges(top1_nodes, top2_nodes)\n\n    # ensure that none of the atom names across the two molecules are the different\n    if check_atom_names_unique:\n        same_atom_names = {a.name for a in top1_nodes}.intersection({a.name for a in top2_nodes})\n        if len(same_atom_names) != 0:\n            logger.debug(f\"The atoms across the two ligands have the same atom names. \"\n                  f\"This might make it harder to trace back any problems. \"\n                  f\"Please ensure atom names are unique across the two ligands. : {same_atom_names}\")\n\n    if config is None:\n        weights = [1, 1]\n    else:\n        weights = config.weights\n\n    # Get the superimposed topology(/ies).\n    suptops = _superimpose_topologies(top1_nodes, top2_nodes, parmed_ligA, parmed_ligZ,\n                                      starting_node_pairs=starting_node_pairs,\n                                      ignore_coords=ignore_coords,\n                                      use_general_type=use_general_type,\n                                      starting_pairs_heuristics=starting_pairs_heuristics,\n                                      starting_pair_seed=starting_pair_seed,\n                                      weights=weights)\n    if not suptops:\n        warnings.warn('Did not find a single superimposition state.')\n        return None\n\n    logger.debug(f'Phase 1: The number of SupTops found: {len(suptops)}')\n    logger.debug(f'SupTops lengths:  {\", \".join([f\"ST{st.id}: {len(st)}\" for st in suptops])}')\n\n    # ignore bond types\n    # they are ignored when creating the run file with tleap anyway\n    for st in suptops:\n        # fixme - transition to config\n        st.ignore_bond_types = ignore_bond_types\n\n    # link the suptops to their original molecule data\n    for suptop in suptops:\n        # fixme - transition to config\n        suptop.set_tops(top1_nodes, top2_nodes)\n        suptop.set_parmeds(parmed_ligA, parmed_ligZ)\n\n    # align the 3D coordinates before applying further changes\n    # use the largest suptop to align the molecules\n    if align_molecules and not ignore_coords:\n        def take_largest(x, y):\n            return x if len(x) > len(y) else y\n        reduce(take_largest, suptops).align_ligands_using_mcs()\n        logger.info(f'RMSD of the best overlay: {suptops[0].align_ligands_using_mcs():.2f}')\n\n    # fixme - you might not need because we are now doing this on the way back\n    # if useCoords:\n    #     for sup_top in sup_tops:\n    #         sup_top.correct_for_coordinates()\n\n    # mismatch atoms as requested\n    if force_mismatch:\n        for sp in suptops:\n            for (a1, a2) in sp.matched_pairs[::-1]:\n                if (a1.name, a2.name) in force_mismatch:\n                    sp.remove_node_pair((a1, a2))\n                    logger.debug(f'Removing the pair: {((a1, a2))}, as requested')\n\n    # ensure that ring-atoms are not matched to non-ring atoms\n    for st in suptops:\n        st.ringring()\n\n    # introduce exceptions to the atom type types so that certain\n    # different atom types are seen as the same\n    # ie allow to swap cc-cd with cd-cc (and other pairs)\n    for st in suptops:\n        st.match_gaff2_nondirectional_bonds()\n\n    # remove matched atom pairs that have a different specific atom type\n    if not use_only_element:\n        for st in suptops:\n            # fixme - rename\n            st.enforce_matched_atom_types_are_the_same()\n\n    # ensure that the bonds are used correctly.\n    # If the bonds disagree, but atom types are the same, remove both bonded pairs\n    # we cannot have A-B where the bonds are different. In this case, we have A-B=C and A=B-C in a ring,\n    # we could in theory remove A,B,C which makes sense as these will show slightly different behaviour,\n    # and this we we avoid tensions in the bonds, and represent both\n    # fixme - apparently we are not relaying on these?\n    # turned off as this is reflected in the atom type\n    if not ignore_bond_types and False:\n        for st in suptops:\n            removed = st.removeMatchedPairsWithDifferentBonds()\n            if not removed:\n                logger.debug(f'Removed bonded pairs due to different bonds: {removed}')\n\n    if not partial_rings_allowed:\n        # remove partial rings, note this is a cascade problem if there are double rings\n        for suptop in suptops:\n            suptop.enforce_no_partial_rings()\n            logger.debug(f'Removed pairs because partial rings are not allowed {suptop._removed_because_unmatched_rings}')\n\n    # note that charges need to be checked before assigning IDs.\n    # ie if charges are different, the matched pair\n    # becomes two different atoms with different IDs\n    if use_charges and not ignore_charges_completely:\n        for sup_top in suptops:\n            removed = sup_top.unmatch_pairs_with_different_charges(atol=pair_charge_atol)\n            if removed:\n                logger.debug(f'Removed pairs with charge incompatibility: '\n                      f'{[(s[0], f\"{s[1]:.3f}\") for s in sup_top._removed_pairs_with_charge_difference]}')\n\n    if not partial_rings_allowed:\n        # We once again check if partial rings were created due to different charges on atoms.\n        for suptop in suptops:\n            suptop.enforce_no_partial_rings()\n            logger.debug(f'Removed pairs because partial rings are not allowed {suptop._removed_because_unmatched_rings}')\n\n    if net_charge_filter and not ignore_charges_completely:\n        # Note that we apply this rule to each suptop.\n        # This is because we are only keeping one suptop right now.\n        # However, if disjointed components are allowed, these number might change.\n        # ensure that each suptop component has net charge differences < 0.1\n        # Furthermore, disjointed components has not yet been applied,\n        # even though it might have an effect, fixme - should disjointed be applied first?\n        # to account for this implement #251\n        logger.debug(f'Accounting for net charge limit of {net_charge_threshold:.3f}')\n        for suptop in suptops[::-1]:\n            suptop.apply_net_charge_filter(net_charge_threshold)\n\n            # remove the suptop from the list if it's empty\n            if len(suptop) == 0:\n                suptops.remove(suptop)\n                continue\n\n            # Display information\n            if suptop._removed_due_to_net_charge:\n                logger.debug(f'SupTop: Removed pairs due to net charge: '\n                      f'{[[p[0], f\"{p[1]:.3f}\"] for p in suptop._removed_due_to_net_charge]}')\n\n    if not partial_rings_allowed:\n        # This is the 3rd check of partial rings. This time they might have been created due to net_charges.\n        for suptop in suptops:\n            suptop.enforce_no_partial_rings()\n            logger.debug(f'Removed pairs because partial rings are not allowed {suptop._removed_because_unmatched_rings}')\n\n    # remove the suptops that are empty\n    for st in suptops[::-1]:\n        if len(st) == 0:\n            suptops.remove(st)\n\n    if not disjoint_components:\n        logger.debug(f'Checking for disjoint components in the {len(suptops)} suptops')\n        # ensure that each suptop represents one CC\n        # check if the graph was divided after removing any pairs (e.g. due to charge mismatch)\n        # fixme - add the log about which atoms are removed?\n        [st.largest_cc_survives() for st in suptops]\n\n        for st in suptops:\n            logger.debug('Removed disjoint components: ', st._removed_because_disjointed_cc)\n\n        # fixme\n        # remove the smaller suptop, or one arbitrary if they are equivalent\n        # if len(suptops) > 1:\n        #     max_len = max([len(suptop) for suptop in suptops])\n        #     for suptop in suptops[::-1]:\n        #         if len(suptop) < max_len:\n        #             suptops.remove(suptop)\n        #\n        #     # if there are equal length suptops left, take only the first one\n        #     if len(suptops) > 1:\n        #         suptops = [suptops[0]]\n        #\n        # assert len(suptops) == 1, suptops\n\n    suptop = extract_best_suptop(suptops, ignore_coords, get_list=False)\n\n    if redistribute_charges_over_unmatched and not ignore_charges_completely:\n        # assume that none of the suptops are disjointed\n        logger.debug('Assuming that all suptops are separate at this point')\n        # fixme: apply distribution of q only on the first st, that's the best one anyway,\n\n        # we only want to apply redistribution once on the largest piece for now\n        suptop.redistribute_charges()\n\n    # atom ID assignment has to come after any removal of atoms due to their mismatching charges\n    suptop.assign_atoms_ids(1)\n\n    # there might be several best solutions, order them according the RMSDs\n    # suptops.sort(key=lambda st: st.rmsd())\n\n    # fixme - remove the hydrogens without attached heavy atoms\n\n    # resolve_sup_top_multiple_match(sup_tops_charges)\n    # sup_top_correct_chirality(sup_tops_charges, sup_tops_no_charges, atol=atol)\n\n    # carry out a check. Each\n    if align_molecules and not ignore_coords:\n        main_rmsd = suptop.align_ligands_using_mcs()\n        for mirror in suptop.mirrors:\n            mirror_rmsd = mirror.align_ligands_using_mcs()\n            if mirror_rmsd < main_rmsd:\n                logger.debug('THE MIRROR RMSD IS LOWER THAN THE MAIN RMSD')\n        suptop.align_ligands_using_mcs(overwrite_original=True)\n\n    # print a general summary\n    logger.info('-------- Summary -----------')\n    logger.info(f'Number of matched pairs: {len(suptop.matched_pairs)} out of {len(top1_nodes)}L/{len(top2_nodes)}R')\n    logger.info(f'Disappearing atoms: { (len(top1_nodes) - len(suptop.matched_pairs)) / len(top1_nodes) * 100:.1f}%')\n    logger.info(f'Appearing atoms: { (len(top2_nodes) - len(suptop.matched_pairs)) / len(top2_nodes) * 100:.1f}%')\n\n    return suptop\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.extract_best_suptop","title":"extract_best_suptop","text":"
    extract_best_suptop(suptops, ignore_coords, weights=[1, 1], get_list=False)\n

    Assumes that any merging possible already took place. We now have a set of solutions and have to select the best ones.

    :param suptops: :param ignore_coords: :return:

    Source code in ties/topology_superimposer.py
    def extract_best_suptop(suptops, ignore_coords, weights=[1, 1], get_list=False):\n    \"\"\"\n    Assumes that any merging possible already took place.\n    We now have a set of solutions and have to select the best ones.\n\n    :param suptops:\n    :param ignore_coords:\n    :return:\n    \"\"\"\n    # fixme - ignore coords currently does not work\n    # multiple different paths to traverse the topologies were found\n    # this means some kind of symmetry in the topologies\n    # For example, in the below drawn case (starting from C1-C11) there are two\n    # solutions: (O1-O11, O2-O12) and (O1-O12, O2-O11).\n    #     LIGAND 1        LIGAND 2\n    #        C1              C11\n    #        \\                \\\n    #        N1              N11\n    #        /\\              / \\\n    #     O1    O2        O11   O12\n    # Here we decide which of the mappings is better.\n    # fixme - uses coordinates to decide which mapping is better.\n    #  - Improve: use dihedral angles to decide which mapping is better too\n    def item_or_list(suptops):\n        if get_list:\n            return suptops\n        else:\n            return suptops[0]\n\n    if len(suptops) == 0:\n        warnings.warn('Cannot decide on the best mapping without any suptops...')\n        return None\n\n    elif len(suptops) == 1:\n        return item_or_list(suptops)\n\n    #candidates = copy.copy(suptops)\n\n    # sort from largest to smallest\n    suptops.sort(key=lambda st: len(st), reverse=True)\n\n    if ignore_coords:\n        return item_or_list(suptops)\n\n    # when length is the same, take the smaller RMSD\n    # most likely this is about hydrogens\n    different_length_suptops = []\n    for key, same_length_suptops in itertools.groupby(suptops, key=lambda st: len(st)):\n        # order by RMSD\n        sorted_by_rmsd = sorted(same_length_suptops, key=lambda st: st.align_ligands_using_mcs())\n        # these have the same lengths and the same RMSD, so they must be mirrors\n        for suptop in sorted_by_rmsd[1:]:\n            if suptop.is_mirror_of(sorted_by_rmsd[0]):\n                sorted_by_rmsd[0].add_mirror_suptop(suptop)\n            else:\n                # add it as a different solution\n                different_length_suptops.append(suptop)\n        different_length_suptops.append(sorted_by_rmsd[0])\n\n    # sort using weights\n    # score = mcs_score * weight - rmsd * weight ;\n    def score(st):\n        # inverse for 0 to be optimal\n        mcs_score = (1 - st.mcs_score()) * weights[0]\n\n        # rmsd 0 is best as well\n        rmsd_score = st.align_ligands_using_mcs() * weights[1]\n\n        return (mcs_score + rmsd_score) / len(weights)\n\n    different_length_suptops.sort(key=score)\n    # if they have a different length, there must be a reason why it is better.\n    # todo\n\n    return item_or_list(different_length_suptops)\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.is_mirror_of_one","title":"is_mirror_of_one","text":"
    is_mirror_of_one(candidate_suptop, suptops, ignore_coords)\n

    \"Mirror\" in the sense that it is an alternative topological way to traverse the molecule.

    Depending on the \"better\" fit between the two mirrors, we pick the one that is better.

    Source code in ties/topology_superimposer.py
    def is_mirror_of_one(candidate_suptop, suptops, ignore_coords):\n    \"\"\"\n    \"Mirror\" in the sense that it is an alternative topological way to traverse the molecule.\n\n    Depending on the \"better\" fit between the two mirrors, we pick the one that is better.\n    \"\"\"\n    for next_suptop in suptops:\n        if next_suptop.is_mirror_of(candidate_suptop):\n            # the suptop saved as the mirror should be the suptop\n            # that is judged to be of a lower quality\n            best_suptop = extract_best_suptop([candidate_suptop, next_suptop], ignore_coords)\n\n            if next_suptop is best_suptop:\n                next_suptop.add_mirror_suptop(candidate_suptop)\n            else:\n                suptops.remove(next_suptop)\n                suptops.append(candidate_suptop)\n\n            return True\n\n    return False\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.generate_nxg_from_list","title":"generate_nxg_from_list","text":"
    generate_nxg_from_list(atoms)\n

    Helper function. Generates a graph from a list of atoms @parameter atoms: follow the internal format for atoms

    Source code in ties/topology_superimposer.py
    def generate_nxg_from_list(atoms):\n    \"\"\"\n    Helper function. Generates a graph from a list of atoms\n    @parameter atoms: follow the internal format for atoms\n    \"\"\"\n    g = nx.Graph()\n    # add attoms\n    [g.add_node(a) for a in atoms]\n    # add all the edges\n    for a in atoms:\n        # add the edges from nA\n        for a_bonded in a.bonds:\n            g.add_edge(a, a_bonded.atom)\n\n    return g\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.get_starting_configurations","title":"get_starting_configurations","text":"
    get_starting_configurations(left_atoms, right_atoms, fraction=0.2, filter_ring_c=True)\n
    Minimise the number of starting configurations to optimise the process speed.\nUse:\n * the rarity of the specific atom types,\n * whether the atoms are bottlenecks (so they do not suffer from symmetry).\n    The issue with symmetry is that it is impossible to find the proper\n    symmetry match if you start from the wrong symmetry.\n@parameter fraction: ensure that the number of atoms used to start the traversal is not more\n    than the fraction value of the overall number of possible matches, counted as\n    a fraction of the maximum possible number of pairs (MIN(LEFTNODES, RIGHTNODES))\n@parameter filter_ring_c: filter out the carbon elements in the rings to avoid any issues\n    with the symmetry. This assumes that a ring usually has one N element, etc.\n

    TODO - ignore hydrogens?

    Source code in ties/topology_superimposer.py
    def get_starting_configurations(left_atoms, right_atoms, fraction=0.2, filter_ring_c=True):\n    \"\"\"\n        Minimise the number of starting configurations to optimise the process speed.\n        Use:\n         * the rarity of the specific atom types,\n         * whether the atoms are bottlenecks (so they do not suffer from symmetry).\n            The issue with symmetry is that it is impossible to find the proper\n            symmetry match if you start from the wrong symmetry.\n        @parameter fraction: ensure that the number of atoms used to start the traversal is not more\n            than the fraction value of the overall number of possible matches, counted as\n            a fraction of the maximum possible number of pairs (MIN(LEFTNODES, RIGHTNODES))\n        @parameter filter_ring_c: filter out the carbon elements in the rings to avoid any issues\n            with the symmetry. This assumes that a ring usually has one N element, etc.\n\n    TODO - ignore hydrogens?\n    \"\"\"\n    logger.debug('Superimposition: optimising the search by narrowing down the starting configuration. ')\n    left_atoms_noh = list(filter(lambda a: a.element != 'H', left_atoms))\n    right_atoms_noh = list(filter(lambda a: a.element != 'H', right_atoms))\n\n    # find out which atoms types are common across the two molecules\n    # fixme - consider subclassing atom from MDAnalysis class and adding functions for some of these features\n    # first, find the unique types for each molecule\n    left_types = {left_atom.type for left_atom in left_atoms_noh}\n    right_types = {right_atom.type for right_atom in right_atoms_noh}\n    common_types = left_types.intersection(right_types)\n\n    # for each atom type, check how many maximum atoms can theoretically be matched\n    per_type_max_counter = {}\n    for atom_type in common_types:\n        left_count_by_type = sum([1 for left_atom in left_atoms if left_atom.type == atom_type])\n        right_count_by_type = sum([1 for right_atom in right_atoms if right_atom.type == atom_type])\n        per_type_max_counter[atom_type] = min(left_count_by_type, right_count_by_type)\n    max_overlap_size = sum(per_type_max_counter.values())\n    logger.info(f'Largest MCS size: {max_overlap_size}')\n\n    left_atoms_starting = left_atoms_noh[:]\n    right_atoms_starting = right_atoms_noh[:]\n\n    # ignore carbons in cycles\n    # fixme - we should not use this for macrocycles, which should be ignored here\n    if filter_ring_c:\n        nxl = generate_nxg_from_list(left_atoms)\n        for cycle in nx.cycle_basis(nxl):\n            # ignore the carbons in the cycle\n            cycle_carbons = list(filter(lambda a: a.element == 'C', cycle))\n            logger.debug(f'Superimposition of left atoms: Ignoring carbons as starting configurations because '\n                  f'they are carbons in a cycle: {cycle_carbons}')\n            [left_atoms_starting.remove(a) for a in cycle_carbons if a in left_atoms_starting]\n        nxr = generate_nxg_from_list(right_atoms_starting)\n        for cycle in nx.cycle_basis(nxr):\n            # ignore the carbons in the cycle\n            cycle_carbons = list(filter(lambda a: a.element == 'C', cycle))\n            logger.debug(f'Superimposition of right atoms: Ignoring carbons as starting configurations because '\n                  f'they are carbons in a cycle: {cycle_carbons}')\n            [right_atoms_starting.remove(a) for a in cycle_carbons if a in right_atoms_starting]\n\n    # find out which atoms types are common across the two molecules\n    # fixme - consider subclassing atom from MDAnalysis class and adding functions for some of these features\n    # first, find the unique types for each molecule\n    left_types = {left_atom.type for left_atom in left_atoms_starting}\n    right_types = {right_atom.type for right_atom in right_atoms_starting}\n    common_types = left_types.intersection(right_types)\n\n    # for each atom type, check how many maximum atoms can theoretically be matched\n    paired_by_type = []\n    max_after_cycle_carbons = 0\n    for atom_type in common_types:\n        picked_left = list(filter(lambda a: a.type == atom_type, left_atoms_starting))\n        picked_right = list(filter(lambda a: a.type == atom_type, right_atoms_starting))\n        paired_by_type.append([picked_left, picked_right])\n        max_after_cycle_carbons += min(len(picked_left), len(picked_right))\n    logger.debug(f'Superimposition: simple max match of atoms after cycle carbons exclusion: {max_after_cycle_carbons}')\n\n    # sort atom according to their type rarity\n    # use the min across, since 1x4 mapping will give 4 options only, so we count this as one,\n    # but 4x4 would give 16,\n    sorted_paired_by_type = sorted(paired_by_type, key=lambda p: min(len(p[0]), len(p[1])))\n\n    # find the atoms in each type and generate appropriate pairs,\n    # use only a fraction of the maximum theoretical match\n    desired_number_of_pairs = int(fraction * max_overlap_size)\n\n    starting_configurations = []\n    added_counter = 0\n    for rare_left_atoms, rare_right_atoms in sorted_paired_by_type:\n        # starting_configurations\n        starting_configurations.extend(list(itertools.product(rare_left_atoms, rare_right_atoms)))\n        added_counter += min(len(rare_left_atoms), len(rare_right_atoms))\n        if added_counter > desired_number_of_pairs:\n            break\n\n    logger.debug(f'Superimposition: initial starting pairs for the search: {starting_configurations}')\n    return starting_configurations\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer._superimpose_topologies","title":"_superimpose_topologies","text":"
    _superimpose_topologies(top1_nodes, top2_nodes, mda1_nodes=None, mda2_nodes=None, starting_node_pairs=None, ignore_coords=False, use_general_type=True, starting_pairs_heuristics=True, starting_pair_seed=None, weights=[1, 1])\n

    Superimpose two molecules.

    @parameter rare_atoms_starting_pair: instead of trying every possible pair for the starting configuration, use several information to narrow down the good possible starting configuration. Specifically, use two things: 1) the extact atom type, find how rare they are, and use the rarity to make the call, 2) use the \"linkers\" and areas that are not parts of the rings to avoid the issue of symmetry in the ring. We are striving here to have 5% starting configurations.

    Source code in ties/topology_superimposer.py
    def _superimpose_topologies(top1_nodes, top2_nodes, mda1_nodes=None, mda2_nodes=None,\n                            starting_node_pairs=None,\n                            ignore_coords=False,\n                            use_general_type=True,\n                            starting_pairs_heuristics=True,\n                            starting_pair_seed=None,\n                            weights=[1, 1]):\n    \"\"\"\n    Superimpose two molecules.\n\n    @parameter rare_atoms_starting_pair: instead of trying every possible pair for the starting configuration,\n        use several information to narrow down the good possible starting configuration. Specifically,\n        use two things: 1) the extact atom type, find how rare they are, and use the rarity to make the call,\n        2) use the \"linkers\" and areas that are not parts of the rings to avoid the issue of symmetry in the ring.\n        We are striving here to have 5% starting configurations.\n    \"\"\"\n\n    # superimposed topologies\n    suptops = []\n    # grow the topologies using every combination node1-node2 as the starting point\n    # fixme - Test/Optimisation: create a theoretical maximum of a match between two molecules\n    # - Proposal 1: find junctions and use them to start the search\n    # - Analyse components of the graph (ie rotatable due to a single bond connection) and\n    #   pick a starting point from each component\n    if not starting_node_pairs:\n        # generate each to each nodes\n        if starting_pair_seed:\n            left_atom = [a for a in list(top1_nodes) if a.name == starting_pair_seed[0]][0]\n            right_atom = [a for a in list(top2_nodes) if a.name == starting_pair_seed[1]][0]\n            starting_node_pairs = [(left_atom, right_atom), ]\n        elif starting_pairs_heuristics:\n            starting_node_pairs = get_starting_configurations(top1_nodes, top2_nodes)\n            logger.debug('Using heuristics to select the initial pairs for searching the maximum overlap.'\n                  'Could produce non-optimal results.')\n        else:\n            starting_node_pairs = list(itertools.product(top1_nodes, top2_nodes))\n            logger.debug('Checking all possible initial pairs to find the optimal MCS. ')\n\n    for node1, node2 in starting_node_pairs:\n        # with the given starting two nodes, generate the maximum common component\n        suptop = SuperimposedTopology(list(top1_nodes), list(top2_nodes), mda1_nodes, mda2_nodes)\n        # fixme turn into a property\n        candidate_suptop = _overlay(node1, node2, parent_n1=None, parent_n2=None, bond_types=(None, None),\n                                    suptop=suptop,\n                                    ignore_coords=ignore_coords,\n                                    use_element_type=use_general_type)\n        if candidate_suptop is None:\n            # there is no overlap, ignore this case\n            continue\n\n        # check if the maximal possible solution was found\n        # Optimise - can you at this point finish the superimposition if the molecules are fully superimposed?\n        # candidate_suptop.is_subgraph_of_global_top()\n\n        if exists_in(candidate_suptop, suptops):\n            continue\n\n        # ignore if it is a subgraph of another solution\n        if subgraph_of(candidate_suptop, suptops):\n            continue\n\n        # check if this superimposed topology is a mirror of one that already exists\n        # fixme the order matters in this place\n        # fixme - what if the mirror has a lower rmsd match? in that case, pick that mirror here\n        if is_mirror_of_one(candidate_suptop, suptops, ignore_coords):\n            continue\n\n        #\n        remove_candidates_subgraphs(candidate_suptop, suptops)\n\n        # while comparing partial overlaps, suptops can be modified\n        # and_ignore = solve_partial_overlaps(candidate_suptop, suptops)\n        # if and_ignore:\n        #     continue\n\n        # fixme - what to do when about the odd pairs randomH-randomH etc? they won't be found in other graphs\n        # follow a rule: if this node was used before in a larger superimposed topology, than it should\n        # not be in the final list (we guarantee that each node is used only once)\n        suptops.append(candidate_suptop)\n\n    # if there are only hydrogens superimposed without a connection to any heavy atoms, ignore these too\n    for suptop in suptops[::-1]:\n        all_hydrogens = True\n        for node1, _ in suptop.matched_pairs:\n            if not node1.type == 'H':\n                all_hydrogens = False\n                break\n        if all_hydrogens:\n            logger.debug(f\"Removing sup top because only hydrogens found {suptop.matched_pairs}\")\n            suptops.remove(suptop)\n\n    # TEST: check that each node was used only once, fixme use only on the winner\n    # for suptop in suptops:\n    #     [all_nodes.extend([node1, node2]) for node1, node2 in suptop.matched_pairs]\n    #     pair_count += len(suptop.matched_pairs)\n    #     assert len(set(all_nodes)) == 2 * pair_count\n\n    # TEST: check that the nodes on the left are always from topology 1 and the nodes on the right are always from top2\n    for suptop in suptops:\n        for node1, node2 in suptop.matched_pairs:\n            assert node1 in list(top1_nodes)\n            assert node2 in list(top2_nodes)\n\n    # clean the overlays by removing sub_overlays.\n    # ie if all atoms in an overlay are found to be a bigger part of another overlay,\n    # then that overlay is better\n    logger.info(f\"Found overlays: {len(suptops)}\")\n\n    # finally, once again, order the suptops and return the best one\n    suptops = extract_best_suptop(suptops, ignore_coords, weights, get_list=True)\n\n    # fixme - return other info\n    return suptops\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.get_atoms_bonds_from_mol2","title":"get_atoms_bonds_from_mol2","text":"
    get_atoms_bonds_from_mol2(ref_filename, mob_filename, use_general_type=True)\n

    Use Parmed to load the files.

    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.get_atoms_bonds_from_mol2--returns","title":"returns","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer.get_atoms_bonds_from_mol2--1-a-dictionary-with-charges-eg-item-c17-0222903","title":"1) a dictionary with charges, e.g. Item: \"C17\" : -0.222903","text":""},{"location":"reference/topology_superimposer/#ties.topology_superimposer.get_atoms_bonds_from_mol2--2-a-list-of-bonds","title":"2) a list of bonds","text":"Source code in ties/topology_superimposer.py
    def get_atoms_bonds_from_mol2(ref_filename, mob_filename, use_general_type=True):\n    \"\"\"\n    Use Parmed to load the files.\n\n    # returns\n    # 1) a dictionary with charges, e.g. Item: \"C17\" : -0.222903\n    # 2) a list of bonds\n    \"\"\"\n    ref = parmed.load_file(str(ref_filename), structure=True)\n    mobile = parmed.load_file(str(mob_filename), structure=True)\n\n    def create_atoms(parmed_atoms):\n        \"\"\"\n        # convert the Parmed atoms into Atom objects.\n        \"\"\"\n        atoms = []\n        for parmed_atom in parmed_atoms:\n            atom_type = parmed_atom.type\n            # atom type might be empty if\n            if not atom_type:\n                # use the atom name as the atom type, e.g. C7\n                atom_type = parmed_atom.name\n\n\n            try:\n                atom = Atom(name=parmed_atom.name, atom_type=atom_type, charge=parmed_atom.charge, use_general_type=use_general_type)\n            except AttributeError:\n                # most likely the charges were missing, manually set the charges to 0\n                atom = Atom(name=parmed_atom.name, atom_type=atom_type, charge=0.0, use_general_type=use_general_type)\n                logger.warning('One of the input files is missing charges. Setting the charge to 0')\n            atom.id = parmed_atom.idx\n            atom.position = [parmed_atom.xx, parmed_atom.xy, parmed_atom.xz]\n            atom.resname = parmed_atom.residue.name\n            atoms.append(atom)\n        return atoms\n\n    universe_ref_atoms = create_atoms(ref.atoms)\n    # note that these coordinate should be superimposed\n    universe_mob_atoms = create_atoms(mobile.atoms)\n\n    # fixme - add a check that all the charges come to 0 as declared in the header\n    universe_ref_bonds = [(b.atom1.idx, b.atom2.idx, b.order) for b in ref.bonds]\n    universe_mob_bonds = [(b.atom1.idx, b.atom2.idx, b.order) for b in mobile.bonds]\n\n    return universe_ref_atoms, universe_ref_bonds, \\\n           universe_mob_atoms, universe_mob_bonds, \\\n           ref, mobile\n
    "},{"location":"reference/topology_superimposer/#ties.topology_superimposer.assign_coords_from_pdb","title":"assign_coords_from_pdb","text":"
    assign_coords_from_pdb(atoms, pdb_atoms)\n

    Match the atoms from the ParmEd object based on a .pdb file and overwrite the coordinates from ParmEd. :param atoms: internal Atom representation (fixme: refer to it here in docu), will have their coordinates overwritten. :param pdb_atoms: atoms loaded with ParmEd with the coordinates to be used

    Source code in ties/topology_superimposer.py
    def assign_coords_from_pdb(atoms, pdb_atoms):\n    \"\"\"\n    Match the atoms from the ParmEd object based on a .pdb file\n    and overwrite the coordinates from ParmEd.\n    :param atoms: internal Atom representation (fixme: refer to it here in docu),\n        will have their coordinates overwritten.\n    :param pdb_atoms: atoms loaded with ParmEd with the coordinates to be used\n\n    \"\"\"\n    for atom in atoms:\n        # find the corresponding atom\n        found_match = False\n        for pdb_atom in pdb_atoms.atoms:\n            if pdb_atom.name.upper() == atom.name.upper():\n                # charges?\n                atom.position = (pdb_atom.xx, pdb_atom.xy, pdb_atom.xz)\n                found_match = True\n                break\n        if not found_match:\n            logger.error(f\"Did not find atom? {atom.name}\")\n            raise Exception(\"wait a minute\")\n
    "},{"location":"usage/api/","title":"Examples - Python","text":"

    TIES also offers a python API. Here is a minimal example:

    from ties import Pair\n\n# load the two ligands and use the default configuration\npair = Pair('l02.mol2', 'l03.mol2')\n# superimpose the ligands passed above\nhybrid = pair.superimpose()\n\n# save the results\nhybrid.write_metadata('meta_l02_l03.json')\nhybrid.write_pdb('l02_l03_morph.pdb')\nhybrid.write_mol2('l02_l03_morph.mol2')\n

    This minimal example can be extended with the protein to generate the input for the TIES_MD package for the simulations in either NAMD or OpenMM.

    Note that in this example we do not set any explicit settings. For that we need to employ the Config class which we can then pass to the Pair.

    Info

    Config contains the settings for all classes in the TIES package, and therefore can be used to define a protocol.

    Whereas all settings can be done in :class:Config, for clarity some can be passed separately here to the :class:Pair. This way, it overwrites the settings in the config object:

    from ties import Pair\nfrom ties import Config\nfrom ties import Protein\n\n\nconfig = Config()\n# configure the two settings\nconfig.workdir = 'ties20'\nconfig.md_engine = 'openmm'\n# set ligand_net_charge as a parameter,\n# which is equivalent to using config.ligand_net_charge\npair = Pair('l02.mol2', 'l03.mol2', ligand_net_charge=-1, config=config)\n# rename atoms to help with any issues\npair.make_atom_names_unique()\n\nhybrid = pair.superimpose()\n\n# save meta data to files\nhybrid.write_metadata()\nhybrid.write_pdb()\nhybrid.write_mol2()\n\n# add the protein for the full RBFE protocol\nconfig.protein = 'protein.pdb'\nconfig.protein_ff = 'leaprc.protein.ff14SB'\nprotein = Protein(config.protein, config)\nhybrid.prepare_inputs(protein=protein)\n

    Below we show the variation in which we are using :class:Config to pass the net charge of the molecule.

    from ties import Pair\nfrom ties import Config\n\n# explicitly create config (which will be used by all classes underneath)\nconfig = Config()\nconfig.ligand_net_charge = -1\n\npair = Pair('l02.mol2', 'l03.mol2', config=config)\npair.make_atom_names_unique()\n\n# overwrite the previous config settings with relevant parameters\nhybrid = pair.superimpose(use_element_in_superimposition=True, redistribute_q_over_unmatched=True)\n\n# save meta data to specific locations\nhybrid.write_metadata('result.json')\nhybrid.write_pdb('result.pdb')\nhybrid.write_mol2('result.mol2')\n\nhybrid.prepare_inputs()\n

    Note that there is also the :class:Ligand that supports additional operations, and can be passed directly to :class:Ligand.

    from ties import Ligand\n\n\nlig = Ligand('l02_same_atom_name.mol2')\n\n# prepare the .mol2 input\nlig.antechamber_prepare_mol2()\n\n# the final .mol2 file\nassert lig.current.exists()\n
    "},{"location":"usage/cli/","title":"CLI","text":"

    TIES can be access via both command line and python interface.

    In the smallest example one can carry out a superimposition employing only two ligands

    ties --ligands l03.mol2 l02.mol2\n

    Ideally these .mol2 files already have a pre-assigned charges in the last column for each atom. See for example MCL1 case.

    In the case of this example, we are working on molecules that are negatively charges (-1e), and we need to specify it:

    ties --ligands l03.mol2 l02.mol2 --ligand-net-charge -1\n

    The order the of the ligands matters and more ligands can be passed. This command creates by default a ties-input directory with all output files. These include meta_*_*.json files which contain the details about how the ligands were superimposed, and what configuration was used. The general directory structure will look like this:

        ties\n    \u251c\u2500\u2500 mol2\n    \u2502    \u251c\u2500\u2500 l02\n    \u2502    \u2514\u2500\u2500 l03\n    \u251c\u2500\u2500 prep\n    \u2502\u00a0\u00a0 \u251c\u2500\u2500 ligand_frcmods\n    \u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u251c\u2500\u2500 l02\n    \u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 l03\n    \u2502\u00a0\u00a0 \u2514\u2500\u2500 morph_frcmods\n    \u2502\u00a0\u00a0     \u2514\u2500\u2500 tests\n    \u2502\u00a0\u00a0         \u2514\u2500\u2500 l02_l03\n    \u2514\u2500\u2500 ties-l02-l03\n        \u2514\u2500\u2500 lig\n            \u2514\u2500\u2500 build\n

    Note that all the output generated by ambertools is stored, and can be investigated.

    The full RBFE requires also the protein, as well as the net charge of the ligands used in the transformation:

    ties -l l02.mol2 l03.mol2 -nc -1 --protein protein.pdb\n

    Check all the options with

    ties -h\n

    Warning

    This code is currently experimental and under active development. If you notice any problems, report them. *

    "}]} \ No newline at end of file