

"""This module contains functions for ligand hybrid structure/topology generation.
"""

import sys, os
import copy as cp
from . import *
from .ndx import *
from .utils import doLog
from .forcefield import *
from random import randint
try:
    from rdkit import Chem
    from rdkit.Chem import AllChem
    from rdkit.Chem import rdFMCS
    from rdkit.Chem import FragmentMatcher, Crippen, rdmolops
except:
    print('RDKit imports failed')


# ================
# Helper functions
# ================
def reformatPDB(fname,num,randint=42,bStrict=False):
    """Higher level command to read, format and call a pdb writer.

    Params
    ------
    fname : str
        input structure file name
    num : int
        number to mark the output pdb
    randint : int
        random number to mark the output pdb
    bStrict : bool
        limit atom names to 3 char (default False)
    Returns
    -------
    newname : str
        name of the output pdb
    atomNameID : dict
        dict[a.id] = a.name
    sigmaHoleID : array
        atom IDs for sigmaholes array = [a.id]
    """

    newname = "tempFormat_"+str(randint)+'_'+str(num)+".pdb"
    m = Model().read(fname)

    # adjust atom names and remember the changes
    atomNameID = {}
    sigmaHoleCounter = 1
    sigmaHoleID = []
    for a in m.atoms:
        newAtomName = a.name
        if ('EP' in a.name) or ('LPH' in a.name):
            newAtomName = 'HSH'+str(sigmaHoleCounter)
            sigmaHoleCounter+=1
            sigmaHoleID.append(a.id)
        atomNameID[a.id] = a.name
        a.name = newAtomName

    writeFormatPDB(newname,m,bStrict=bStrict)
    return(newname,atomNameID,sigmaHoleID)

def writeFormatPDB(fname,m,title="",nr=1,bStrict=False):
    """Writes formatted pdb of a ligand.

    Params
    ------
    fname : str
        output file name
    m : Model
        pmx model
    title : str
        currently not used
    nr : int
        currently not used 
    bStrict : bool
        limit atom names to 3 char (default False)
    Returns
    -------
    None
    """

    atNameLen = 4
    if bStrict==True:
        atNameLen = 3

    fp = open(fname,'w')
    for atom in m.atoms:
        foo = cp.deepcopy(atom)
        # chlorine
        if( 'CL' in atom.name or 'Cl' in atom.name or 'cl' in atom.name ):
            foo.name = "CL"#+"  "
            print(foo,file=fp)
        # bromine
        elif( 'BR' in atom.name or 'Br' in atom.name or 'br' in atom.name ):
            foo.name = "BR"#+"  "
            print(foo,file=fp)
        elif( len(atom.name) > atNameLen): # too long atom name
            foo = cp.deepcopy(atom)
            foo.name = foo.name[:atNameLen]
            print(foo,file=fp)
        else:
            print(atom,file=fp)
    fp.write('ENDMDL\n')
    fp.close()


def restoreAtomNames(mol,atomNameID):
    """Sets atomma names in RDKit model to those from the dictionary.

    Params
    ------
    mol : RDKit model
        structure model
    atomNameID : dictionary
        dict[a.id] = a.name

    Returns
    -------
    None
    """

    for atom in mol.GetAtoms():
        newname = atom.GetMonomerInfo().GetName()
        ind = atom.GetIdx()+1
        oldname = atomNameID[ind]
#        print oldname,newname,len(oldname),len(newname)
#        if (newname.strip() != oldname.strip()) and ('EP' in oldname):
        nametoset = "{:<4}".format(oldname[:4])
        atom.GetMonomerInfo().SetName(nametoset)

def submol_by_index(mol,ind):
    """Extract submolecule from RDKit model defined by index

    Params
    ------
    mol : RDKit_model
        molecule
    ind : array
        atom ids

    Returns
    -------
    copyMol : RDKit model
        submolecule
    """
    copyMol = cp.deepcopy(mol)
    editMol = Chem.EditableMol(copyMol)
    indRm = []
#   create an inverted list of ind, i.e. indRm
    for a in mol.GetAtoms():
        found = 0
        for i in ind:
            if(i == a.GetIdx()):
                found = 1
                break
        if(found == 0):
            indRm.append(a.GetIdx())
#   remove the indRm atoms
    indRm.sort(reverse=True)
    for i in indRm:
        editMol.RemoveAtom(i)
    copyMol = editMol.GetMol()
    return(copyMol)


def checkRingsOnlyFlag(mol1,mol2):
    """Checks if both molecules have rings.

    Params
    ------
    mol1 : rdkit_molecule
        molecule 1
    mol2 : rdkit_molecule
        molecule 2

    Returns
    -------
    bool
        True if both molecules have rings, False otherwise
    """

    flag = 0
    for atom in mol1.GetAtoms():
        if(atom.IsInRing()==True):
            flag = flag + 1
            break
    for atom in mol2.GetAtoms():
        if(atom.IsInRing()==True):
            flag = flag + 1
            break
    if(flag==2):
        return(True)
    else:
        return(False)

def assignFF(model, itp):
    """Assigns ff parameters from itp to the pmx Model.

    Params
    ------
    model : pmx Model
        model
    itp : itp TopolBase 
        itp

    Returns
    -------
    None
    """

    for i, atom in enumerate(model.atoms):
        at = itp.atoms[i]
        atom.atomtype = at.atomtype
        atom.cgnr = at.cgnr
        atom.q = at.q
        atom.m = at.m
        atom.atomtypeB = at.atomtypeB
        atom.qB = at.qB
        atom.mB = at.mB

def readPairsFile(fn):
    """Read pairs file.

    Params
    ------
    fn : str
        filename 

    Returns
    -------
    plst : array
        list of pairs
    """

    l = open(fn).readlines()
    plst = []
    for line in l:
       foo = line.split()
       plst.append([int(foo[0]),int(foo[1])])
    return plst

def adjustCoords(m,mol):
    """Replaces the coordinates of a pmx structure Model with those from RDKit structure model.

    Params
    ------
    m : pmx Model
        structure pmx
    mol : RDKit Model
        structure rdkit

    Returns
    -------
    None
    """

    conf = mol.GetConformer()
    for ai in m.atoms:
        ind = ai.id
#       print ai.x[0]
        posj = conf.GetAtomPosition(ind-1)
        ai.x[0] = posj.x
        ai.x[1] = posj.y
        ai.x[2] = posj.z

def restoreAtomNames(mol,atomNameDict):
    """Set atom names in an RDKit structure model to those in a provided dictionary.
    This is a specific function used by superimposeStructures()

    Params
    ------
    mol : RDKit Model
        structure rdkit
    atomNameDict : dictionary
        atom names

    Returns
    -------
    None
    """
    for atom in mol.GetAtoms():
        newname = atom.GetMonomerInfo().GetName()
        if newname in atomNameDict.keys():
            oldname = atomNameDict[newname]
            atom.GetMonomerInfo().SetName(oldname)

def superimposeStructures( fname1, fname2, m3, m2, plst, logfile ):
    """This is a special function for superimposing two ligands.
    Specific for ligandHybridTop

    Params
    ------
    fname1 : str
        filename for pdb 1
    fname2 : str
        filename for pdb 2
    m3 : pmx Model
        model m1 that will get the coordinates of m2
    m2 : pmx Model
        model m2 that will be fit on m1 
    plst : array
        pairs list
    Returns
    -------
    m3 : pmx Model
        model m1 fit onto m2
    m2 : pmx Model
        model m2 fit onto m1
    """

    n1 = []
    n2 = []
    for p in plst:
        n1.append(int(p[0])-1)
        n2.append(int(p[1])-1)

    ################################################
    doLog(logfile,'Superimposing mol2 on mol1')
    # mol1
    try:
        pdbName1,atomNameDict1,fooSH = reformatPDB(fname1,1)
    except:
        pdbName1,atomNameDict1,fooSH = reformatPDB(fname1,1,bStrict=True)
    mol1 = Chem.MolFromPDBFile(pdbName1,removeHs=False,sanitize=False)
    os.remove(pdbName1)
    # mol2
    try:
        pdbName2,atomNameDict2,fooSH = reformatPDB(fname2,2)
    except:
        pdbName2,atomNameDict2,fooSH = reformatPDB(fname2,2,bStrict=True)
    mol2 = Chem.MolFromPDBFile(pdbName2,removeHs=False,sanitize=False)
    os.remove(pdbName2)
    # create a backup of mol2 for later
    mol2backup = cp.deepcopy(mol2)
    # fit
    try:
        Chem.rdMolAlign.AlignMol(mol2,mol1,atomMap=list(zip(n2,n1)))
    except:
        doLog(logfile,'ERROR: fitting mol2 on mol1 failed')
    # adjust coordinates of m2
    adjustCoords(m2,mol2)
    restoreAtomNames(mol1,atomNameDict1)
    restoreAtomNames(mol2,atomNameDict2)

    ################################################
    doLog(logfile,'Superimposing mol1 on mol2')
    # fit
    try:
        Chem.rdMolAlign.AlignMol(mol1,mol2backup,atomMap=list(zip(n1,n2)))
    except:
        doLog(logfile,'ERROR: fitting mol2 on mol1 failed')
    # adjust coordinates of m1
    adjustCoords(m3,mol1)
    restoreAtomNames(mol1,atomNameDict1)
    restoreAtomNames(mol2,atomNameDict2)



# ======================================
# The main class for ligand atom mapping
# ======================================
class LigandAtomMapping:
    """Class contains two main functions (mcs and alignment)
    for ligand atom mapping.

    Parameters
    ----------
    mol1 : rdkit molecule object
        molecule 1
    ...

    Attributes
    ----------
    bH2H : bool
        map hydrogen-to-hydrogen
    ....

    """

#    def __init__(self, mol1, mol2, molForMcs1, molForMcs2, bH2H, bH2Hpolar, bH2Heavy, bdMCS, bRingsOnly, d, bChiral, sigmaHoleID1, sigmaHoleID2,
#                 t, bElements, bCarbonize, bBreakRings=False ):
    def __init__(self, **kwargs):
        self.mol1 = None
        self.mol2 = None
        self.molForMcs1 = None
        self.molForMcs2 = None
        self.bH2H = False
        self.H2Hpolar = False
        self.H2Heavy = False
        self.bdMCS = False
        self.bRingsOnly = False
        self.d = 0.05
        self.bChiral = True
        self.sigmaHoleID1 = []
        self.sigmaHoleID2 = []
        self.t = 10
        self.bExactElementMatch = False
        self.bElements = True
        self.bCarbonize = True
        self.logfile = False
        self.commandName = 'log'
        self.bBreakRings = False # for now always set to False, i.e. hidden option
        self.bCrippen = True # another option that is always set to True

        for key, val in kwargs.items():
            setattr(self,key,val)

    def _calc_score( self, n1,n2, mol1,mol2 ):
        res = 0.0
        nn1 = float(len(n1))
        nn2 = float(len(n2))
        res = (nn1+nn2)/2.0
        if( self.bH2H==True or self.bH2Heavy==True): # consider hydrogens
            na1 = float(self.mol1.GetNumAtoms())
            na2 = float(self.mol2.GetNumAtoms())
            if self.bH2Hpolar==False and self.bH2Heavy==False:
                # discard polar hydrogens, as they were not considered
                nhpolar1 = self._calc_polarH( mol1 )
                nhpolar2 = self._calc_polarH( mol2 )
                na1 = na1 - nhpolar1
                na2 = na2 - nhpolar2
        else: # no hydrogens
            na1 = float(self.mol1.GetNumHeavyAtoms())
            na2 = float(self.mol2.GetNumHeavyAtoms())
            if self.bH2Hpolar==True:
                # add polar hydrogens, as they were considered
                nhpolar1 = self._calc_polarH( mol1 )
                nhpolar2 = self._calc_polarH( mol2 )
                na1 = na1 + nhpolar1
                na2 = na2 + nhpolar2
        res = 1.0 - res/(na1+na2-res)
        return(res)

    def _calc_polarH( self, mol ):
        n = 0
        for a in mol.GetAtoms():
            if a.GetAtomicNum()==1:
                if self._isPolarH(a):
                    n = n+1
        return(n)

    def _write_pairs(self, n1,n2,pairsFilename):
        fp = open(pairsFilename,"w")
        for i1,i2 in zip(n1,n2):
            foo = i1 + 1
            bar = i2 + 1
            fp.write("%s    %s\n" % (foo,bar) )
        fp.close()

    def _compare_mappings_by_size( self, mol1,mol2, n1A,n2A, n1B,n2B ):
        if len(n1A)>len(n1B):
            return(n1A,n2A)
        elif len(n1A)<len(n1B):
            return(n1B,n2B)
        elif len(n1A)==len(n1B):
            counter1 = self._count_ringNonRing( n1A,n2A, mol1,mol2 )
            counter2 = self._count_ringNonRing( n1B,n2B, mol1,mol2 )
            if counter1>counter2:
                return(n1B,n2B)
            else:
                return(n1A,n2A)

    def _count_ringNonRing( self, n1,n2, mol1,mol2 ):
        counter = 0
        for i,j in zip(n1,n2):
            a1 = mol1.GetAtomWithIdx(i)
            a2 = mol2.GetAtomWithIdx(j)
            if a1.IsInRing()==True and a2.IsInRing()==False:
                counter+=1
            elif a1.IsInRing()==False and a2.IsInRing()==True:
                counter+=1
        return(counter)

 
#*************************************************#
# MCS #
#*************************************************#
    def mcs(self):
        """MCS search routine.

        Params
        ------

        Returns
        -------
        n1
            index
        """

        # make all atoms into carbon
        foo = cp.deepcopy(self.molForMcs1)
        bar = cp.deepcopy(self.molForMcs2)
        hnum1 = 52
        hnum2 = 53
        if self.bExactElementMatch==True:
            self.bElements = True
            self._carbonizeHonly( foo, hnum1 )
            self._carbonizeHonly( bar, hnum2 )
        elif( self.bRingsOnly==True ):
            self.bElements = True
            self._carbonize(foo,hnum1,ringAtomMask=42)
            self._carbonize(bar,hnum2,ringAtomMask=43)
        elif( self.bCarbonize==True ):
            self._carbonize(foo,hnum1)
            self._carbonize(bar,hnum2)
        elif( self.bH2Heavy==False ): # only change hydrogen numbering
            self._carbonizeHonly( foo, hnum1 )
            self._carbonizeHonly( bar, hnum2 )


        mols = [foo,bar]
        Chem.GetSymmSSSR(foo)
        Chem.GetSymmSSSR(bar)
        doLog(self.logfile,"MCS searching...",commandName=self.commandName)
#    res = MCS.FindMCS(mols,ringMatchesRingOnly=True, completeRingsOnly=True, atomCompare='elements', bondCompare='any', timeout=int(t), maximize='bonds')
    # for new RDKit-2018 use below
        if self.bElements==True:
            res = rdFMCS.FindMCS(mols,ringMatchesRingOnly=True, completeRingsOnly=True, timeout=self.t, maximizeBonds=True, bondCompare=rdFMCS.BondCompare.CompareAny, atomCompare=rdFMCS.AtomCompare.CompareElements)
        else:
            res = rdFMCS.FindMCS(mols,ringMatchesRingOnly=True, completeRingsOnly=True, timeout=self.t, maximizeBonds=True, bondCompare=rdFMCS.BondCompare.CompareAny, atomCompare=rdFMCS.AtomCompare.CompareAny)
        p = Chem.FragmentMatcher
        pp = p.FragmentMatcher()
        n1_list = []
        n2_list = []
        try:
            pp.Init(res.smartsString)
        except:
            return(n1_list,n2_list)
        n1_list = pp.GetMatches(foo,uniquify=0)
        n2_list = pp.GetMatches(bar,uniquify=0)
        doLog(self.logfile,'Found %d MCSs in total (mol1: %d, mol2: %d), each with %d atoms and %d bonds' % (len(n1_list)*len(n2_list),len(n1_list),len(n2_list),res.numAtoms,res.numBonds),commandName=self.commandName)

        #### NEW ###
        # there are no hydrogens mapped at this point (unless bH2Heavy was True), map them if needed in one of the later stages
#        if (self.bH2Heavy==False and self.bH2H==True):
#            n1_list,n2_list = self._mcsHmap( n1_list, n2_list )

        # from this point n1_list and n2_list elements must match 1to1, i.e. the number of elements in the lists is the same

        # triple bond rule: for simulation stability do not allow morphing an atom that is involved in a triple bond
        # into an atom that is involved in a non-triple bond (and vice versa)
        # also checking possible issues with the 1-2, 1-3 and 1-4 interactions
        # at this point investigate all mcs matches, i.e. two for loops: for n1... for n2...
        n1_foo = []
        n2_foo = []
#        for n1,n2 in list(zip(n1_list,n2_list)):
        for n1 in n1_list:
            n1new = cp.deepcopy(n1)
            for n2 in n2_list:
                n2new = cp.deepcopy(n2)
                n1new,n2new = self._tripleBond(self.mol1,self.mol2,n1new,n2new)
                n1new,n2new = self._checkTop(n1new,n2new)
                n1new,n2new = self._disconnectedRecursive(self.mol1,self.mol2,n1new,n2new)
                if len(n1new)>0 and len(n2new)>0:
                    n1_foo.append(n1new)
                    n2_foo.append(n2new)
        n1_list = cp.copy(n1_foo)
        n2_list = cp.copy(n2_foo)

# test
#    for n1,n2 in zip(n1_list,n2_list):
#        print "foo"
#	for i1,i2 in zip(n1,n2):
#	    print i1+1,i2+1
#	print "\n";
#    sys.exit(0)
         #############################################
         ######### chirality check ###################
        if( self.bChiral==True ):
            doLog(self.logfile,"Chirality check.",commandName=self.commandName)
            n1_foo = []
            n2_foo = []
            for n1,n2 in list(zip(n1_list,n2_list)):
                while(self._bCheckChiralViolation(self.mol1,self.mol2,n1,n2)==True):
                    n1,n2 = self._checkChiral(self.mol1,self.mol2,n1,n2)
                    n1,n2 = self._disconnectedRecursive(self.mol1,self.mol2,n1,n2)
                if len(n1)>0 and len(n2)>0:
                    n1_foo.append(n1)
                    n2_foo.append(n2)
            n1_list = cp.copy(n1_foo)
            n2_list = cp.copy(n2_foo)

# checking possible issues with the 1-2, 1-3 and 1-4 interactions
# this is done before the ringCheck and repeated after
        n1_foo = []
        n2_foo = []
        for n1,n2 in list(zip(n1_list,n2_list)):
            n1,n2 = self._checkTop(n1,n2) 
            n1_foo.append(n1)
            n2_foo.append(n2)
        n1_list = cp.copy(n1_foo)
        n2_list = cp.copy(n2_foo)

#############################################
######### do not break rings ################
        if( self.bBreakRings==False ):
            doLog(self.logfile,"Avoiding breaking rings.",commandName=self.commandName)
            n1_foo = []
            n2_foo = []
            for n1,n2 in list(zip(n1_list,n2_list)):
                n1,n2 = self._matchFullRings(self.mol1,self.mol2,n1,n2)
                n1,n2 = self._disconnectedRecursive(self.mol1,self.mol2,n1,n2)
                if len(n1)>0 and len(n2)>0:
                    n1_foo.append(n1)
                    n2_foo.append(n2)
            n1_list = cp.copy(n1_foo)
            n2_list = cp.copy(n2_foo)
# test
#    for n1,n2 in zip(n1_list,n2_list):
#        print "foo"
#	for i1,i2 in zip(n1,n2):
#	    print i1+1,i2+1
#	print "\n";
#    sys.exit(0)


        # if distances to be compared
        if(self.bdMCS==True):
            n1_list,n2_list = self._mcsDist(self.mol1,self.mol2,n1_list,n2_list)
            # due to meeting distance criterium
            # the rings may be broken
            # and disconnected fragments may appear
            if( self.bBreakRings==False ):
                doLog(self.logfile,"Avoiding breaking rings after meeting distance criterium.",commandName=self.commandName)
                n1_foo = []
                n2_foo = []
                for n1,n2 in list(zip(n1_list,n2_list)):
                    n1,n2 = self._matchFullRings(self.mol1,self.mol2,n1,n2)
                    n1,n2 = self._disconnectedRecursive(self.mol1,self.mol2,n1,n2)
                    if len(n1)>0 and len(n2)>0:
                        n1_foo.append(n1)
                        n2_foo.append(n2)
                n1_list = cp.copy(n1_foo)
                n2_list = cp.copy(n2_foo)

        # if there are several MCSs, select the one yielding the smallest RMSD
        n1,n2 = self._selectOneMCS(n1_list,n2_list,self.mol1,self.mol2)

        # map hydrogens now
        if (self.bH2Heavy==False and self.bH2H==True):
            n1,n2 = self._mapH( n1, n2 )

        # one more final check for possible issues with the 1-2, 1-3 and 1-4 interactions
        n1,n2 = self._checkTop(n1,n2) 

        # remove sigma hole virtual particles
        n1,n2 = self._removeSigmaHoles( n1,n2,self.sigmaHoleID1,self.sigmaHoleID2)

        doLog(self.logfile,'Final MCS that survived after pruning: %d atoms' % (len(n1)),commandName=self.commandName)

        return(n1,n2)

    def _carbonize(self, mol, hnum, ringAtomMask=None):
        bRingsOnly = None
        if self.bH2Heavy==True:
            hnum = 6

        for atom in mol.GetAtoms():
            if(atom.GetAtomicNum() != 1):
                atom.SetAtomicNum(6)
                if atom.IsInRing()==True:
                    atom.SetAtomicNum(6) # here I could set a specific atom number for rings to avoid ring-match-nonring, but it is not necessary, because later checks would catch it?
            else:
                atom.SetAtomicNum( hnum )

            if( (ringAtomMask!=None) and (atom.IsInRing()==False) ):
                atom.SetAtomicNum(ringAtomMask)

    def _carbonizeHonly(self, mol, hnum ):
        for atom in mol.GetAtoms():
            if(atom.GetAtomicNum() == 1):
                atom.SetAtomicNum(hnum)

    def _mcsHmap( self, n1_list, n2_list ):
        n1 = []
        n2 = []
        for nfoo in n1_list:
            for nbar in n2_list:
                foo,bar = self._mapH(nfoo,nbar)
                n1.append(foo)
                n2.append(bar)
        return(n1,n2)

    def _countHydrogens( self, nb ):
        counter = 0
        for n in nb:
            if n.GetAtomicNum()==1:
                counter+=1
        return(counter)

    def _mapH( self, nfoo, nbar ):
        newn1 = []
        newn2 = []
        c1 = self.mol1.GetConformer()
        c2 = self.mol2.GetConformer()

        for n1,n2 in list(zip(nfoo,nbar)):
            newn1.append(n1)
            newn2.append(n2)

            a1 = self.mol1.GetAtomWithIdx(n1)
            a2 = self.mol2.GetAtomWithIdx(n2)
            id1 = a1.GetAtomicNum()
            id2 = a2.GetAtomicNum()
            nb1 = a1.GetNeighbors()
            nb2 = a2.GetNeighbors()

            # only allow mapping hydrogens in those cases, 
            # when n1 and n2 have an equal number of them bound
            nH1 = self._countHydrogens(nb1)
            nH2 = self._countHydrogens(nb2)
            if nH1!=nH2:
                continue
            # also skip the rest of the loop if there are no neighbour hydrogens
            if nH1==0 or nH2==0:
                continue

            # TODO: add a distance criterium for the optimal hydrogen mapping
            # DONE: needs some more testing
            for neigh1 in nb1:
                dMin = 999999.999
                possibleHydrogen1 = None
                possibleHydrogen2 = None
                for neigh2 in nb2:
                    if neigh1.GetAtomicNum()==1 and neigh2.GetAtomicNum()==1:
                        if neigh1.GetIdx() not in newn1 \
                           and neigh2.GetIdx() not in newn2:
                            bPolar1 = self._isPolarH( neigh1 )
                            bPolar2 = self._isPolarH( neigh2 )
                            if( bPolar1==True and bPolar2==True):
                                if( self.bH2Hpolar==False ):
                                    continue
                            # calculate distance
                            pos1 = c1.GetAtomPosition(neigh1.GetIdx())
                            pos2 = c2.GetAtomPosition(neigh2.GetIdx())
                            d = pos1.Distance(pos2) # in Angstroms, but it doesn't matter for identifying minimal distance
                            if d<dMin:
                                dMin = d
                                possibleHydrogen1 = neigh1.GetIdx()
                                possibleHydrogen2 = neigh2.GetIdx()

                if possibleHydrogen1!=None and possibleHydrogen2!=None:
                    newn1.append(possibleHydrogen1)
                    newn2.append(possibleHydrogen2)

        return(newn1,newn2)

    def _isPolarH( a ):
        neighb = a.GetNeighbors()
        for a2 in neighb:
            anum2 = a2.GetAtomicNum()
            if anum2!=6:
                return(True)
        return(False)

    def _isTriple(self,mol,a):
        bTriple = False
        neighb = a.GetNeighbors()
        # analyze C,N (S not considered, because somewhat exotic) atoms for triple bonds
        # C
        if( a.GetAtomicNum()==6 ):
            if( len(a.GetNeighbors())==2 ):
                for neighb in a.GetNeighbors():
                    if( neighb.GetAtomicNum()==7 ):
                        if( len(neighb.GetNeighbors())==1 ):
                            bTriple=True
                    if( neighb.GetAtomicNum()==6 ):
                        if( len(neighb.GetNeighbors())==2 ):
                            if( self._getBondLength(mol,a.GetIdx(),neighb.GetIdx())<1.25 ): # need to check bond length (in Angstroms)
                                bTriple=True
        # N
        elif( a.GetAtomicNum()==7 ):
            if( len(a.GetNeighbors())==1 ):
                bTriple=True

   #    Chem.MolToMolFile(mol1,"foomol.mol")
#    foo = Chem.MolFromMolFile("foomol.mol")
#    Chem.MolToMolFile(mol2,"barmol.mol")
#    bar = Chem.MolFromMolFile("barmol.mol")
#    os.remove("foomol.mol")
#    os.remove("barmol.mol")

        return(bTriple)


    def _tripleBond(self,mol1,mol2,nfoo,nbar):
        newn1 = []
        newn2 = []
        for n1,n2 in zip(nfoo,nbar):
            a1 = mol1.GetAtomWithIdx(n1)
            a2 = mol2.GetAtomWithIdx(n2)
            bTriple1 = False
            bTriple2 = False
            # identify if bTriple is True/False
            bTriple1 = self._isTriple(mol1,a1)
            bTriple2 = self._isTriple(mol2,a2)
            if(bTriple1==True and bTriple2==False):
                continue
            elif(bTriple2==True and bTriple1==False):
                continue
            newn1.append(n1)
            newn2.append(n2)
        return(newn1,newn2)

    def _checkTop(self, n1, n2 ):
    #    return(n1,n2)
        # 1) generate 1-2, 1-3 and 1-4 lists
        # 2) identify problematic mappings
        # 3) fix the problems: discard the atom with fewer mapped neighbours

        ####### 1-2 #########    
        # 1a) 1-2 lists
        dict12_mol1 = self._getList12(self.mol1,n1)
        dict12_mol2 = self._getList12(self.mol2,n2)
        # 2a) identify problems 1-2; and 
        # 3a) fix 1-2
        rem12_mol2_start,rem12_mol2_end = self._findProblemsExclusions(n1,n2,dict12_mol1,dict12_mol2,case='1-2') # output: indeces of mol2
        n2,n1 = self._fixProblemsExclusions(self.mol2,self.mol1,n2,n1,rem12_mol2_start,rem12_mol2_end)
        rem12_mol1_start,rem12_mol1_end = self._findProblemsExclusions(n2,n1,dict12_mol2,dict12_mol1,case='1-2') # output: indeces of mol1
        n1,n2 = self._fixProblemsExclusions(self.mol1,self.mol2,n1,n2,rem12_mol1_start,rem12_mol1_end)

        ####### 1-3 #########    
        # 1b) 1-3 lists
        dict13_mol1 = self._getList13(self.mol1,n1)
        dict13_mol2 = self._getList13(self.mol2,n2)
        # 2b) identify problems 1-3 and
        # 3b) fix 1-3
        rem13_mol2_start,rem13_mol2_end = self._findProblemsExclusions(n1,n2,dict13_mol1,dict13_mol2,case='1-3') # output: indeces of mol2
        n2,n1 = self._fixProblemsExclusions(self.mol2,self.mol1,n2,n1,rem13_mol2_start,rem13_mol2_end)
        rem13_mol1_start,rem13_mol1_end = self._findProblemsExclusions(n2,n1,dict13_mol2,dict13_mol1,case='1-3') # output: indeces of mol1
        n1,n2 = self._fixProblemsExclusions(self.mol1,self.mol2,n1,n2,rem13_mol1_start,rem13_mol1_end)

        ####### 1-4 #########    
        # 1b) 1-4 lists
        dict14_mol1 = self._getList14(self.mol1,n1)
        dict14_mol2 = self._getList14(self.mol2,n2)
        # 2b) identify problems 1-4 and 
        # 3b) fix 1-4
        rem14_mol2_start,rem14_mol2_end = self._findProblemsExclusions(n1,n2,dict14_mol1,dict14_mol2,case='1-4') # output: indeces of mol2
        n2,n1 = self._fixProblemsExclusions(self.mol2,self.mol1,n2,n1,rem14_mol2_start,rem14_mol2_end)
        rem14_mol1_start,rem14_mol1_end = self._findProblemsExclusions(n2,n1,dict14_mol2,dict14_mol1,case='1-4') # output: indeces of mol1
        n1,n2 = self._fixProblemsExclusions(self.mol1,self.mol2,n1,n2,rem14_mol1_start,rem14_mol1_end)

        # treat disconnected
 #       n1,n2 = disconnectedMCS(mol1,mol2,n1,n2,bH2H,bH2Heavy)
        n1,n2 = self._disconnectedRecursive(self.mol1,self.mol2,n1,n2)
        return(n1,n2)

    def _getList12(self, mol, n):
        dict12 = {}
        for a1 in mol.GetAtoms():
            iStart1 = a1.GetIdx()
            if iStart1 not in n:
                continue
            neighbours1 = a1.GetNeighbors()
            for a2 in neighbours1: # 1-2
                iEnd2 = a2.GetIdx()
                if iEnd2 not in n:
                    continue
                if iEnd2 == iStart1:
                    continue
                if iStart1 in dict12.keys():
                    if iEnd2 not in dict12[iStart1]:
                        dict12[iStart1].append(iEnd2)
                else:
                    dict12[iStart1] = [iEnd2]
        return(dict12)

    def _getList13(self, mol,n):
        dict13 = {}
        for a1 in mol.GetAtoms():
            iStart1 = a1.GetIdx()
            if iStart1 not in n:
                continue
            neighbours1 = a1.GetNeighbors()
            for a2 in neighbours1: # 1-2
                i2 = a2.GetIdx()
#                if i2 not in n:
    #                continue
                if i2 == iStart1:
                    continue
                neighbours2 = a2.GetNeighbors()
                for a3 in neighbours2: # 1-3
                    iEnd3 = a3.GetIdx()
                    if iEnd3 not in n:
                        continue
                    if (iEnd3==iStart1) or (iEnd3==i2):
                        continue
                    if iStart1 in dict13.keys():
                        if iEnd3 not in dict13[iStart1]:
                            dict13[iStart1].append(iEnd3)
                    else:
                        dict13[iStart1] = [iEnd3]
        return(dict13)

    def _getList14(self, mol,n):
        dict14 = {}
        for a1 in mol.GetAtoms():
            iStart1 = a1.GetIdx()
            if iStart1 not in n:
                continue
            neighbours1 = a1.GetNeighbors()
            for a2 in neighbours1: # 1-2
                i2 = a2.GetIdx()
    #            if i2 not in n:
    #                continue
                if i2 == iStart1:
                    continue
                neighbours2 = a2.GetNeighbors()
                for a3 in neighbours2: # 1-3
                    i3 = a3.GetIdx()
    #               if i3 not in n:
    #                   continue
                    if (i3==iStart1) or (i3==i2):
                        continue
                    neighbours3 = a3.GetNeighbors()
                    for a4 in neighbours3: # 1-4
                        iEnd4 = a4.GetIdx()
                        if iEnd4 not in n:
                            continue
                        if (iEnd4==iStart1) or (iEnd4==i2) or (iEnd4==i3):
                            continue
                        if iStart1 in dict14.keys():
                            if iEnd4 not in dict14[iStart1]:
                                dict14[iStart1].append(iEnd4)
                        else:
                            dict14[iStart1] = [iEnd4]
        return(dict14)

    def _findProblemsExclusions(self,n1,n2,dict_mol1,dict_mol2,case='1-2,1-3,1-4'):
        rem_start = []
        rem_end = []
        # consider start and end atoms in mol1, e.g. 1-4 interaction
        for iStart in dict_mol1.keys():
            for iEnd in dict_mol1[iStart]:
                # find their matchings in mol2
                jStart,jEnd = self._getAttr(n1,n2,iStart,iEnd)
                if( (jStart==None) or (jEnd==None) ): # mapped to a dummy, thus no worries
                    continue
                # start atom is found in the matchings of mol2
                if jStart in dict_mol2.keys():
                    # if end atom in the matchings of mol2 is not present, the matching needs removing
                    if jEnd not in dict_mol2[jStart]:
                        # maybe entry already exists
                        if ((jStart in rem_start) or (jStart in rem_end)) and ((jEnd in rem_start) or (jEnd in rem_end)):
                            continue
                        rem_start.append(jStart)
                        rem_end.append(jEnd)
                elif jEnd not in dict_mol2.keys():
                    # an unusual situation, but can happen, it is safer to remove this mapping
#                    doLog(self.logfile,"Warning: something wrong in the {0} lists. Trying to proceed with the warning...".format(case),commandName=self.commandName)
                    rem_start.append(jStart)
                    rem_end.append(jEnd)
        return(rem_start,rem_end)

    def _fixProblemsExclusions(self,mol1,mol2,n1,n2,startList,endList):
        rem1 = []
        rem2 = []
        for iStart,iEnd in list(zip(startList,endList)):
            jStart,jEnd = self._getAttr(n1,n2,iStart,iEnd)
            # count iStart mapped neighbours
            startNeighb = 0
            for b1 in mol1.GetBonds():
                foo = b1.GetBeginAtomIdx()
                bar = b1.GetEndAtomIdx()
                if( iStart==foo ): # atom of interest
                    a,b = self._getAttr(n1,n2,iStart,bar)
                    if( (a!=None) and (b!=None) ):
                        startNeighb = startNeighb+1
                elif( iStart==bar ): # atom of interest
                    a,b = self._getAttr(n1,n2,iStart,foo)
                    if( (a!=None) and (b!=None) ):
                        startNeighb = startNeighb+1
            # count iEnd mapped neighbour
            endNeighb = 0
            for b1 in mol1.GetBonds():
                foo = b1.GetBeginAtomIdx()
                bar = b1.GetEndAtomIdx()
                if( iEnd==foo ): # atom of interest
                    a,b = self._getAttr(n1,n2,iEnd,bar)
                    if( (a!=None) and (b!=None) ):
                        endNeighb = endNeighb+1
                elif( iEnd==bar ): # atom of interest 
                    a,b = self._getAttr(n1,n2,iEnd,foo)
                    if( (a!=None) and (b!=None) ):
                        endNeighb = endNeighb+1
            # add to remove list
            if( startNeighb < endNeighb ):
                rem1.append(iStart)
                rem2.append(jStart)
            else:
                rem1.append(iEnd)
                rem2.append(jEnd)
        # remove
        n1_out = []
        n2_out = []
        for i1,i2 in list(zip(n1,n2)):
            if( (i1 in rem1) or (i2 in rem2) ):
                continue
            n1_out.append(i1)
            n2_out.append(i2)
        return(n1_out,n2_out)

    def _getAttr(self,n1,n2,iStart,iEnd):
        jStart = None
        jEnd = None
        for foo,bar in list(zip(n1,n2)):
            if(foo==iStart):
                jStart = bar
            if(foo==iEnd):
                jEnd = bar
        return(jStart,jEnd)

    def _disconnectedRecursive(self,mol1,mol2,ind1,ind2):
        ###################################
        ### find disconnected fragments ###
        # molecule 1
        n1_newlist = []
        n1_fragments = []
        for ind in ind1:
            if ind in n1_newlist:
                continue
            fragment = []
            atom = mol1.GetAtomWithIdx(ind)
            self._recursiveFragmentSearch(mol1,atom,fragment,ind1)
            n1_newlist.extend(fragment)
            n1_fragments.append(fragment)
        # molecule 2
        n2_newlist = []
        n2_fragments = []
        for ind in ind2:
            if ind in n2_newlist:
                continue
            fragment = []
            atom = mol2.GetAtomWithIdx(ind)
            self._recursiveFragmentSearch(mol2,atom,fragment,ind2)
            n2_newlist.extend(fragment)
            n2_fragments.append(fragment)
###########################################
#### identify all the fragment matches ####
# for testing purposes
#    n2_fragments = [[0, 25, 24, 23, 22, 21, 20, 19, 14], [13, 12, 3, 2, 1, 27, 28, 5, 6, 7, 9, 11, 10, 8, 33, 35, 36, 34, 32, 30, 31, 29, 4, 18, 17, 16, 38, 39, 40, 41, 42, 43, 37, 15, 44, 45, 46, 26, 50, 47, 48, 49], [51]]
        matchDict1 = {}
        matchDict2 = {}
        for id1,id2 in list(zip(ind1,ind2)):
            # find id1 
            key1 = ''
            for i in range(0,len(n1_fragments)):
                if id1 in n1_fragments[i]:
                    key1 =  str(i)
                    break
            # find id2
            key2 = ''
            for i in range(0,len(n2_fragments)):
                if id2 in n2_fragments[i]:
                    key2 =  str(i)
                    break
            key = key1+'_'+key2
            if key in matchDict1.keys():
                matchDict1[key].append(id1)
                matchDict2[key].append(id2)
            else:
                matchDict1[key] = [id1]
                matchDict2[key] = [id2]
        ##################################
        #### find the largest matches ####
        maxMatchSize = -1
        minMatchRMSD = 99999.999
        maxMatchKey = ''
        for key in matchDict1:
            if len(matchDict1[key]) > maxMatchSize:
                maxMatchSize = len(matchDict1[key])
                maxMatchKey = key
                minMatchRMSD = Chem.rdMolAlign.AlignMol(mol2,mol1,atomMap=list(zip(matchDict2[key],matchDict1[key])))
            elif len(matchDict1[key]) == maxMatchSize:
                rmsd = Chem.rdMolAlign.AlignMol(mol2,mol1,atomMap=list(zip(matchDict2[key],matchDict1[key])))
                if rmsd < minMatchRMSD:
                    minMatchRMSD = rmsd
                    maxMatchKey = key
    #########################
    ######## output #########
        if maxMatchKey == '':
            return([],[])
        return(matchDict1[maxMatchKey],matchDict2[maxMatchKey])

    def _recursiveFragmentSearch(self,mol,atom,fragment,n_list):
        ind = atom.GetIdx()
        if (ind not in fragment) and (ind in n_list):
            fragment.append(ind)
            a = mol.GetAtomWithIdx(ind)
            nb = a.GetNeighbors()
            for neigh in nb:
                self._recursiveFragmentSearch(mol,neigh,fragment,n_list)

    # this module checks for a chirality violation and returns True if smth wrong is found
    def _bCheckChiralViolation(self,mol1,mol2,n1,n2):
        bViolation = False
        # if only one atom is in the mapping
        if len(n1)<2 and len(n2)<2:
            return(bViolation)
        # create constraint for alignment
        constrMap = list(zip(n1,n2))
        # align on the subset n1,n2
        Chem.rdMolAlign.AlignMol(mol2,mol1,atomMap=list(zip(n2,n1)))
        rem1 = []
        rem2 = []
        bonds1 = mol1.GetBonds()
        bonds2 = mol2.GetBonds()
        # create two dictionaries for mappings [n1] = n2
        dictn1n2 = self._mappingDict( n1,n2 )
        dictn2n1 = self._mappingDict( n2,n1 )

        for i1,i2 in list(zip(n1,n2)):
            a1 = mol1.GetAtomWithIdx(i1)
            a2 = mol2.GetAtomWithIdx(i2)
            # checking chirality
            chirality1 = a1.GetChiralTag()
            chirality2 = a2.GetChiralTag()
            # also checking if some bonds are not single
            bonds1 = a1.GetBonds()
            bonds2 = a2.GetBonds()
            bNonsingleBond1 = self._check_nonsingle_bonds( mol1, mol2, bonds1, n1, n2, dictn1n2 )
            bNonsingleBond2 = self._check_nonsingle_bonds( mol2, mol1, bonds2, n2, n1, dictn2n1 )
#            print bNonsingleBond1
#            bonds_a1 = find_atom_bonds( i1, a1, bonds1 ) # find all bonds in which a1 takes part
#            sys.exit(0)
            if (str(chirality1) != "CHI_UNSPECIFIED") or (bNonsingleBond1==True):
#               print "1check",i1,chirality1
                localEnv1 = self._localEnvironment(mol1,i1,n1)
                if len(localEnv1)>2:
                    localEnv2 = self._getMappedList(n1,n2,localEnv1)
                    Chem.rdMolAlign.AlignMol(mol2,mol1,atomMap=list(zip(localEnv2,localEnv1)))
                    #alignOnSubset(mol1,mol2,zip(localEnv1,localEnv2))
                nb1 = a1.GetNeighbors()
                self._checkNeighDist(mol1,mol2,nb1,n1,n2,rem1,rem2)
            if (str(chirality2) != "CHI_UNSPECIFIED") or (bNonsingleBond2==True):
#               print "2check",i2,chirality2
                localEnv2 = self._localEnvironment(mol2,i2,n2)
                if len(localEnv2)>2:
                    localEnv1 = self._getMappedList(n2,n1,localEnv2)
                    Chem.rdMolAlign.AlignMol(mol1,mol2,atomMap=list(zip(localEnv1,localEnv2)))
                    #alignOnSubset(mol1,mol2,zip(localEnv1,localEnv2))
                nb2 = a2.GetNeighbors()
                self._checkNeighDist(mol2,mol1,nb2,n2,n1,rem2,rem1)
            if len(rem1)+len(rem2)>0:
                return(True)
        return(bViolation)

    def _mappingDict( self, n1, n2 ):
        out = {}
        for i1,i2 in list(zip(n1,n2)):
            out[i1] = i2
        return(out)

    def _check_nonsingle_bonds( self, mol1, mol2, bonds1, n1, n2, dictn1n2 ):
        for bond in bonds1:
            foo = bond.GetBeginAtomIdx()
            bar = bond.GetEndAtomIdx()
            if (foo in n1) and (bar in n1): # atoms of the bond considered are both in n1, therefore they are also in n2
                a1 = mol1.GetAtomWithIdx( foo )
                a2 = mol1.GetAtomWithIdx( bar )
                bondLength1 = self._getBondLength(mol1,foo,bar)
                if self._is_nonsingle( bondLength1, a1.GetAtomicNum(), a2.GetAtomicNum() )==True:
                    return(True)
        return(False)

    def _getBondLength(self, mol,id1,id2):
        conf = mol.GetConformer()
        pos1 = conf.GetAtomPosition(id1)
        pos2 = conf.GetAtomPosition(id2)
        return(pos1.Distance(pos2))

# bond length table
# http://www.chem.tamu.edu/rgroup/connell/linkfiles/bonds.pdf
# http://www.wiredchemist.com/chemistry/data/bond_energies_lengths.html
    def _is_nonsingle( self, d, anum1, anum2 ):
        if anum1==6 and anum2==6: # C C
            if d < 1.45:
                return(True)
        elif (anum1==6 and anum2==7) or (anum2==6 and anum1==7): # C N
            if d < 1.4:
                return(True)
        elif (anum1==6 and anum2==8) or (anum2==6 and anum1==8): # C O
            if d < 1.35:
                return(True)
        elif (anum1==6 and anum2==16) or (anum2==6 and anum1==16): # C S
            if d < 1.75:
                return(True)
        elif anum1==7 and anum2==7: # N N
            if d < 1.35:
                return(True)
        elif (anum1==7 and anum2==8) or (anum2==7 and anum1==8): # N O
            if d < 1.3:
                return(True)
        elif anum1==8 and anum2==8: # O O
            if d < 1.3:
                return(True)
        elif (anum1==8 and anum2==15) or (anum2==8 and anum1==15): # O P
            if d < 1.575:
                return(True)
        elif anum1==16 and anum2==16: # S S
            if d < 1.75:
                return(True)
        return(False)

    def _localEnvironment(self,mol,i,n):
        a1 = mol.GetAtomWithIdx(i)
        nb1 = a1.GetNeighbors()
        # 1-2 and 1-3
        list12 = []
        list13 = []
        for a2 in nb1:
            ind1 = a2.GetIdx()
            if ind1 in n:
                list12.append(ind1)
            nb2 = a2.GetNeighbors()
            for a3 in nb2:
                ind2 = a3.GetIdx()
                if (ind2 in n) and (ind2 not in list12) and (ind2 not in list13) and (ind2 != i):
                    list13.append(ind2)
        # final list
        listFinal = [i] + list12 + list13
        return(listFinal)

    def _getMapped(self, n1,n2,ind1):
        ind2 = None
        for id1,id2 in list(zip(n1,n2)):
            if id1==ind1:
                ind2=id2
                return(ind2)
        return(ind2)

    def _getMappedList(self,n1,n2,indList1):
        indList2 = []
        for ind1 in indList1:
            for id1,id2 in list(zip(n1,n2)):
                if id1==ind1:
                    indList2.append(id2)
                    break
        return(indList2)

    def _checkNeighDist(self,mol1,mol2,nb1,n1,n2,rem1,rem2):
        c1 = mol1.GetConformer()
        c2 = mol2.GetConformer()
        for neigh1 in nb1:
            ind1 = neigh1.GetIdx()
            if ind1 in n1:
                ind2 = self._getMapped(n1,n2,ind1)
                if ind2!=None:
                    dist = self._distanceBased(mol1,mol2,0.0,id1=[ind1],id2=[ind2],calcOnly=True)
#                   print ind1,ind2,dist
                    if dist > 0.15: # 0.15 nm distance should be forgiving enough, but also catch the cases of chirality inversions
                        if (ind1 not in rem1) and (ind2 not in rem2):
                            rem1.append(ind1)
                            rem2.append(ind2)

    def _distanceBased(self, mol1, mol2, d, id1=None, id2=None, calcOnly=False):
        pairs1 = []
        pairs2 = []

        # to choose one MCS out of many
        if(calcOnly==True):
            dist = 0.0
            c1 = mol1.GetConformer()
            c2 = mol2.GetConformer()
            for ind1,ind2 in list(zip(id1,id2)):
                pos1 = c1.GetAtomPosition(ind1)
                pos2 = c2.GetAtomPosition(ind2)
                dist = dist + 0.1*pos1.Distance(pos2) # Angstroms in pdb files
            return(dist)

        # o3a
        if(id1==None or id2==None):
            c1 = mol1.GetConformer()
            c2 = mol2.GetConformer()
            for a1 in mol1.GetAtoms():
                pos1 = c1.GetAtomPosition(a1.GetIdx())
                dd = d*10.0 # Angstroms in pdb files
                keep1 = None
                keep2 = None
                for a2 in mol2.GetAtoms():
                    pos2 = c2.GetAtomPosition(a2.GetIdx())
                    dist = pos1.Distance(pos2)
                    if(dist < dd):
                        dd = dist
                        keep1 = a1.GetIdx()
                        keep2 = a2.GetIdx()
                if( (keep1 is not None) and (keep2 is not None) ):
                    pairs1.append(keep1)
                    pairs2.append(keep2)
            return(pairs1,pairs2)

        # mcs
        for ind1 in id1:
            c1 = mol1.GetConformer()
            pos1 = c1.GetAtomPosition(ind1)
            dd = d*10.0 # Angstroms in pdb files
            keep1 = None
            keep2 = None
            for ind2 in id2:
                c2 = mol2.GetConformer()
                pos2 = c2.GetAtomPosition(ind2)
                dist = pos1.Distance(pos2)
                if(dist < dd):
                    dd = dist
                    keep1 = ind1
                    keep2 = ind2
            if( (keep1 is not None) and (keep2 is not None) ):
                pairs1.append(keep1)
                pairs2.append(keep2)
        return(pairs1,pairs2)

    def _checkChiral(self,mol1,mol2,n1,n2):
        # create constraint for alignment
        constrMap = list(zip(n1,n2))
        # align on the subset n1,n2
        Chem.rdMolAlign.AlignMol(mol2,mol1,atomMap=list(zip(n2,n1)))
        bonds1 = mol1.GetBonds()
        bonds2 = mol2.GetBonds()
        # create two dictionaries for mappings [n1] = n2
        dictn1n2 = self._mappingDict( n1,n2 )
        dictn2n1 = self._mappingDict( n2,n1 )

        rem1 = []
        rem2 = []
        for i1,i2 in list(zip(n1,n2)):
            a1 = mol1.GetAtomWithIdx(i1)
            a2 = mol2.GetAtomWithIdx(i2)
            chirality1 = a1.GetChiralTag()
            chirality2 = a2.GetChiralTag()
            # also checking if some bonds are not single
            bonds1 = a1.GetBonds()
            bonds2 = a2.GetBonds()
            bNonsingleBond1 = self._check_nonsingle_bonds( mol1, mol2, bonds1, n1, n2, dictn1n2 )
            bNonsingleBond2 = self._check_nonsingle_bonds( mol2, mol1, bonds2, n2, n1, dictn2n1 )
            if (str(chirality1) != "CHI_UNSPECIFIED") or (bNonsingleBond1==True):
#               print "1",i1,chirality1
                # try fitting locally on the 1-2, 1-3 atoms
                localEnv1 = self._localEnvironment(mol1,i1,n1)
                if len(localEnv1)>2:
                    localEnv2 = self._getMappedList(n1,n2,localEnv1)
                    Chem.rdMolAlign.AlignMol(mol2,mol1,atomMap=list(zip(localEnv2,localEnv1)))
#                   alignOnSubset(mol1,mol2,zip(localEnv1,localEnv2))
                # get the neighbours
                nb1 = a1.GetNeighbors()
                # check if the matched neighbours in the aligned molecules are too far
                self._checkNeighDist(mol1,mol2,nb1,n1,n2,rem1,rem2)
            if (str(chirality2) != "CHI_UNSPECIFIED") or (bNonsingleBond2==True):
#               print "2",i2,chirality2
                # try fitting locally on the 1-2, 1-3 atoms
                localEnv2 = self._localEnvironment(mol2,i2,n2)
                if len(localEnv2)>2:
                    localEnv1 = self._getMappedList(n2,n1,localEnv2)
                    Chem.rdMolAlign.AlignMol(mol1,mol2,atomMap=list(zip(localEnv1,localEnv2)))
#                    alignOnSubset(mol1,mol2,zip(localEnv1,localEnv2))
                # get the neighbours
                nb2 = a2.GetNeighbors()
                # check if the matched neighbours in the aligned molecules are too far
                self._checkNeighDist(mol2,mol1,nb2,n2,n1,rem2,rem1)

        ####### remove #######
        n1_out = []
        n2_out = []
        for i1,i2 in list(zip(n1,n2)):
            if( (i1 in rem1) or (i2 in rem2) ):
                continue
            n1_out.append(i1)
            n2_out.append(i2)
        return(n1_out,n2_out)

    def _matchFullRings(self,mol1,mol2,n1,n2):
        n1_init = cp.deepcopy(n1)
        n2_init = cp.deepcopy(n2)

        r1 = mol1.GetRingInfo()
        r2 = mol2.GetRingInfo()
        if isinstance(r1,tuple)==False:
            r1 = r1.AtomRings()
        if isinstance(r2,tuple)==False:
            r2 = r2.AtomRings()
        rem1 = []
        rem2 = []
        # investigate the rings: soft-check
#        print "Soft-check for ring matching\n"
        for i,j in list(zip(n1,n2)):
            a1 = mol1.GetAtomWithIdx(i)
            a2 = mol2.GetAtomWithIdx(j)
            ring1 = a1.IsInRing()
            ring2 = a2.IsInRing()
            if( (ring1==True) and (ring2==False) ):
                rem1.append(i)
                rem2.append(j)
            elif( (ring1==False) and (ring2==True) ):
                rem1.append(i)
                rem2.append(j)
            elif( (ring1==True) and (ring2==True) ):
                mapped1 = False
                mapped2 = False
                # here only checking if, given one morphable atom in a ring,
                # is there at least one ring which would harbor this atom
                # and all the other atoms in that ring would also be morphable
                for ar1 in r1:#.AtomRings():
                    if( i in ar1 ):
                        mapped1 = self._isMapped(ar1,n1)
                    if( mapped1 == True):
                        break
                for ar2 in r2:#.AtomRings():
                    if( j in ar2 ):
                        mapped2 = self._isMapped(ar2,n2)
                    if( mapped2 == True):
                        break
                if( (mapped1==False) or (mapped2==False) ):
                    rem1.append(i)
                    rem2.append(j)
        # before removing, check for a special case:
        # a single non-ring atom could be allowed to map to a single ring atom
        # if the rest of the ring atoms are not mapped
        dontRem1 = []
        dontRem2 = []

        # remove
        n1_out = []
        n2_out = []
        for i1,i2 in list(zip(n1,n2)):
            if( (i1 in rem1) or (i2 in rem2) ):
                if( (i1 not in dontRem1) and (i2 not in dontRem2) ):
                    continue
            n1_out.append(i1)
            n2_out.append(i2)

        # investigate the rings: strict check
        # after the soft-check only those morphable atoms have survived
        # which belong to rings in both structures
        # check if more than two atoms in a ring are morphed, while the rest are not
        bFound = True
        while( bFound==True ):
            n1 = cp.deepcopy(n1_out)
            n2 = cp.deepcopy(n2_out)
            bRem1 = False
            bRem2 = False
            minRemSize = 999
            minRem = []

            # go over the rings in the first molecule
            for ar1 in r1:#.AtomRings():
                bRem1,mappedInd = self._countMapped(ar1,n1)
#                print ar1,mappedInd,bRem1
                # if an inappropriate mapping is found
                if( bRem1 == True ):
                    # identify the other ring that participates in this mapping
                    # more precisely, find the smallest number of atoms to remove
                    for ar in r1:#.AtomRings():
                        if ar==ar1:
                            continue
                        bAddThisRing = False
                        for a in ar:
                            if a in mappedInd:
                                bAddThisRing = True
                                break
                        foo = []
                        if bAddThisRing==True:
                            for a in ar:
                                foo.append(a)
                        if len(foo)>0 and len(foo)<minRemSize:
                            minRemSize = len(foo)
                            minRem = cp.deepcopy(foo)
                    # identify the counterpart indices
                    minRemB = []
                    for i1,i2 in list(zip(n1,n2)):
                        if i1 in minRem:
                            minRemB.append(i2)
                    # remove the atoms
                    n1_out,n2_out = self._removeInd( n1,n2,minRem,minRemB )
#                    print n1,minRem
                    break
            if minRemSize>0 and minRemSize<999:
                continue
#            sys.exit(0)

            # go over the rings in the second molecule
            for ar2 in r2:#.AtomRings():
                bRem2,mappedInd = self._countMapped(ar2,n2)
                # if an inappropriate mapping is found
                if( bRem2 == True ):
                    # identify the other ring that participates in this mapping
                    # more precisely, find the smallest number of atoms to remove
                    for ar in r2:#.AtomRings():
                        if ar==ar2:
                            continue
                        bAddThisRing = False
                        for a in ar:
                            if a in mappedInd:
                                bAddThisRing = True
                                break
                        foo = []
                        if bAddThisRing==True:
                            for a in ar:
                                foo.append(a)
                        if len(foo)>0 and len(foo)<minRemSize:
                            minRemSize = len(foo)
                            minRem = cp.deepcopy(foo)
                    # identify the counterpart indices
                    minRemB = []
                    for i1,i2 in list(zip(n1,n2)):
                        if i2 in minRem:
                            minRemB.append(i1)
                    # remove the atoms
                    n1_out,n2_out = self._removeInd( n1,n2,minRemB,minRem )
                    break
            if minRemSize>0 and minRemSize<999:
                continue

            bFound = False

        ##########################################################
        ## also consider allowing mappings where only one ring atom  ##
        ## is mapped to only one atom in another ring ##
        ############################################################
        n1_return = [] # list of atoms to return to the mapping
        n2_return = [] # list of atoms to return to the mapping
        for i,j in list(zip(n1_init,n2_init)): # these *_init lists are prior to ring processing
             if (i not in n1_out) and (j not in n2_out): # a mapping was removed by ring processing
                 a1 = mol1.GetAtomWithIdx(i)
                 a2 = mol2.GetAtomWithIdx(j)
                 ring1 = a1.IsInRing()
                 ring2 = a2.IsInRing()
                 if( (ring1==True) and (ring2==True) ):
                     neighbors1 = a1.GetNeighbors()
                     neighbors2 = a2.GetNeighbors()
                     bOK1 = False
                     bOK2 = False
                     for nn1 in neighbors1:
                         nnind1 = nn1.GetIdx()
                         if nnind1 in n1_out:
                             bOK1 = True
                             break
                     for nn2 in neighbors2:
                         nnind2 = nn2.GetIdx()
                         if nnind2 in n2_out:
                             bOK2 = True
                             break
                     if bOK1==True and bOK2==True:
                         n1_return.append(i)
                         n2_return.append(j)
        # return what has been found
        for i,j in list(zip(n1_return,n2_return)):
            bOK1 = False
            bOK2 = False
            for ar1 in r1: # go over rings in the first molecule
                if i in ar1: # atom is in this ring
                    foo,mappedInd = self._countMapped(ar1,n1_out)
                    if len(mappedInd)==0: # no other atoms are mapped in this ring
                        bOK1 = True
            for ar2 in r2: # go over rings in the second molecule
                if j in ar2: # atom is in this ring
                    foo,mappedInd = self._countMapped(ar2,n2_out)
                    if len(mappedInd)==0: # no other atoms are mapped in this ring
                        bOK2 = True
            if bOK1==True and bOK2==True:
#                n1_init.remove(i)
#                n2_init.remove(j)
                n1_out.append(i)
                n2_out.append(j)

        ##########################################################
        ## at this point the rings have been properly processed ##
        ## allow for a single non-ring atom to match a ring atom ##
        ## to do this, go over the mappings and return such pairs ##
        ############################################################
        n1_return = [] # list of atoms to return to the mapping
        n2_return = [] # list of atoms to return to the mapping
        for i,j in list(zip(n1_init,n2_init)): # these *_init lists are prior to ring processing
             if (i not in n1_out) and (j not in n2_out): # a mapping was removed by ring processing
                 a1 = mol1.GetAtomWithIdx(i)
                 a2 = mol2.GetAtomWithIdx(j)
                 ring1 = a1.IsInRing()
                 ring2 = a2.IsInRing()
                 if( ( (ring1==True) and (ring2==False) ) or ( (ring1==False) and (ring2==True) ) ):
                     neighbors1 = a1.GetNeighbors() # it is sufficient to check for one of the molecules
                     for nn1 in neighbors1:
                         nnind1 = nn1.GetIdx()
                         if (nnind1 in n1_out) and (nn1.GetAtomicNum()>1): # not hydrogen
                             n1_return.append(i)
                             n2_return.append(j)
                             break
#        print "VG",n1_return
        # return what has been found
        for i,j in list(zip(n1_return,n2_return)):
            n1_out.append(i)
            n2_out.append(j)

        return(n1_out,n2_out)

    def _removeInd( self, n1,n2,rem1,rem2 ):
        n1_out = []
        n2_out = []
        for i1,i2 in zip(n1,n2):
            if( (i1 in rem1) or (i2 in rem2) ):
                continue
            n1_out.append(i1)
            n2_out.append(i2)
        return(n1_out,n2_out)

    def _isMapped(self,ring,ind):
        for a in ring:
            if( a in ind):
                continue
            else:
                return(False)
        return(True)

    def _countMapped(self,ring,ind):
        countMapped = 0
        countUnmapped = 0
        mapped = []
        for a in ring:
            if( a in ind ):
                countMapped += 1
                mapped.append(a)
            else:
                countUnmapped += 1
    #    print ring,countMapped,countUnmapped,ind
        if (countMapped > 2) and (countUnmapped > 1):
            return(True,mapped)
        return(False,mapped)

    def _mcsDist(self,mol1,mol2,n1_list,n2_list):
        n1 = []
        n2 = []
        # distances
        maxMCS = 0 # size of the largest MCS fulfilling the distances
        for nfoo,nbar in list(zip(n1_list,n2_list)):
            alignID = list(zip(nfoo,nbar))
            ##########################################
            ###### o3a alignment may work better #####
            rmsd = Chem.rdMolAlign.AlignMol(mol2,mol1,atomMap=list(zip(nbar,nfoo)))
    #       rmsd = alignOnSubset(mol1,mol2,alignID) # but it has some dependence on the molecule sequence, not sure if I trust it
    #        print "RMSD after alignment: %f Angstroms" %rmsd
            x,y = self._distance_based(mol1,mol2,nfoo,nbar)
            n1.append(x)
            n2.append(y)
            if(len(x)>maxMCS):
                maxMCS = len(x)

        nn1 = []
        nn2 = []
        # match rings and remove disconnected
        maxMCS = 0
        for nfoo,nbar in list(zip(n1,n2)):
            # rings
            x,y = self._matchRings(mol1,mol2,nfoo,nbar)
            # disconnected
            #x,y = disconnectedMCS(mol1,mol2,x,y,bH2H,bH2Heavy)
            x,y = self._disconnectedRecursive(mol1,mol2,x,y)
            nn1.append(x)
            nn2.append(y)
            if(len(x)>maxMCS):
                maxMCS = len(x)

        doLog(self.logfile,"maxMCS after distance treatment: %d" % maxMCS,commandName=self.commandName)

        n1 = []
        n2 = []
        # only keep the largest MCSs
        maxSize = self._getLargestList(nn1+nn2)
        for nfoo,nbar in list(zip(nn1,nn2)):
            if(len(nfoo)==maxSize and len(nbar)==maxSize):
                n1.append(nfoo)
                n2.append(nbar)
    #           print "foo",nfoo,nbar
        return(n1,n2)

    def _distance_based(self, mol1, mol2, id1=None, id2=None, calcOnly=False):
        pairs1 = []
        pairs2 = []

        # to choose one MCS out of many
        if(calcOnly==True):
            dist = 0.0
            c1 = mol1.GetConformer()
            c2 = mol2.GetConformer()
            for ind1,ind2 in list(zip(id1,id2)):
                pos1 = c1.GetAtomPosition(ind1)
                pos2 = c2.GetAtomPosition(ind2)
                dist = dist + 0.1*pos1.Distance(pos2) # Angstroms in pdb files
            return(dist)

        # o3a
        if(id1==None or id2==None):
            c1 = mol1.GetConformer()
            c2 = mol2.GetConformer()
            for a1 in mol1.GetAtoms():
                pos1 = c1.GetAtomPosition(a1.GetIdx())
                dd = self.d*10.0 # Angstroms in pdb files
                keep1 = None
                keep2 = None
                for a2 in mol2.GetAtoms():
                    pos2 = c2.GetAtomPosition(a2.GetIdx())
                    dist = pos1.Distance(pos2)
                    if(dist < dd):
                        dd = dist
                        keep1 = a1.GetIdx()
                        keep2 = a2.GetIdx()
                if( (keep1 is not None) and (keep2 is not None) ):
                    pairs1.append(keep1)
                    pairs2.append(keep2)
            return(pairs1,pairs2)
    
        # mcs
        for ind1 in id1:
            c1 = mol1.GetConformer()
            pos1 = c1.GetAtomPosition(ind1)
            dd = self.d*10.0 # Angstroms in pdb files
            keep1 = None
            keep2 = None
            for ind2 in id2:
                c2 = mol2.GetConformer()
                pos2 = c2.GetAtomPosition(ind2)
                dist = pos1.Distance(pos2)
                if(dist < dd):
                    dd = dist
                    keep1 = ind1
                    keep2 = ind2
            if( (keep1 is not None) and (keep2 is not None) ):
                pairs1.append(keep1)
                pairs2.append(keep2)
        return(pairs1,pairs2)

    # checking that a non-ring atom would not be morphed into a ring atom
    def _matchRings(self,mol1,mol2,nfoo,nbar):
        newn1 = []
        newn2 = []
        for n1,n2 in list(zip(nfoo,nbar)):
            a1 = mol1.GetAtomWithIdx(n1)
            a2 = mol2.GetAtomWithIdx(n2)
#            arom1 = a1.GetIsAromatic()
#            arom2 = a2.GetIsAromatic()
            ring1 = a1.IsInRing()
            ring2 = a2.IsInRing()
            if(ring1==True and ring2==False):
                continue
            if(ring1==False and ring2==True):
                continue
            newn1.append(n1)
            newn2.append(n2)

        # only one atom morphed in a ring will not work, check for that
        n1,n2 = self._oneAtomInRing(mol1,mol2,newn1,newn2)
        return(n1,n2)

    def _oneAtomInRing(self,mol1,mol2,n1,n2):
        newn1 = []
        newn2 = []
        for i,j in list(zip(n1,n2)):
            a1 = mol1.GetAtomWithIdx(i)
            a2 = mol2.GetAtomWithIdx(j)
            ring1 = a1.IsInRing()
            ring2 = a2.IsInRing()
            if( ring1==True and ring2==True ):
                bonds1 = a1.GetBonds()
                bonds2 = a2.GetBonds()
                found = 0
                for b1 in bonds1:
                    id1 = b1.GetEndAtomIdx()
                    at1 = b1.GetEndAtom()
                    if( b1.GetEndAtomIdx()==i ):
                        id1 = b1.GetBeginAtomIdx()
                        at1 = b1.GetBeginAtom()
                    for b2 in bonds2:
                        id2 = b2.GetEndAtomIdx()
                        at2 = b2.GetEndAtom()
                        if( b2.GetEndAtomIdx()==j ):
                            id2 = b2.GetBeginAtomIdx()
                            at2 = b2.GetBeginAtom()
                        if(at1.IsInRing()==True and at2.IsInRing()==True):
                            if( (id1 in n1) and (id2 in n2) ):
                                found = 1
                                break
                    if(found==1):
                        break
                if(found==1):
                    newn1.append(i)
                    newn2.append(j)
            else:
                newn1.append(i)
                newn2.append(j)
        return(newn1,newn2)

    def _getLargestList(self,lists):
        res = 0
        for l in lists:
            if(len(l) > res):
                res = len(l)
        return(res)

    def _selectOneMCS(self,n1_list,n2_list,mol1,mol2):
        if len(n1_list)==0 or len(n2_list)==0:
            return([],[])

        n1 = n1_list[0]
        n2 = n2_list[0]

        # select the largest MCSs
        largestSize = -42
        for nfoo,nbar in list(zip(n1_list,n2_list)):
            if len(nfoo)>largestSize:
                largestSize = len(nfoo)
        n1_largest = []
        n2_largest = []
        for nfoo,nbar in list(zip(n1_list,n2_list)):
            if len(nfoo)==largestSize:
                n1_largest.append(nfoo)
                n2_largest.append(nbar)

        # of the largest ones eliminate those that have a ring/non-ring mapping for a single atom
        n1_largestB = []
        n2_largestB = []
        for nfoo,nbar in list(zip(n1_largest,n2_largest)):
            bOK = True
            for i,j in list(zip(nfoo,nbar)):
                a1 = mol1.GetAtomWithIdx(i)
                a2 = mol2.GetAtomWithIdx(j)
                if a1.IsInRing()==True and a2.IsInRing()==False:
                    bOK = False
                    break
                elif a1.IsInRing()==False and a2.IsInRing()==True:
                    bOK = False
                    break
            if bOK==True:
                n1_largestB.append(nfoo)
                n2_largestB.append(nbar)
    
        # of the largest ones select the minRMSD
        rmsdMin = 9999.999
        for nfoo,nbar in list(zip(n1_largestB,n2_largestB)):
            # align
            alignID = list(zip(nbar,nfoo))
            try:
                rmsd = Chem.rdMolAlign.AlignMol(mol2,mol1,atomMap=alignID)
            except:
                rmsd = rmsdMin*10.0
    #            x,y = distance_based(mol1,mol2,d,nfoo,nbar,True)
                # compare
            if( rmsd < rmsdMin ):
                rmsdMin = rmsd
                n1 = nfoo
                n2 = nbar
        return(n1,n2)

    def _removeSigmaHoles( self, n1,n2,sigmaHoleID1,sigmaHoleID2):
        n1new = []
        n2new = []
        for (i,j) in list(zip(n1,n2)):
            if (i+1 not in sigmaHoleID1) and (j+1 not in sigmaHoleID2):
                n1new.append(i)
                n2new.append(j)
        return(n1new,n2new)

#*************************************************#
# O3A Alignment #
#*************************************************#
    def alignment(self):
        """Alignment based routine.

        Params
        ------

        Returns
        -------
        n1
            index
        """

        n1 = []
        n2 = []
        ####################################
        # prepare molecules and parameters #
        ####################################
        if( self.bRingsOnly==True ):
            submol1 = self._subMolRing(self.mol1)
            submol2 = self._subMolRing(self.mol2)
        ###################
        #### now align ####
        ###################
        if( self.bCrippen==True ): # always Crippen
            mol1_crippen = self._genCrippen(self.mol1)
            mol2_crippen = self._genCrippen(self.mol2)
            if( self.bRingsOnly==True ):
                submol1_crippen = self._genCrippen(submol1)
                submol2_crippen = self._genCrippen(submol2)
                pyO3A = AllChem.GetCrippenO3A(submol1,submol2,submol1_crippen,submol2_crippen,options=0)
                rmsd = pyO3A.Align()
                # now align the full molecule
                pyO3A = AllChem.GetCrippenO3A(self.mol1,submol1,mol1_crippen,submol1_crippen,options=0)
                pyO3A.Align()
            else:
                pyO3A = AllChem.GetCrippenO3A(self.mol1,self.mol2,mol1_crippen,mol2_crippen,options=0) # mol1=probe, mol2=ref
                pyO3A.Align()
            # distances
            n1,n2 = self._distance_based(self.mol1,self.mol2)
        # hydrogen rule
        n1,n2 = self._removeH(self.mol1,self.mol2,n1,n2)
        # remove sigma hole virtual particles
        n1,n2 = self._removeSigmaHoles( n1,n2,self.sigmaHoleID1,self.sigmaHoleID2)
        # remove polar hydrogen mappings
        # n1,n2 = removePolarHmappings(mol1,mol2,n1,n2,bH2Hpolar)
        # triple bond rule: for simulation stability do not allow morphing an atom that is involved in a triple bond
        # into an atom that is involved in a non-triple bond (and vice versa)
        n1,n2 = self._tripleBond(self.mol1,self.mol2,n1,n2)
        # rings
        n1,n2 = self._matchRings(self.mol1,self.mol2,n1,n2)
        # checking possible issues with the 1-2, 1-3 and 1-4 interactions
        # this is done before the ringCheck and repeated after
        n1,n2 = self._checkTop(n1,n2)
        # do not break rings
        if( self.bBreakRings==False ):
            doLog(self.logfile,"Avoiding breaking rings.",commandName=self.commandName)
            n1,n2 = self._matchFullRings(self.mol1,self.mol2,n1,n2)
        # treat disconnected
        # n1,n2 = disconnectedMCS(mol1,mol2,n1,n2,bH2H,bH2heavy)
        n1,n2 = self._disconnectedRecursive(self.mol1,self.mol2,n1,n2)
        # n1 and n2 are not sorted by pairs at this point
        n1,n2 = self._sortInd(self.mol1,self.mol2,n1,n2)
        # checking possible issues with the 1-2, 1-3 and 1-4 interactions
        n1,n2 = self._checkTop(n1,n2)

        return(n1,n2,pyO3A)

    def _sortInd(self,mol1,mol2,ind1,ind2):
        n1out = []
        n2out = []
        c1 = mol1.GetConformer()
        c2 = mol2.GetConformer()
        for id1 in ind1:
            pos1 = c1.GetAtomPosition(id1)
            minDist = 9999.999
            keep = -1
            for id2 in ind2:
                pos2 = c2.GetAtomPosition(id2)
                if(pos1.Distance(pos2)<minDist):
                    minDist = pos1.Distance(pos2)
                    keep = id2
            n1out.append(id1)
            n2out.append(keep)
        return(n1out,n2out)

    def _removeSigmaHoles( self,n1,n2,sigmaHoleID1,sigmaHoleID2):
        n1new = []
        n2new = []
        for (i,j) in list(zip(n1,n2)):
            if (i+1 not in sigmaHoleID1) and (j+1 not in sigmaHoleID2):
                n1new.append(i)
                n2new.append(j)
        return(n1new,n2new)

    def _removeH(self, mol1,mol2,nfoo,nbar):
        newn1 = []
        newn2 = []
        for n1,n2 in list(zip(nfoo,nbar)):
            a1 = mol1.GetAtomWithIdx(n1)
            a2 = mol2.GetAtomWithIdx(n2)
            id1 = a1.GetAtomicNum()
            id2 = a2.GetAtomicNum()
            bPolar1 = False
            bPolar2 = False

            # if both hydrogens and both not sigma hole
            if (id1==1) and (id2==1) and \
                 ('HSH' not in a1.GetMonomerInfo().GetName() ) and \
                 ('HSH' not in a2.GetMonomerInfo().GetName() ) :
                bPolar1 = self._isPolarH( a1 )
                bPolar2 = self._isPolarH( a2 )
                if(bPolar1==True and bPolar2==True):
                    if( self.bH2Hpolar==False ):
                        continue
                elif( self.bH2H==False ):
                    continue
                # only allow mapping hydrogens in those cases,
                # when heavyAtom1 and heavyAtom2 have an equal number of them bound
                heavyAtom1 = a1.GetNeighbors()[0]
                heavyAtom2 = a2.GetNeighbors()[0]
                nb1 = heavyAtom1.GetNeighbors() 
                nb2 = heavyAtom2.GetNeighbors() 
                nH1 = self._countHydrogens(nb1)
                nH2 = self._countHydrogens(nb2)
                if nH1!=nH2:
                    continue
            elif(self.bH2Heavy==False and ( (id1==1) ^ (id2==1) ) ): # ^ := xor
                continue

            newn1.append(n1)
            newn2.append(n2)
        return(newn1,newn2)

    def _isPolarH( self, a ):
        neighb = a.GetNeighbors()
        for a2 in neighb:
            anum2 = a2.GetAtomicNum()
            if anum2!=6:
                return(True)
        return(False)

    def _genCrippen(self, mol):
        crippen = Crippen._GetAtomContribs(mol)
        return(crippen)

    def _subMolRing(self, mol):
        copyMol = cp.deepcopy(mol)
        editMol = Chem.EditableMol(copyMol)
        indRm = []
    #   create an inverted list of ind, i.e. indRm
        for a in mol.GetAtoms():
            if(a.IsInRing()==False):
                indRm.append(a.GetIdx())
    #   remove the indRm atoms
        indRm.sort(reverse=True)
        for i in indRm:
            editMol.RemoveAtom(i)
        copyMol = editMol.GetMol()
        return(copyMol)


#*************************************************#
# Hybrid topology  #
#*************************************************#
class LigandHybridTopology:
    """Class builds a hybrid topology for two ligands.

    Parameters
    ----------
    ...

    Attributes
    ----------
    ....

    """

    def __init__(self, **kwargs):
        ####################################################
        ##### these parameters need to be initialized ######
        ####################################################
        self.m1 = None # original coordinates of pmx Model 1
        self.m2 = None # Model 2, but with coordinates of Model 1
        self.m3 = None # Model 1, but with coordinates of Model 2
        self.m4 = None # original coordinates of pmx Model 2
        self.itp1 = None
        self.itp2 = None
        self.bFit = False
        self.scDUMm = 1.0
        self.scDUMa = 1.0
        self.scDUMd = 1.0
        self.bDeAng = False
        self.pairList = []
        self.grps = None
        self.d = 0.05
        self.logfile = None
        self.bDist = True # this variable is always set to True
        
        for key, val in kwargs.items():
            setattr(self,key,val)


    def _makeHybridTop( self ):
        """The main function of this class.
            Generated hybrid topology
    
        Parameters
        ----------
        None

        Returns
        -------
        None

        """

#####################################
        # make atom pairs
        doLog(self.logfile,"Making atom pairs.....")
        self.atomPairs = [] # atom pairs
        self._make_atom_pairs( )

#####################################
        # identify dummies
        doLog(self.logfile,"Identifying dummies.....")
        self.dumsA = []
        self.dumsA_nofit = []
        self.dumsB = []
        self._make_dummies( )
        doLog(self.logfile, "Generated %d atom-atom pairs" % len(self.atomPairs))
        doLog(self.logfile,"Dummies in state A: %d" % len(self.dumsA))
        doLog(self.logfile,"Dummies in state B: %d" % len(self.dumsB))

#####################################
        # make B-states
        doLog(self.logfile,"Making B-states....")
        self._make_Bstates()

#####################################
        # construct dummies
        self.id_dicAB = {}
        self.id_dicBA = {}
        doLog(self.logfile,"Constructing dummies....")
        self._construct_dummies()

#####################################
        # make bonds
        doLog(self.logfile,"Construct bonds....")
        self.newbonds = []
        self._make_bonds()

#####################################
        # make angles
        doLog(self.logfile,"Construct angles....")
        self.newangles = []
        self._make_angles()

#####################################
        # make dihedrals
        doLog(self.logfile,"Construct dihedrals....")
        self.newdihedrals = []
        self._make_dihedrals()

#####################################
        # make vsites2
        doLog(self.logfile,"Construct vsites2....")
        self.newvsites2 = [] # vsites of type 2
        self.bHasVsites2 = False
        self._make_vsites2()

#####################################
        # make vsites3
        doLog(self.logfile,"Construct vsites3....")
        self.newvsites3 = [] # vsites of type 3
        self.bHasVsites3 = False
        self._make_vsites3()

#####################################
        # make exclusions
        doLog(self.logfile,"Construct exclusions....")
        self.newexclusions = [] 
        self.bHasExclusions = False
        self._make_exclusions()

#####################################
        # make pairs (1-4)
        doLog(self.logfile,"Construct pairs (1-4 interactions)....")
        self.newpairs = []
        self._make_pairs14()

#####################################
        # assemble new itp
        doLog(self.logfile,"Assembling new itp....")
        self.newitp = ''
        self.qA = []
        self.qB = []
        self.qA_mem = [] # needed for split topologies
        self.qB_mem = [] # needed for split topologies
        self._assemble_itp( )


    def _make_atom_pairs( self ):
        if len(self.pairList)>0:
            for n1, n2 in self.pairList:
                a1 = self.m1.fetch_atoms(n1,how='byid')[0]
                a4 = self.m4.fetch_atoms(n2,how='byid')[0]
                self.atomPairs.append( (a1, a4))
                for atom3 in self.m3.atoms:
                    if a1.id == atom3.id:
                        atom3.x = a4.x
                        if(self.bFit==True):
                            a2 = self.m2.fetch_atoms(n2,how='byid')[0]
                            atom3.x = a2.x
        elif(self.grps!=None and self.bDist==True):
            lst1 = self.m1.get_by_id(self.grps[0])
            lst2 = self.m4.get_by_id(self.grps[1])
            if self.bFit==True:
                lst2 = self.m2.get_by_id(self.grps[1])
            for atom in lst1:
                mi = self.d # nm
                keep = None
                for at in lst2:
                    d = (atom-at)/10.0
                    if d < mi:
                        keep = at
                        mi = d
                if keep is not None:
                    self.atomPairs.append( (atom, keep) )
                    for atom3 in self.m3.atoms:
                        if atom.id == atom3.id:
                            atom3.x = keep.x
        elif(self.bDist==True):
            mx = m4
            if bFit==True:
                mx = m2
            for atom in self.m1.atoms:
                mi = self.d # nm
                keep = None
                for at in self.mx.atoms:
                    d = (atom-at)/10.0
                    if d < mi:
                        keep = at
                        mi = d
                if keep is not None:
                    self.atomPairs.append( (atom, keep) )
                    for atom3 in self.m3.atoms:
                        if atom.id == atom3.id:
                            atom3.x = keep.x

    def _make_dummies( self ):
        morphsA = list(map(lambda p: p[1], self.atomPairs))
        morphsB = list(map(lambda p: p[0], self.atomPairs))
        for (atomfit,atomorig) in list(zip(self.m2.atoms,self.m4.atoms)):
            if (atomfit not in morphsA) and (atomorig not in morphsA):
#                self.dumsA.append( cp.deepcopy(atomfit) )
#                self.dumsA_nofit.append( cp.deepcopy(atomorig) )
                self.dumsA.append( atomfit )
                self.dumsA_nofit.append( atomorig )
        for (atomfit,at) in list(zip(self.m1.atoms,self.m3.atoms)):
            if atomfit not in morphsB:
                self.dumsB.append( atomfit )
#                self.dumsA_nofit_rev.append(at)
#        for atom in self.m1.atoms:
#            if atom not in morphsB:
#                self.dumsB.append(atom)

    def _make_Bstates( self ):
        for a1, a2 in self.atomPairs:
            a1.atomtypeB = a2.atomtype
            a2.atomtypeB = a1.atomtype #this is my change to catch the DISAPPEARING dihedrals
            a1.nameB = a2.name
            a1.qB = a2.q
            a1.mB = a2.m
            a1.idB = a2.id
            a2.idB = a1.id #this is my change to catch the DISAPPEARING dihedrals
            a2.mB = a1.m
            doLog(self.logfile, "Atom....: %4d  %12s | %6.2f | %6.2f -> %12s | %6.2f | %6.2f" %\
                   (a1.id, a1.atomtype, a1.q, a1.m, a1.atomtypeB, a1.qB, a1.mB))


    def _construct_dummies( self ):


#####################################
        # dummies in stateA
        doLog(self.logfile,"Dummies in stateA: ")

        for (atom,at) in list(zip(self.dumsA,self.dumsA_nofit)):
            atom.id_old = atom.id
            atom.nameB = atom.name
            if atom.name.startswith('H'):
                atom.name = 'HV'+atom.name[1:]
            else:
                atom.name = 'D'+atom.name
            atom.atomtypeB = atom.atomtype
            atom.atomtype = 'DUM_'+atom.atomtype
            atom.qB = atom.q
            atom.q = 0
            atom.mB = atom.m
            atom.m = atom.mB #1.
            atom.m = atom.m*self.scDUMm
            if( atom.m < 1.0 and atom.mB != 0.0): # exception for virtual particles
                atom.m = 1.0
            self.m1.residues[0].append(atom)

            at.id_old = at.id
            at.nameB = at.name
            if at.name.startswith('H'):
                at.name = 'HV'+at.name[1:]
            else:
                at.name = 'D'+at.name
            at.atomtypeB = at.atomtype
            at.atomtype = 'DUM_'+at.atomtype
            at.qB = at.q
            at.q = 0
            at.mB = at.m
            at.m = at.mB #1.
            at.m = at.m*self.scDUMm
            if( at.m < 1.0 and at.mB != 0.0): # exception for virtual particles
                at.m = 1.0
            self.m3.residues[0].append(at)

            doLog(self.logfile, "Dummy...: %4d  %12s | %6.2f | %6.2f -> %12s | %6.2f | %6.2f" %\
                     (atom.id, atom.atomtype, atom.q, atom.m, atom.atomtypeB, atom.qB, atom.mB))

#####################################
        # dummies in stateB
        doLog(self.logfile,"Dummies in stateB: ")
        for atom in self.dumsB:
            atom.atomtypeB = 'DUM_'+atom.atomtype
            atom.qB = 0
            atom.mB = atom.m #1.

            atom.mB = atom.mB*self.scDUMm
            if( atom.mB < 1.0 and atom.m != 0.0): # exception for virtual particles
                atom.mB = 1.0

            doLog(self.logfile, "Dummy...: %4d  %12s | %6.2f | %6.2f -> %12s | %6.2f | %6.2f" %\
                   (atom.id, atom.atomtype, atom.q, atom.m, atom.atomtypeB, atom.qB, atom.mB))

#####################################
        # establish atom ID mapping between states
        for atom in self.m1.atoms:
            if hasattr(atom,"idB"):
                self.id_dicAB[atom.id] = atom.idB
                self.id_dicBA[atom.idB] = atom.id
            if hasattr(atom,"id_old"):
                self.id_dicAB[atom.id] = atom.id_old
                self.id_dicBA[atom.id_old] = atom.id


    def _make_bonds( self ):
#####################################
        # first molecule bonds
        for b in self.itp1.bonds:
            id1 = b[0].id
            id2 = b[1].id
            a1 = self.m1.atoms[id1-1]
            a2 = self.m1.atoms[id2-1]
            bOk = False
            if hasattr(a1,"idB") and hasattr(a2,"idB"):
                idB1 = a1.idB
                idB2 = a2.idB
                entr = self._get_ff_entry([idB1, idB2], self.itp2, what= 'bond')
                if entr is not None:
                    self.newbonds.append([b[0],b[1],b[2],[b[2]]+b[3],[b[2]]+entr])
                    bOk = True
                else:
                    bOk = False
            elif a1.atomtypeB[:3] == 'DUM' or a2.atomtypeB[:3] == 'DUM':
                entr = self._get_ff_entry([a1.id, a2.id], self.itp1, what= 'bond')
                if entr is not None:
                    self.newbonds.append ([b[0],b[1],b[2],[b[2]]+b[3],[b[2]]+entr])
                    bOk = True
                else:
                    bOk = False
            else:
                self.newbonds.append(b)
                bOk = True

            if not bOk:
                doLog(self.logfile, "Error: Something went wrong while assigning bonds!")
                doLog(self.logfile, "A-> Atom1: %d-%s Atom2: %d-%s" %(a1.id, a1.name, a2.id, a2.name))
                doLog(self.logfile, "B-> Atom1: %d-%s Atom2: %d-%s" %(a1.idB, a1.nameB, a2.idB, a2.nameB))
                doLog(self.logfile,"Exiting....")
                sys.exit(1)

#####################################
        # second molecule bonds
        for b in self.itp2.bonds:
            newid1 = self.id_dicBA[b[0].id]
            newid2 = self.id_dicBA[b[1].id]
            a1 = self.m1.atoms[newid1-1]
            a2 = self.m1.atoms[newid2-1]
            if a1.atomtype.startswith('DUM') or \
               a2.atomtype.startswith('DUM'):
                self.newbonds.append( [a1, a2, 1, [1]+b[-1], [1]+b[-1]] )




    def _make_angles( self ):
#####################################
        # first molecule angles
        decoupAngles = []
        for b in self.itp1.angles:
            angtype = b[3]
            ####### explanation for angle parameters ############
            # for angtype 1, entry is [angle,force_const]
            # for angtype 5, entry is [5,angle,force_const,bond,force_const]
            # write_angles() in forcefield2.py expects entry [angtype,angle,force_const,...]
            id1 = b[0].id
            id2 = b[1].id
            id3 = b[2].id
            a1 = self.m1.atoms[id1-1]
            a2 = self.m1.atoms[id2-1]
            a3 = self.m1.atoms[id3-1]
            bOk = False
            if hasattr(a1,"idB") and hasattr(a2,"idB") and hasattr(a3,"idB"):
                idB1 = a1.idB
                idB2 = a2.idB
                idB3 = a3.idB
                entr = self._get_ff_entry([idB1, idB2, idB3], self.itp2, what= 'angle')
                if entr is not None:
                    if angtype==1:
                        self.newangles.append ([b[0],b[1],b[2],angtype,[angtype]+b[4],[angtype]+entr])
                    else:
                        self.newangles.append ([b[0],b[1],b[2],angtype,b[4],entr])
                    bOk = True
                else:
                    bOk = False
            elif a1.atomtypeB[:3] == 'DUM' or \
                     a2.atomtypeB[:3] == 'DUM' or \
                     a3.atomtypeB[:3] == 'DUM':
                entr = self._get_ff_entry([a1.id, a2.id, a3.id], self.itp1, what= 'angle')
                if (angtype!=1) and (entr is not None): # remove the angtype from the entr
                    entr = entr[1:]
                if entr is not None:
                    if( (a1.atomtypeB[:3] != 'DUM' and a2.atomtypeB[:3] != 'DUM') \
                        or (a1.atomtypeB[:3] != 'DUM' and a3.atomtypeB[:3] != 'DUM') \
                        or (a2.atomtypeB[:3] != 'DUM' and a3.atomtypeB[:3] != 'DUM') ):
#                   if( (a1.atomtypeB[:3] != 'DUM') or (a2.atomtypeB[:3] != 'DUM') or (a3.atomtypeB[:3] != 'DUM') ):
                        entr[1] = self.scDUMa*entr[1]
                        if angtype==5:
                            entr[3] = self.scDUMa*entr[3]
                        if(self.bDeAng==True):
                            if( a1.atomtypeB[:3] == 'DUM' ):
                                if( id1 in decoupAngles ):
                                    entr[1] = 0.0
                                    if angtype==5:
                                        entr[3] = 0.0
                                else:
                                    decoupAngles.append(id1)
                            elif( a2.atomtypeB[:3] == 'DUM' ):
                                if( id2 in decoupAngles ):
                                    entr[1] = 0.0
                                    if angtype==5:
                                        entr[3] = 0.0
                                else:
                                    decoupAngles.append(id2)
                            elif( a3.atomtypeB[:3] == 'DUM' ):
                                if( id3 in decoupAngles ):
                                    entr[1] = 0.0
                                    if angtype==5:
                                        entr[3] = 0.0
                                else:
                                    decoupAngles.append(id3)
                    if angtype==1:
                        self.newangles.append ([b[0],b[1],b[2],angtype,[angtype]+b[4],[angtype]+entr])
                    else:
                        self.newangles.append ([b[0],b[1],b[2],angtype,b[4],[angtype]+entr])
                    bOk = True
                else:
                    bOk = False
            else:
                self.newangles.append(b)
                bOk = True

            if not bOk:
                doLog(self.logfile, "Error: Something went wrong while assigning angles!")
                doLog(self.logfile, "A-> Atom1: %d-%s Atom2: %d-%s Atom3: %d-%s" \
                       %(a1.id, a1.name, a2.id, a2.name, a3.id, a3.name))
                doLog(self.logfile, "B-> Atom1: %d-%s Atom2: %d-%s Atom3: %d-%s" \
                       %(a1.idB, a1.nameB, a2.idB, a2.nameB, a3.idB, a3.nameB))
                doLog(self.logfile,"Exiting....")
                sys.exit(1)

#####################################
        # second molecule angles
        decoupAngles = []
        for b in self.itp2.angles:
            angtype = b[3]
            entry = b[-1]
            # for type 1
            paramA = [ 1,entry[0],0.0 ]
            paramAscDum = [ 1,entry[0],self.scDUMa*float(entry[1]) ]
            paramB = [ 1,entry[0],entry[1] ]
            # for type 5
            if angtype==5:
                paramA = [ 5,entry[1],0.0,entry[3],0.0 ]
                paramAscDum = [ 5,entry[1],self.scDUMa*float(entry[2]),entry[3],self.scDUMa*float(entry[4]) ]
                paramB = [ 5,entry[1],entry[2],entry[3],entry[4] ]

            newid1 = self.id_dicBA[b[0].id]
            newid2 = self.id_dicBA[b[1].id]
            newid3 = self.id_dicBA[b[2].id]
            a1 = self.m1.atoms[newid1-1]
            a2 = self.m1.atoms[newid2-1]
            a3 = self.m1.atoms[newid3-1]
            if a1.atomtype.startswith('DUM') or \
               a2.atomtype.startswith('DUM') or \
               a3.atomtype.startswith('DUM'):
                if( (a1.atomtype.startswith('DUM')==False and a2.atomtype.startswith('DUM')==False) \
                   or (a1.atomtype.startswith('DUM')==False and a3.atomtype.startswith('DUM')==False) \
                   or (a2.atomtype.startswith('DUM')==False and a3.atomtype.startswith('DUM')==False) ):
#               if( a1.atomtype.startswith('DUM')==False or a2.atomtype.startswith('DUM')==False or a3.atomtype.startswith('DUM')==False ):

                    if(self.bDeAng==True):
                        if( a1.atomtype.startswith('DUM')==True ):
                            if newid1 in decoupAngles:
                                self.newangles.append([ a1,a2,a3,angtype, paramA, paramB ])
                            else:
                                self.newangles.append([ a1,a2,a3,angtype, paramAscDum, paramB ])
                                decoupAngles.append(newid1)
                        elif( a2.atomtype.startswith('DUM')==True ):
                            if newid2 in decoupAngles:
                                self.newangles.append([ a1,a2,a3,angtype, paramA, paramB ])
                            else:
                                self.newangles.append([ a1,a2,a3,angtype, paramAscDum, paramB ])
                                decoupAngles.append(newid2)
                        elif( a3.atomtype.startswith('DUM')==True ):
                            if newid3 in decoupAngles:
                                self.newangles.append([ a1,a2,a3,angtype, paramA, paramB ])
                            else:
                                self.newangles.append([ a1,a2,a3,angtype, paramAscDum, paramB ])
                                decoupAngles.append(newid3)
                    else:
                        self.newangles.append([ a1,a2,a3,angtype, paramAscDum, paramB ])
                else:
                    self.newangles.append([ a1,a2,a3,angtype, paramB, paramB ])


    def _make_dihedrals( self ):
        cpItp1 = cp.deepcopy(self.itp1)
        cpItp2 = cp.deepcopy(self.itp2)

#####################################
        # first molecule dihedrals
        for b in self.itp1.dihedrals:
            id1 = b[0].id
            id2 = b[1].id
            id3 = b[2].id
            id4 = b[3].id
            dih_type = b[4]
            a1 = self.m1.atoms[id1-1]
            a2 = self.m1.atoms[id2-1]
            a3 = self.m1.atoms[id3-1]
            a4 = self.m1.atoms[id4-1]
            entrA = self._get_ff_entry([id1, id2, id3, id4, dih_type], cpItp1, what= 'dihedral')
            bOk = False
            if hasattr(a1,"idB") and hasattr(a2,"idB") and \
                   hasattr(a3,"idB") and hasattr(a4,"idB"):
                # switch the A state off
                dih = self._gen_dih_entry2(a1, a2, a3, a4, dih_type, entrA,None)
                self.newdihedrals.extend(dih)
                bOk = True
            else:
                # switch the B state on
                if a1.atomtypeB[:3] == 'DUM' or \
                         a2.atomtypeB[:3] == 'DUM' or \
                         a3.atomtypeB[:3] == 'DUM' or \
                         a4.atomtypeB[:3] == 'DUM':
                    if entrA is not None:
                        dih = self._gen_dih_entry2(a1, a2, a3, a4, dih_type,entrA,None)
                        self.newdihedrals.extend(dih)
                        if( a1.atomtypeB[:3] != 'DUM' or a2.atomtypeB[:3] != 'DUM' or a3.atomtypeB[:3] != 'DUM' or a4.atomtypeB[:3] != 'DUM' ):
                            if dih_type==2 or dih_type==4:# or dih_type==1: # disable improper for dummy-nondummy
                                dih = self._gen_dih_entry2(a1, a2, a3, a4, dih_type,None,entrA,1.0,0.0)
                            else:
                                dih = self._gen_dih_entry2(a1, a2, a3, a4, dih_type,None,entrA,1.0,self.scDUMd)
                        else:
                            dih = self._gen_dih_entry2(a1, a2, a3, a4, dih_type,None,entrA)
                        self.newdihedrals.extend(dih)
                        bOk = True
                    else:
                        bOk = False
                else:
                    self.newdihedrals.append(b)
                    bOk = True

            if not bOk:
                doLog(self.logfile, "Error: Something went wrong while assigning dihedrals!")
                doLog(self.logfile, "A-> Atom1: %d-%s Atom2: %d-%s Atom3: %d-%s Atom3: %d-%s" \
                       %(a1.id, a1.name, a2.id, a2.name, a3.id, a3.name, a4.id, a4.name))
                doLog(self.logfile, "B-> Atom1: %d-%s Atom2: %d-%s Atom3: %d-%s Atom3: %d-%s" \
                       %(a1.idB, a1.nameB, a2.idB, a2.nameB, a3.idB, a3.nameB, a4.idB, a4.nameB))
                doLog(self.logfile,"Exiting....")
                sys.exit(1)

#####################################
        # second molecule dihedrals
        for b in self.itp2.dihedrals:
            id1 = b[0].id
            id2 = b[1].id
            id3 = b[2].id
            id4 = b[3].id
            aB1 = self.m4.atoms[id1-1]
            aB2 = self.m4.atoms[id2-1]
            aB3 = self.m4.atoms[id3-1]
            aB4 = self.m4.atoms[id4-1]
            newid1 = self.id_dicBA[b[0].id]
            newid2 = self.id_dicBA[b[1].id]
            newid3 = self.id_dicBA[b[2].id]
            newid4 = self.id_dicBA[b[3].id]
            a1 = self.m1.atoms[newid1-1]
            a2 = self.m1.atoms[newid2-1]
            a3 = self.m1.atoms[newid3-1]
            a4 = self.m1.atoms[newid4-1]
            dih_type = b[4]
            entrB = self._get_ff_entry([b[0].id,b[1].id,b[2].id,b[3].id, dih_type], cpItp2, what='dihedral')
            bOk = False
            if hasattr(aB1,"idB") and hasattr(aB2,"idB") and \
                   hasattr(aB3,"idB") and hasattr(aB4,"idB"):
                # switch the B state off
                dih = self._gen_dih_entry2(a1,a2,a3,a4, dih_type,None,entrB)
                self.newdihedrals.extend(dih)
                bOk = True
            else:
                # switch the A state on
                if a1.atomtype.startswith('DUM') or \
                   a2.atomtype.startswith('DUM') or \
                   a3.atomtype.startswith('DUM') or \
                   a4.atomtype.startswith('DUM'):
                    if entrB is not None:
                        dih = self._gen_dih_entry2(a1,a2,a3,a4,dih_type,None,entrB)
                        self.newdihedrals.extend(dih)
                        if( a1.atomtype.startswith('DUM')==False or a2.atomtype.startswith('DUM')==False \
                            or a3.atomtype.startswith('DUM')==False or a4.atomtype.startswith('DUM')==False ):
                            if dih_type==2 or dih_type==4:# or dih_type==1: # disable improper for dummy-nondummy
                                dih = self._gen_dih_entry2(a1,a2,a3,a4,dih_type,entrB,None,0.0,1.0)
                            else:
                                dih = self._gen_dih_entry2(a1,a2,a3,a4,dih_type,entrB,None,self.scDUMd,1.0)
                        else:
                            dih = self._gen_dih_entry2(a1,a2,a3,a4,dih_type,entrB,None)
                        self.newdihedrals.extend(dih)
                        bOk = True
                    else:
                        bOk = False
                else:
                    self.newdihedrals.append(b)
                    bOk = True

            if not bOk:
                doLog(self.logfile, "Error: Something went wrong while assigning dihedrals!")
                doLog(self.logfile, "A-> Atom1: %d-%s Atom2: %d-%s Atom3: %d-%s Atom3: %d-%s" \
                       %(a1.id, a1.name, a2.id, a2.name, a3.id, a3.name, a4.id, a4.name))
                doLog(self.logfile, "B-> Atom1: %d-%s Atom2: %d-%s Atom3: %d-%s Atom3: %d-%s" \
                       %(a1.idB, a1.nameB, a2.idB, a2.nameB, a3.idB, a3.nameB, a4.idB, a4.nameB))
                doLog(self.logfile,"Exiting....")
                sys.exit(1)

    def _gen_dih_entry2(self, a1,a2,a3,a4,dihtype,entry=None,entryB=None,scDumA=1.0,scDumB=1.0):
        dihList = []
        ids = [a1.id,a2.id,a3.id,a4.id]
        if entry!=None:
            entry = entry[0].split()
            entry = [float(foo) for foo in entry]
        if entryB!=None:
            entryB = entryB[0].split()
            entryB = [float(foo) for foo in entryB]
        if (dihtype == 3 ):
            zeroesA = [0,0,0,0,0,0]
            zeroesB = [0,0,0,0,0,0]
        elif (dihtype == 2): # improper has no multiplicity
            angleA = 0
            angleB = 0
            if( entry != None ):
                angleA = entry[0]
            if( entryB != None ):
                angleB = entryB[0]
            zeroesA = [angleA,0]
            zeroesB = [angleB,0]
        else: # all the other types are the same
            multA = 0
            multB = 0
            angleA = 0
            angleB = 0
            if( entry != None ):
                angleA = entry[0]
                multA = entry[2]
            if( entryB != None ):
                angleB = entryB[0]
                multB = entryB[2]
            zeroesA = [angleA,0,multA]
            zeroesB = [angleB,0,multB]

        if( entry != None ):
            if( dihtype == 3 ):
                entry = [float(foo)*scDumA for foo in entry]
            else:
                entry[1] = scDumA*float(entry[1])
            dih = [a1,a2,a3,a4]+[dihtype]+[[dihtype]+entry]+[[dihtype]+zeroesA]
            dihList.append(dih)
        if( entryB != None ):
            if( dihtype == 3 ):
                entryB = [float(foo)*scDumB for foo in entryB]
            else:
                entryB[1] = scDumB*float(entryB[1])
            dih = [a1,a2,a3,a4]+[dihtype]+[[dihtype]+zeroesB]+[[dihtype]+entryB]
            dihList.append(dih)
#        if( entry==None ):
#            dih = ids+entryB+zeroesB
#            dihList.append(dih)
#        if( entryB==None ):
#           dih = ids+zeroesA+entry
#           dihList.append(dih)
        return dihList

    def _make_vsites2( self ):
#####################################
        # first molecule vsites
        if(self.itp1.virtual_sites2):
            self.bHasVsites2 = True
            for b in self.itp1.virtual_sites2:
#                id1 = b[0].id
#                id2 = b[1].id
#                id3 = b[2].id
#                a1 = self.m1.atoms[id1-1]
#                a2 = self.m1.atoms[id2-1]
#                a3 = self.m1.atoms[id3-1]
                self.newvsites2.append(b)

#####################################
        # second molecule vsites
        if(self.itp2.virtual_sites2):
            self.bHasVsites2 = True
            for b in self.itp2.virtual_sites2:
                newid1 = self.id_dicBA[b[0].id]
                newid2 = self.id_dicBA[b[1].id]
                newid3 = self.id_dicBA[b[2].id]
                a1 = self.m1.atoms[newid1-1]
                a2 = self.m1.atoms[newid2-1]
                a3 = self.m1.atoms[newid3-1]
                vsiteToAdd = [a1,a2,a3,b[3],b[4]]
                if( self._check_if_vsite2_exists( self.newvsites2, vsiteToAdd )==False ):
                    self.newvsites2.append( [a1, a2, a3, b[3], b[4]] )

    def _make_vsites3( self ):
#####################################
        # first molecule vsites
        if(self.itp1.virtual_sites3):
            self.bHasVsites3 = True
            for b in self.itp1.virtual_sites3:
                self.newvsites3.append(b)

#####################################
        # second molecule vsites
        if(self.itp2.virtual_sites3):
            self.bHasVsites3 = True
            for b in self.itp2.virtual_sites3:
                newid1 = self.id_dicBA[b[0].id]
                newid2 = self.id_dicBA[b[1].id]
                newid3 = self.id_dicBA[b[2].id]
                newid4 = self.id_dicBA[b[3].id]
                a1 = self.m1.atoms[newid1-1]
                a2 = self.m1.atoms[newid2-1]
                a3 = self.m1.atoms[newid3-1]
                a4 = self.m1.atoms[newid4-1]
                vsiteToAdd = [a1,a2,a3,a4,b[4],b[5]]
                if( self._check_if_vsite3_exists( self.newvsites3, vsiteToAdd )==False ):
                    self.newvsites3.append( [a1, a2, a3, a4, b[4], b[5]] )

    def _make_exclusions( self ):
#####################################
        # first molecule exclusions
        if(self.itp1.exclusions):
            self.bHasExclusions = True
            for excl in self.itp1.exclusions:
                exclToAdd = []
                for ex in excl:
                    newid = ex.id
                    newa = self.m1.atoms[newid-1]
                    exclToAdd.append(newa)
                self.newexclusions.append( exclToAdd )

#####################################
        # second molecule exclusions
        if(self.itp2.exclusions):
            self.bHasExclusions = True
            for excl in self.itp2.exclusions:
                exclToAdd = []
                for ex in excl:
                    newid = self.id_dicBA[ex.id]
                    newa = self.m1.atoms[newid-1]
                    exclToAdd.append(newa)
                if( self._check_if_exclusion_valid( self.newexclusions, exclToAdd )==True ):
                    self.newexclusions.append( exclToAdd )

    def _make_pairs14( self ):
        pp = []
#####################################
        # first molecule pairs14
        for p in self.itp1.pairs:
            self.newpairs.append( p )
            pp.append( (p[0].id,p[1].id) )

#####################################
        # second molecule pairs14
        for p in self.itp2.pairs:
            newid1 = self.id_dicBA[p[0].id]
            newid2 = self.id_dicBA[p[1].id]
            a1 = self.m1.atoms[newid1-1]
            a2 = self.m1.atoms[newid2-1]
            if (newid1, newid2) not in pp and \
               (newid2, newid1) not in pp:
                self.newpairs.append([ a1, a2, 1] )

    def _assemble_itp( self ):
        self.newitp = TopolBase(filename=None)
        self.newitp.name = self.itp1.name
        self.newitp.nrexcl = self.itp1.nrexcl
        self.newitp.atoms = self.m1.atoms
        self.newitp.residues = self.m1.residues
        for i, atom in list(enumerate(self.newitp.atoms)):
            atom.cgnr = i +1
        self.newitp.bonds = self.newbonds
        self.newitp.pairs = self.newpairs
        self.newitp.angles = self.newangles
        self.newitp.dihedrals = self.newdihedrals
        self.newitp.virtual_sites2 = self.newvsites2
        self.newitp.has_vsites2 = self.bHasVsites2
        self.newitp.virtual_sites3 = self.newvsites3
        self.newitp.has_vsites3 = self.bHasVsites3
        self.newitp.exclusions = self.newexclusions
        self.newitp.has_exclusions = self.bHasExclusions
        # get charges
        self.qA, self.qB = self._sum_charge_of_states( self.newitp )
        self.qA_mem = cp.deepcopy( self.qA )
        self.qB_mem = cp.deepcopy( self.qB )

    def _sum_charge_of_states( self, itp ):
        qA = 0.0
        qB = 0.0
        for a in itp.atoms:
            qA += a.q
            if self._atoms_morphe([a]):
                qB+=a.qB
            else:
                qB+=a.q
        return(qA, qB)
#        return([qA], [qB])

    def _atoms_morphe( self, atoms ):
        for atom in atoms:
            if atom.atomtypeB is not None and (atom.q!=atom.qB or atom.m != atom.mB): 
                return(True)
        return(False)

    def _check_if_vsite2_exists( self, vsites, xs):
        for v in vsites:
            if v[0].id==xs[0].id and v[1].id==xs[1].id and v[2].id==xs[2].id and v[3]==xs[3] and v[4]==xs[4]:
                return(True)
            elif v[0].id==xs[0].id and v[1].id==xs[1].id and v[2].id==xs[2].id:
                print("ERROR: it seems that vsites2 are to be morphed. Gromacs does not support that")
                sys.exit(0)
        return(False)

    def _check_if_vsite3_exists( self, vsites, xs):
        for v in vsites:
            if v[0].id==xs[0].id and v[1].id==xs[1].id and v[2].id==xs[2].id and v[3].id==xs[3].id and v[4]==xs[4] and v[5]==xs[5]:
                return(True)
            elif v[0].id==xs[0].id and v[1].id==xs[1].id and v[2].id==xs[2].id and v[3].id==xs[3].id:
                print("ERROR: it seems that vsites3 are to be morphed. Gromacs does not support that")
                sys.exit(0)
        return(False)

    def _check_if_exclusion_valid( self, exclusions, ex ):
        # return True, if all fine and ex can be added

        # check if the first atom is dummy. If so, all fine
        a0 = ex[0]
        if ('DUM' in a0.atomtype) or ('DUM' in a0.atomtypeB):
            return(True)

        # check if an exclusion already exists
        for excl in exclusions:
            if excl[0]==ex[0]: # some interactions for this atom are already excluded
                if len(excl)!=len(ex): # the exclusion lists are of different length
                    print('ERROR: Something wrong with exclusions: ',excl,ex)
                    sys.exit(0)
                else:
                    for e in excl[1:]:
                        if e not in ex:
                            print('ERROR: Something wrong with exclusions: ',excl,ex)
                            sys.exit(0)
        return(True)


    def _get_ff_entry(self, ids, itp, gmx45=True, what = 'bond'):
        if what == 'bond':
            for b in itp.bonds:
                if (b[0].id == ids[0] and b[1].id == ids[1]) or \
                   (b[1].id == ids[0] and b[0].id == ids[1]):
                    out = cp.deepcopy(b)
                    return(out[3:][0])
        elif what == 'angle':
            for b in itp.angles:
                if (b[0].id == ids[0] and b[1].id == ids[1] and b[2].id == ids[2]) or \
                   (b[2].id == ids[0] and b[1].id == ids[1] and b[0].id == ids[2]):
                    out = cp.deepcopy(b)
                    return(out[4:][0])
        elif what == 'dihedral':
            if (ids[4] == 3):
                for b in itp.dihedrals:
                    if (b[0].id == ids[0] and b[1].id == ids[1] and b[2].id == ids[2] and b[3].id == ids[3]) or \
                       (b[3].id == ids[0] and b[2].id == ids[1] and b[1].id == ids[2] and b[0].id == ids[3]):
                        if( ids[4] == b[4] ):
                            itp.dihedrals.remove(b)
                            out = cp.deepcopy(b)
                            return(out[5:])
            elif (ids[4] == 9):
                for b in itp.dihedrals:
                    if (b[0].id == ids[0] and b[1].id == ids[1] and b[2].id == ids[2] and b[3].id == ids[3]) or \
                       (b[3].id == ids[0] and b[2].id == ids[1] and b[1].id == ids[2] and b[0].id == ids[3]):
                        if( ids[4] == b[4] ):
                            itp.dihedrals.remove(b)
                            out = cp.deepcopy(b)
                            return(out[5:])
            elif (ids[4]==1 and gmx45==True):
                for b in itp.dihedrals:
                    if (b[0].id == ids[0] and b[1].id == ids[1] and b[2].id == ids[2] and b[3].id == ids[3]) or \
                       (b[3].id == ids[0] and b[2].id == ids[1] and b[1].id == ids[2] and b[0].id == ids[3]):
                        if( ids[4] == b[4] ):
                            itp.dihedrals.remove(b)
                            out = cp.deepcopy(b)
                            return(out[5:])
            elif (ids[4]==2 or ids[4]==4 ): # improper
                for b in itp.dihedrals:
                    sum = 0
                    for b1 in range(0,4):
                        for b2 in range(0,4):
                            if (b[b1].id == ids[b2]):
                               sum += 1
                               break
                    if ( (sum ==4) and (b[4]==2 or b[4]==4) ):
                        itp.dihedrals.remove(b)
                        out = cp.deepcopy(b)
                        return(out[5:])
            elif (ids[4]==1 and gmx45==False ): # improper as proper in version < gmx45
                for b in itp.dihedrals:
                    sum = 0
                    for b1 in range(0,4):
                        for b2 in range(0,4):
                            if (b[b1].id == ids[b2]):
                                sum += 1
                                break
                    if ( (sum == 4) and (b[4]==1) ):
                        itp.dihedrals.remove(b)
                        out = cp.deepcopy(b)
                        return(out[5:])
        return None

    def _write_ffitp( self, fname, m ):
        fp = open(fname,'w')
        dd = []
        fp.write('[ atomtypes ]\n')
        for atom in m.atoms:
            if atom.atomtype.startswith('DUM') and atom.atomtype not in dd:
                fp.write('%8s %12.6f %12.6f %3s %12.6f %12.6f\n' % (atom.atomtype, 0, 0, 'A',0,0) )
                dd.append(atom.atomtype)
            elif atom.atomtypeB.startswith('DUM') and atom.atomtypeB not in dd:
                fp.write('%8s %12.6f %12.6f %3s %12.6f %12.6f\n' % (atom.atomtypeB, 0, 0, 'A',0,0) )
                dd.append(atom.atomtypeB)
        fp.close()

    def _write_split_itp( self, fname ):
        root, ext = os.path.splitext(fname)
        out_file_qoff = root+'_qoff'+ext
        out_file_vdw = root+'_vdw'+ext
        out_file_qon = root+'_qon'+ext

        doLog(self.logfile,'------------------------------------------------------')
        doLog(self.logfile,'Creating splitted topologies............')
        doLog(self.logfile,'Making "qoff" topology : "%s"' % out_file_qoff)
        contQ = cp.deepcopy(self.qA_mem)
        self.newitp.write( out_file_qoff, stateQ = 'AB', stateTypes = 'AA', dummy_qB='off',
                          scale_mass=1.0, target_qB = self.qA, stateBonded = 'AA', full_morphe = False )
        doLog(self.logfile,'Charge of state A: %g' % self.qA )
        doLog(self.logfile,'Charge of state B: %g' % self.qB )

        doLog(self.logfile,'------------------------------------------------------')
        doLog(self.logfile,'Making "vdw" topology : "%s"' % out_file_vdw )
        contQ = cp.deepcopy(self.qA_mem)
        self.newitp.write( out_file_vdw, stateQ = 'BB', stateTypes = 'AB', dummy_qA='off', dummy_qB = 'off',
                          scale_mass = 1.0, target_qB = contQ, stateBonded = 'AB' , full_morphe = False)
        doLog(self.logfile,'Charge of state A: %g' % self.qA)
        doLog(self.logfile,'Charge of state B: %g' % self.qB)
        doLog(self.logfile,'------------------------------------------------------')

        doLog(self.logfile,'Making "qon" topology : "%s"' % out_file_qon)
        self.newitp.write( out_file_qon, stateQ = 'BB', stateTypes = 'BB', dummy_qA='off', dummy_qB = 'on',
                          scale_mass = 1.0, target_qB = self.qB_mem,  stateBonded = 'BB' , full_morphe = False)
        doLog(self.logfile,'Charge of state A: %g' % self.qA)
        doLog(self.logfile,'Charge of state B: %g' % self.qB)
        doLog(self.logfile,'------------------------------------------------------')

#**********************************************#
#### merge ff itp files ####
#**********************************************#
class _FFatom:
    def __init__(self,list):
        self.type = list[0]
        self.sigmaA = list[1]
        self.epsA = list[2]
        self.A = list[3]
        self.sigmaB = list[4]
        self.epsB = list[5]

class _FFfile:
    def __init__(self, fname=None):
        if fname is not None:
            foo = fname.split('.')
            bar = re.sub('ff','',foo[0])
            self.name = bar
            self.atoms = []
            self._read_ffitp(fname)

    def _read_ffitp(self,file):
        l = open(file).readlines()
        toSkip = "atomtypes"
        for line in l:
            if toSkip not in line:
                if line.split():
                    self.atoms.append(_FFatom(line.split()))

def _get_FF_atoms( ffs ):
    atoms = {}
    for ffile in ffs:
        ff = _FFfile(ffile)
        for at1 in ff.atoms:
            if at1.type in atoms.keys(): # check if atom type already exists
                at2 = atoms[at1.type]
                if (at1.type == at2.type and at1.sigmaA == at2.sigmaA and at1.epsA == at2.epsA and at1.sigmaB == at2.sigmaB and at1.epsB == at2.epsB):
                    continue
                else:
                    sys.stdout.write('Found two atoms of type %s, but they have different parameters, consider renaming atom types\n' % at1.type)
            else:
                atoms[at1.type] = at1

    return (atoms)

def _write_FF_file(atoms,file):
    fp = open(file,'w')
    fp.write('[ atomtypes ]\n')
    for atype in atoms.keys():
        at = atoms[atype]
        fp.write('      %s      %s      %s      %s      %s      %s\n' % (at.type,at.sigmaA,at.epsA,at.A,at.sigmaB,at.epsB) )
        
def _merge_FF_files( fnameOut, ffsIn=[] ):
    atoms = _get_FF_atoms( ffsIn )
    _write_FF_file( atoms, fnameOut )
    

