## @file mol3D.py
#  Defines mol3D class and contains useful manipulation/retrieval routines.
#
#  Written by Tim Ioannidis and JP Janet for HJK Group
#
#  Dpt of Chemical Engineering, MIT

from math import sqrt
import numpy as np
from ofrcalc.classes.atom3D import atom3D
from ofrcalc.classes.globalvars import globalvars
import openbabel
import sys, time, os, subprocess, random, shutil, unicodedata, inspect, tempfile, re


## Euclidean distance between points
#  @param R1 Point 1
#  @param R2 Point 2
#  @return Euclidean distance
def distance(R1, R2):
    dx = R1[0] - R2[0]
    dy = R1[1] - R2[1]
    dz = R1[2] - R2[2]
    d = sqrt(dx ** 2 + dy ** 2 + dz ** 2)
    return d


## Class for molecules that will be used to manipulate coordinates and other properties
class mol3D:
    ## Constructor
    #  @param self The object pointer
    def __init__(self):
        ## List of atom3D objects
        self.atoms = []
        ## Number of atoms
        self.natoms = 0
        ## Mass of molecule
        self.mass = 0
        ## Size of molecule
        self.size = 0
        ## Charge of molecule
        self.charge = 0
        ## Force field optimization settings
        self.ffopt = 'BA'
        ## Name of molecule
        self.name = ''
        ## Holder for openbabel molecule
        self.OBMol = False
        ## Holder for bond order matrix
        self.BO_mat = False
        ## List of connection atoms
        self.cat = []
        ## Denticity
        self.denticity = 0
        ## Identifier
        self.ident = ''
        ## Holder for global variables
        self.globs = globalvars()
        ## Holder for molecular graph
        self.graph = []
        self.xyzfile = 'undef'
        self.updated = False
        self.needsconformer = False
        ## Holder for molecular group
        self.grps = False
        # Holder rfor metals
        self.metals = False

        ## Holder for partial charge for each atom
        self.partialcharges = []

        # ---geo_check------
        self.dict_oct_check_loose = self.globs.geo_check_dictionary()["dict_oct_check_loose"]
        self.dict_oct_check_st = self.globs.geo_check_dictionary()["dict_oct_check_st"]
        self.dict_oneempty_check_loose = self.globs.geo_check_dictionary()["dict_oneempty_check_loose"]
        self.dict_oneempty_check_st = self.globs.geo_check_dictionary()["dict_oneempty_check_st"]
        self.oct_angle_ref = self.globs.geo_check_dictionary()["oct_angle_ref"]
        self.oneempty_angle_ref = self.globs.geo_check_dictionary()["oneempty_angle_ref"]
        self.geo_dict = dict()
        self.std_not_use = list()

        self.num_coord_metal = -1
        self.catoms = list()
        self.init_mol_trunc = False
        self.my_mol_trunc = False
        self.flag_oct = -1
        self.flag_list = list()
        self.dict_lig_distort = dict()
        self.dict_catoms_shape = dict()
        self.dict_orientation = dict()
        self.dict_angle_linear = dict()


    ## Add atom to molecule
    #
    #  Added atom is appended to the end of the list.
    #  @param self The object pointer
    #  @param atom atom3D of atom to be added
    def addAtom(self, atom, index=None):
        if index == None:
            index = len(self.atoms)
        # self.atoms.append(atom)
        self.atoms.insert(index, atom)
        if atom.frozen:
            self.atoms[index].frozen = True
        self.natoms += 1
        self.mass += atom.mass
        self.size = self.molsize()
        self.graph = []
        self.metal = False

    ## Change type of atom in molecule
    #
    #  @param self The object pointer
    #  @param atom_ind int index of atom
    #  @param atom_type str type of the new atom
    def changeAtomtype(self, atom_ind, atom_type):
        self.atoms[atom_ind].sym = atom_type
        self.metal = False



    ## Computes coordinates of center of mass of molecule
    #  @param self The object pointer
    #  @return List of center of mass coordinates
    def centermass(self):
        # OUTPUT
        #   - pcm: vector representing center of mass
        # initialize center of mass and mol mass
        pmc = [0, 0, 0]
        mmass = 0
        # loop over atoms in molecule
        if self.natoms > 0:
            for atom in self.atoms:
                # calculate center of mass (relative weight according to atomic mass)
                xyz = atom.coords()
                pmc[0] += xyz[0] * atom.mass
                pmc[1] += xyz[1] * atom.mass
                pmc[2] += xyz[2] * atom.mass
                mmass += atom.mass
            # normalize
            pmc[0] /= mmass
            pmc[1] /= mmass
            pmc[2] /= mmass
        else:
            pmc = False
            print('ERROR: Center of mass calculation failed. Structure will be inaccurate.\n')
        return pmc

    ## Computes coordinates of center of symmetry of molecule
    #
    #  Identical to centermass, but not weighted by atomic masses.
    #  @param self The object pointer
    #  @return List of center of symmetry coordinates
    def centersym(self):
        # initialize center of mass and mol mass
        pmc = [0, 0, 0]
        # loop over atoms in molecule
        for atom in self.atoms:
            # calculate center of symmetry
            xyz = atom.coords()
            pmc[0] += xyz[0]
            pmc[1] += xyz[1]
            pmc[2] += xyz[2]
        # normalize
        pmc[0] /= self.natoms
        pmc[1] /= self.natoms
        pmc[2] /= self.natoms
        return pmc

    ## remove all openbabel bond order indo
    #  @param self The object pointer
    def cleanBonds(self):
        obiter = openbabel.OBMolBondIter(self.OBMol)
        n = self.natoms
        bonds_to_del = []
        for bond in obiter:
            these_inds = [bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()]
            bonds_to_del.append(bond)
        for i in bonds_to_del:
            self.OBMol.DeleteBond(i)

    ## Converts OBMol to mol3D
    #
    #  Generally used after openbabel operations, such as when initializing a molecule from a file or FF optimizing it.
    #  @param self The object pointer
    def convert2mol3D(self):
        # initialize again
        self.initialize()
        # get elements dictionary
        elem = globalvars().elementsbynum()
        # loop over atoms
        for atom in openbabel.OBMolAtomIter(self.OBMol):
            # get coordinates
            pos = [atom.GetX(), atom.GetY(), atom.GetZ()]
            # get atomic symbol
            sym = elem[atom.GetAtomicNum() - 1]
            # add atom to molecule
            self.addAtom(atom3D(sym, [pos[0], pos[1], pos[2]]))
        # reset metal ID
        self.metal = False

    ## Converts mol3D to OBMol
    #
    #  Required for performing openbabel operations on a molecule, such as FF optimizations.
    #  @param self The object pointer
    #  @param force_clean bool force no bond info retention 
    #  @param ignoreX bool skip "X" atoms in mol3D conversion 
    def convert2OBMol(self, force_clean=False, ignoreX=False):

        # get BO matrix if exits:
        repop = False

        if not (self.OBMol == False) and not force_clean:
            BO_mat = self.populateBOMatrix()

            repop = True
        elif not (self.BO_mat == False) and not force_clean:
            BO_mat = self.BO_mat
            repop = True
            # write temp xyz
        fd, tempf = tempfile.mkstemp(suffix=".xyz")
        os.close(fd)
        # self.writexyz('tempr.xyz', symbsonly=True)
        self.writexyz(tempf, symbsonly=True, ignoreX=ignoreX)

        obConversion = openbabel.OBConversion()
        obConversion.SetInFormat("xyz")

        OBMol = openbabel.OBMol()
        obConversion.ReadFile(OBMol, tempf)

        self.OBMol = []
        self.OBMol = OBMol

        os.remove(tempf)

        if repop and not force_clean:
            self.cleanBonds()
            for i in range(0, self.natoms):
                for j in range(0, self.natoms):
                    if BO_mat[i][j] > 0:
                        self.OBMol.AddBond(i + 1, j + 1, int(BO_mat[i][j]))

    def resetBondOBMol(self):
        if self.OBMol:
            BO_mat = self.populateBOMatrix()
            self.cleanBonds()
            for i in range(0, self.natoms):
                for j in range(0, self.natoms):
                    if BO_mat[i][j] > 0:
                        self.OBMol.AddBond(i + 1, j + 1, int(BO_mat[i][j]))
        else:
            print("OBmol doies not exist")

    ## Combines two molecules
    #
    #  Each atom in the second molecule is appended to the first while preserving orders.
    #  @param self The object pointer
    #  @param mol mol3D containing molecule to be added
    #  @param list of tuples (ind1,ind2,order) bonds to add (optional)
    #  @param dirty bool set to true for straight addition, not bond safe
    #  @return mol3D contaning combined molecule
    # def combine(self,mol):
    #    cmol = self
    #    n_one = cmol.natoms
    #    n_two = mol.natoms
    #    for atom in mol.atoms:
    #        cmol.addAtom(atom)
    #    cmol.graph = []
    #    return cmol

    def combine(self, mol, bond_to_add=[], dirty=False):
        cmol = self

        if not dirty:
            # BondSafe
            cmol.convert2OBMol(force_clean=False, ignoreX=True)
            mol.convert2OBMol(force_clean=False, ignoreX=True)

            n_one = cmol.natoms
            n_two = mol.natoms
            n_tot = n_one + n_two
            # allocate
            jointBOMat = np.zeros([n_tot, n_tot])
            # get individual mats 
            con_mat_one = cmol.populateBOMatrix()
            con_mat_two = mol.populateBOMatrix()
            # combine mats
            for i in range(0, n_one):
                for j in range(0, n_one):
                    jointBOMat[i][j] = con_mat_one[i][j]
            for i in range(0, n_two):
                for j in range(0, n_two):
                    jointBOMat[i + n_one][j + n_one] = con_mat_two[i][j]
            # optional add additional bond(s)
            if bond_to_add:
                for bond_tuples in bond_to_add:
                    # print('adding bond ' + str(bond_tuples))
                    jointBOMat[bond_tuples[0], bond_tuples[1]] = bond_tuples[2]
                    jointBOMat[bond_tuples[1], bond_tuples[0]] = bond_tuples[2]
                    # jointBOMat[bond_tuples[0], bond_tuples[1]+n_one] = bond_tuples[2]
                    # jointBOMat[bond_tuples[1]+n_one, bond_tuples[0]] = bond_tuples[2]

        # add mol3Ds
        for atom in mol.atoms:
            cmol.addAtom(atom)
        if not dirty:
            cmol.convert2OBMol(ignoreX=True)
            # clean all bonds
            cmol.cleanBonds()
            # restore bond info
            for i in range(0, n_tot):
                for j in range(0, n_tot):
                    if jointBOMat[i][j] > 0:
                        cmol.OBMol.AddBond(i + 1, j + 1, int(jointBOMat[i][j]))
        # reset graph
        cmol.graph = []
        self.metal = False
        return cmol

    ## Prints coordinates of all atoms in molecule
    #  @param self The object pointer
    #  @return String containing coordinates
    def coords(self):
        # OUTPUT
        #   - atom: string with xyz-style coordinates
        ss = ''  # initialize returning string
        ss += "%d \n\n" % self.natoms
        for atom in self.atoms:
            xyz = atom.coords()
            ss += "%s \t%f\t%f\t%f\n" % (atom.sym, xyz[0], xyz[1], xyz[2])
        return ss

    ## Returns coordinates of all atoms in molecule as a list of lists
    #  @param self The object pointer
    #  @return List of all atoms in molecule
    def coordsvect(self):
        ss = []
        for atom in self.atoms:
            xyz = atom.coords()
            ss.append(xyz)
        return np.array(ss)

    def symvect(self):
        ss = []
        for atom in self.atoms:
            ss.append(atom.sym)
        return np.array(ss)

    ## Copies properties and atoms of another existing mol3D object into current mol3D object.
    #
    #  WARNING: NEVER EVER USE mol3D = mol0 to do this. It doesn't work.
    #  
    #  WARNING: ONLY USE ON A FRESH INSTANCE OF MOL3D.
    #  @param self The object pointer
    #  @param mol0 mol3D of molecule to be copied
    def copymol3D(self, mol0):
        # copy atoms
        for i, atom0 in enumerate(mol0.atoms):
            self.addAtom(atom3D(atom0.sym, atom0.coords(), atom0.name))
            if atom0.frozen:
                self.getAtom(i).frozen = True
        # copy other attributes
        self.cat = mol0.cat
        self.charge = mol0.charge
        self.denticity = mol0.denticity
        self.ident = mol0.ident
        self.ffopt = mol0.ffopt
        self.OBMol = mol0.OBMol

    ## Create molecular graph (connectivity matrix) from mol3D info
    #  @param self The object pointer
    #  @oct flag to control  special oct-metal bonds
    def createMolecularGraph(self, oct=True):
        index_set = list(range(0, self.natoms))
        A = np.zeros((self.natoms, self.natoms))
        catoms_metal = list()
        metal_ind = None
        for i in index_set:
            if oct:
                if self.getAtom(i).ismetal():
                    this_bonded_atoms = self.get_fcs()
                    metal_ind = i
                    catoms_metal = this_bonded_atoms
                    if i in this_bonded_atoms:
                        this_bonded_atoms.remove(i)
                else:
                    this_bonded_atoms = self.getBondedAtomsOct(i, debug=False)
            else:
                this_bonded_atoms = self.getBondedAtoms(i, debug=False)
            for j in index_set:
                if j in this_bonded_atoms:
                    A[i, j] = 1
        if not metal_ind == None:
            for i in index_set:
                if not i in catoms_metal:
                    A[i, metal_ind] = 0
                    A[metal_ind, i] = 0
        self.graph = A

    ## Deletes specific atom from molecule
    #
    #  Also updates mass and number of atoms, and resets the molecular graph.
    #  @param self The object pointer
    #  @param atomIdx Index of atom to be deleted
    def deleteatom(self, atomIdx):
        self.convert2OBMol()
        self.OBMol.DeleteAtom(self.OBMol.GetAtom(atomIdx + 1))
        self.mass -= self.getAtom(atomIdx).mass
        self.natoms -= 1
        self.graph = []
        self.metal = False
        del (self.atoms[atomIdx])

    ## Freezes specific atom in molecule
    #
    #  This is for the FF optimization settings.
    #  @param self The object pointer
    #  @param atomIdx Index of atom to be frozen
    def freezeatom(self, atomIdx):
        # INPUT
        #   - atomIdx: index of atom to be frozen
        self.atoms[atomIdx].frozen = True

    ## Deletes list of atoms from molecule
    #
    #  Loops over deleteatom, starting from the largest index so ordering is preserved.
    #  @param self The object pointer
    #  @param Alist List of atom indices to be deleted
    def deleteatoms(self, Alist):
        for h in sorted(Alist, reverse=True):
            self.deleteatom(h)

    ## Freezes list of atoms in molecule
    #
    #  Loops over freezeatom(), starting from the largest index so ordering is preserved.
    #  @param self The object pointer
    #  @param Alist List of atom indices to be frozen
    def freezeatoms(self, Alist):
        # INPUT
        #   - Alist: list of atoms to be frozen
        for h in sorted(Alist, reverse=True):
            self.freezeatom(h)

    ## Deletes all hydrogens from molecule.
    #
    #  Calls deleteatoms, so ordering of heavy atoms is preserved.
    #  @param self The object pointer
    def deleteHs(self):
        hlist = []
        for i in range(self.natoms):
            if self.getAtom(i).sym == 'H':
                hlist.append(i)
        self.deleteatoms(hlist)

    ## Gets distance between centers of mass of two molecules 
    #  @param self The object pointer
    #  @param mol mol3D of second molecule
    #  @return Center of mass distance
    def distance(self, mol):
        # INPUT
        #   - mol: second molecule
        # OUTPUT
        #   - pcm: distance between centers of mass
        cm0 = self.centermass()
        cm1 = mol.centermass()
        pmc = distance(cm0, cm1)
        return pmc

    def findcloseMetal(self, atom0):
        if not self.metals:
            self.findMetal()

        mm = False
        mindist = 1000
        for i in enumerate(self.metals):
            atom = self.getAtom(i)
            if distance(atom.coords(), atom0.coords()) < mindist:
                mindist = distance(atom.coords(), atom0.coords())
                mm = i
        # if no metal, find heaviest atom
        if not mm:
            maxaw = 0
            for i, atom in enumerate(self.atoms):
                if atom.atno > maxaw:
                    mm = i
        return mm

    ## Finds metal atoms in molecule
    #  @param self The object pointer
    #  @return List of indices of metal atoms
    def findMetal(self):
        if not self.metals:
            mm = []
            for i, atom in enumerate(self.atoms):
                if atom.ismetal():
                    mm.append(i)
            self.metals = mm
        return (self.metals)

    ## Finds atoms in molecule with given symbol
    #  @param self The object pointer
    #  @param sym Desired element symbol
    #  @return List of indices of atoms with given symbol
    def findAtomsbySymbol(self, sym):
        mm = []
        for i, atom in enumerate(self.atoms):
            if atom.sym == sym:
                mm.append(i)
        return mm

    ## Finds a submolecule within the molecule given the starting atom and the separating atom
    #
    #  Illustration: H2A-B-C-DH2 will return C-DH2 if C is the starting atom and B is the separating atom.
    #  
    #  Alternatively, if C is the starting atom and D is the separating atom, returns H2A-B-C.
    #  @param self The object pointer
    #  @param atom0 Index of starting atom
    #  @param atomN Index of separating atom
    #  @return List of indices of atoms in submolecule
    def findsubMol(self, atom0, atomN):
        subm = []
        conatoms = [atom0]
        conatoms += self.getBondedAtoms(atom0)  # connected atoms to atom0
        if atomN in conatoms:
            conatoms.remove(atomN)  # check for atomN and remove
        subm += conatoms  # add to submolecule
        # print('conatoms', conatoms)
        while len(conatoms) > 0:  # while list of atoms to check loop
            for atidx in subm:  # loop over initial connected atoms
                if atidx != atomN:  # check for separation atom
                    newcon = self.getBondedAtoms(atidx)
                    if atomN in newcon:
                        newcon.remove(atomN)
                    for newat in newcon:
                        if newat not in conatoms and newat not in subm:
                            conatoms.append(newat)
                            subm.append(newat)
                if atidx in conatoms:
                    conatoms.remove(atidx)  # remove from list to check
        # subm.sort()
        return subm

    ## Gets an atom with specified index
    #  @param self The object pointer
    #  @param idx Index of desired atom
    #  @return atom3D of desired atom
    def getAtom(self, idx):
        return self.atoms[idx]

    ## Gets atoms in molecule
    #  @param self The object pointer
    #  @return List of atoms in molecule
    def getAtoms(self):
        return self.atoms

    ## Gets number of unique elements in molecule
    #  @param self The object pointer
    #  @return List of symbols of unique elements in molecule
    def getAtomTypes(self):
        unique_atoms_list = list()
        for atoms in self.getAtoms():
            if atoms.symbol() not in unique_atoms_list:
                unique_atoms_list.append(atoms.symbol())
        return unique_atoms_list

    ## Gets coordinates of atom with specified index
    #  @param self The object pointer
    #  @param idx Index of desired atom
    #  @return List of coordinates of desired atom
    def getAtomCoords(self, idx):
        # print(self.printxyz())
        return self.atoms[idx].coords()

    ## Gets atoms bonded to a specific atom
    #
    #  This is determined based on BOMatrix..
    #
    #  @param self The object pointer
    #  @param ind Index of reference atom
    #  @return List of indices of bonded atoms
    def getBondedAtomsBOMatrix(self, ind, debug=False):
        ratom = self.getAtom(ind)
        self.convert2OBMol()
        OBMatrix = self.populateBOMatrix()
        # calculates adjacent number of atoms
        nats = []
        for i in range(len(OBMatrix[ind])):
            if OBMatrix[ind][i] > 0:
                nats.append(i)
        return nats

    ## Gets atoms bonded to a specific atom
    #
    #  This is determined based on augmented BOMatrix.
    #
    #  @param self The object pointer
    #  @param ind Index of reference atom
    #  @return List of indices of bonded atoms
    def getBondedAtomsBOMatrixAug(self, ind, debug=False):
        ratom = self.getAtom(ind)
        self.convert2OBMol()
        OBMatrix = self.populateBOMatrixAug()
        # calculates adjacent number of atoms
        nats = []
        for i in range(len(OBMatrix[ind])):
            if OBMatrix[ind][i] > 0:
                nats.append(i)
        return nats

    ## Gets atoms bonded to a specific atom
    #
    #  This is determined based on element-specific distance cutoffs, rather than predefined valences.
    #  
    #  This method is ideal for metals because bond orders are ill-defined.
    #
    #  For pure organics, the OBMol class provides better functionality.
    #  @param self The object pointer
    #  @param ind Index of reference atom
    #  @return List of indices of bonded atoms
    def getBondedAtoms(self, ind, debug=False):
        ratom = self.getAtom(ind)
        # calculates adjacent number of atoms
        nats = []
        for i, atom in enumerate(self.atoms):
            d = distance(ratom.coords(), atom.coords())
            distance_max = 1.15 * (atom.rad + ratom.rad)
            if atom.symbol() == "C" and not ratom.symbol() == "H":
                distance_max = min(2.75, distance_max)
            if ratom.symbol() == "C" and not atom.symbol() == "H":
                distance_max = min(2.75, distance_max)
            if ratom.symbol() == "H" and atom.ismetal:
                ## tight cutoff for metal-H bonds
                distance_max = 1.1 * (atom.rad + ratom.rad)
            if atom.symbol() == "H" and ratom.ismetal:
                ## tight cutoff for metal-H bonds
                distance_max = 1.1 * (atom.rad + ratom.rad)
            if atom.symbol() == "I" or ratom.symbol() == "I" and not (atom.symbol() == "I" and ratom.symbol() == "I"):
                distance_max = 1.05 * (atom.rad + ratom.rad)
                # print(distance_max)
            if atom.symbol() == "I" or ratom.symbol() == "I":
                distance_max = 0
            if (d < distance_max and i != ind):
                nats.append(i)
        return nats

    ## Gets atoms bonded to a specific atom with a given threshold
    #
    #  This is determined based on user-specific distance cutoffs.
    #  
    #  This method is ideal for metals because bond orders are ill-defined.
    #
    #  For pure organics, the OBMol class provides better functionality.
    #  @param self The object pointer
    #  @param ind Index of reference atom
    #  @param threshold multiplier for the sum of covalent radii cut-off
    #  @return List of indices of bonded atoms
    def getBondedAtomsByThreshold(self, ind, threshold, debug=False):
        ratom = self.getAtom(ind)
        # calculates adjacent number of atoms
        nats = []
        for i, atom in enumerate(self.atoms):
            d = distance(ratom.coords(), atom.coords())
            distance_max = threshold * (atom.rad + ratom.rad)
            if atom.symbol() == "C" and not ratom.symbol() == "H":
                distance_max = min(2.75, distance_max)
            if ratom.symbol() == "C" and not atom.symbol() == "H":
                distance_max = min(2.75, distance_max)
            if ratom.symbol() == "H" and atom.ismetal:
                ## tight cutoff for metal-H bonds
                distance_max = 1.1 * (atom.rad + ratom.rad)
            if atom.symbol() == "H" and ratom.ismetal:
                ## tight cutoff for metal-H bonds
                distance_max = 1.1 * (atom.rad + ratom.rad)
            if atom.symbol() == "I" or ratom.symbol() == "I" and not (atom.symbol() == "I" and ratom.symbol() == "I"):
                distance_max = 1.05 * (atom.rad + ratom.rad)
                # print(distance_max)
            if atom.symbol() == "I" or ratom.symbol() == "I":
                distance_max = 0
            if (d < distance_max and i != ind):
                nats.append(i)
        return nats

    ## Gets a user-specified number of atoms bonded to a specific atom
    #
    #  This is determined based on adjusting the threshold until the number of atoms specified is reached.
    #  
    #  This method is ideal for metals because bond orders are ill-defined.
    #
    #  For pure organics, the OBMol class provides better functionality.
    #  @param self The object pointer
    #  @param ind Index of reference atom
    #  @param CoordNo the number of atoms specified
    #  @return List of indices of bonded atoms
    def getBondedAtomsByCoordNo(self, ind, CoordNo, debug=False):
        ratom = self.getAtom(ind)
        # calculates adjacent number of atoms
        nats = []
        thresholds = [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8]
        for i, threshold in enumerate(thresholds):
            nats = self.getBondedAtomsByThreshold(ind, threshold)
            if len(nats) == CoordNo:
                break
        if len(nats) != CoordNo:
            print('Could not find the number of bonded atoms specified coordinated to the atom specified.')
            print('Please either adjust the number of bonded atoms or the index of the center atom.')
            print('A list of bonded atoms is still returned. Be cautious with the list')

        return nats


    ## Gets atoms bonded to a specific atom using the molecular graph, or creates it
    #
    #  @param self The object pointer
    #  @param ind Index of reference atom
    #  @param oct Flag to turn on special octahedral complex routines
    #  @return List of indices of bonded atoms
    def getBondedAtomsSmart(self, ind, oct=True, geo_check=False):
        if not len(self.graph):
            self.createMolecularGraph(oct=oct)
        return list(np.nonzero(np.ravel(self.graph[ind]))[0])

    ## Gets non-H atoms bonded to a specific atom
    #
    #  Otherwise identical to getBondedAtoms().
    #  @param self The object pointer
    #  @param ind Index of reference atom
    #  @return List of indices of bonded atoms
    def getBondedAtomsnotH(self, ind):
        ratom = self.getAtom(ind)
        # calculates adjacent number of atoms
        nats = []
        for i, atom in enumerate(self.atoms):
            d = distance(ratom.coords(), atom.coords())
            distance_max = 1.15 * (atom.rad + ratom.rad)
            if atom.ismetal() or ratom.ismetal():
                distance_max = 1.35 * (atom.rad + ratom.rad)
            else:
                distance_max = 1.15 * (atom.rad + ratom.rad)
            if (d < distance_max and i != ind and atom.sym != 'H'):
                nats.append(i)
        return nats

    ## Gets H atoms bonded to a specific atom
    #
    #  Otherwise identical to getBondedAtoms().
    #  @param self The object pointer
    #  @param ind Index of reference atom
    #  @return List of indices of bonded atoms
    def getBondedAtomsH(self, ind):
        ratom = self.getAtom(ind)
        # calculates adjacent number of atoms
        nats = []
        for i, atom in enumerate(self.atoms):
            d = distance(ratom.coords(), atom.coords())
            distance_max = 1.15 * (atom.rad + ratom.rad)
            if atom.ismetal() or ratom.ismetal():
                distance_max = 1.35 * (atom.rad + ratom.rad)
            else:
                distance_max = 1.15 * (atom.rad + ratom.rad)
            if (d < distance_max and i != ind and atom.sym == 'H'):
                nats.append(i)
        return nats

    ## Gets C=C atoms in molecule
    #  @param self The object pointer
    #  @return List of atom3D objects of H atoms
    def getC2Cs(self):
        # values to store
        c2clist = []
        c2list = []
        # self.createMolecularGraph(oct=False)
        # order c2 carbons by priority
        # c2list_and_prio = []
        # for i in range(self.natoms):
        #     if self.getAtom(i).sym == 'C' and len(self.getBondedAtoms(i)) == 3:
        #         fatnos = sorted([self.getAtom(fidx).atno for fidx in self.getBondedAtoms(i)])[::-1]
        #         fpriority = float('.'.join([str(fatnos[0]), ''.join([str(i) for i in fatnos[1:]])]))
        #         c2list_and_prio.append((fpriority, i))
        # c2 carbons
        for i in range(self.natoms):
            if self.getAtom(i).sym == 'C' and len(self.getBondedAtoms(i)) == 3:
                c2list.append(i)
        # c2list = [c2[0] for c2 in sorted(c2list_and_prio)[::-1]]
        # for each c2 carbon, find if any of its neighbors are c2
        for c2 in c2list:
            fidxes = self.getBondedAtoms(c2)
            c2partners = []
            for fidx in fidxes:
                if fidx in c2list:
                    # c2partners.append(fidx)
                    c2clist.append([c2, fidx])
                    c2clist.append([fidx, c2])
            # num_c2partners = len(c2partners)
            # # if num_c2partners > 1:
            # #     for c2partner in c2partners:
            # #         score = 0
            # #         sidxes = self.getBondedAtoms(fidx)
            # #         for sidx in sidxes:
            # # elif num_c2partners == 1:
            # if num_c2partners > 1:
            #     c2clist.append([c2, fidx],[fidx, c2])
            #     continue
            # else:
            #     continue

        return c2clist

    ## Gets atom that is furthest from the molecule COM along a given direction and returns the corresponding distance
    #  @param self The object pointer
    #  @param uP Search direction
    #  @return Distance
    def getfarAtomdir(self, uP):
        dd = 1000.0
        atomc = [0.0, 0.0, 0.0]
        for atom in self.atoms:
            d0 = distance(atom.coords(), uP)
            if d0 < dd:
                dd = d0
                atomc = atom.coords()
        return distance(self.centermass(), atomc)

    ## Gets atom that is furthest from the given atom
    #  @param self The object pointer
    #  @param reference index of reference atom
    #  @param symbol type of atom to return
    #  @return farIndex index of furthest atom

    def getFarAtom(self, reference, atomtype=False):
        referenceCoords = self.getAtom(reference).coords()
        dd = 0.00
        farIndex = reference
        for ind, atom in enumerate(self.atoms):
            allow = False
            if atomtype:
                if atom.sym == atomtype:
                    allow = True
                else:
                    allow = False

            else:
                allow = True
            d0 = distance(atom.coords(), referenceCoords)
            if d0 > dd and allow:
                dd = d0
                farIndex = ind
        return farIndex

    ## Gets list of atoms of the fragments in the mol3D
    #  @param sel The object pointer
    def getfragmentlists(self):
        atidxes_total = []
        atidxes_unique = set([0])
        atidxes_done = []
        natoms_total_ = len(atidxes_done)
        natoms_total = self.natoms
        while natoms_total_ < natoms_total:
            natoms_ = len(atidxes_unique)
            for atidx in atidxes_unique:
                if atidx not in atidxes_done:
                    atidxes_done.append(atidx)
                    atidxes = self.getBondedAtoms(atidx)
                    atidxes.extend(atidxes_unique)
                    atidxes_unique = set(atidxes)
                    natoms = len(atidxes_unique)
                    natoms_total_ = len(atidxes_done)
            if natoms_ == natoms:
                atidxes_total.append(list(atidxes_unique))
                for atidx in range(natoms_total):
                    if atidx not in atidxes_done:
                        atidxes_unique = set([atidx])
                        natoms_total_ = len(atidxes_done)
                        break

        return atidxes_total

    ## Gets H atoms in molecule
    #  @param self The object pointer
    #  @return List of atom3D objects of H atoms
    def getHs(self):
        hlist = []
        for i in range(self.natoms):
            if self.getAtom(i).sym == 'H':
                hlist.append(i)
        return hlist

    ## Gets H atoms bonded to specific atom3D in molecule
    #  @param self The object pointer
    #  @param ratom atom3D of reference atom
    #  @return List of atom3D objects of H atoms
    def getHsbyAtom(self, ratom):
        nHs = []
        for i, atom in enumerate(self.atoms):
            if atom.sym == 'H':
                d = distance(ratom.coords(), atom.coords())
                if (d < 1.2 * (atom.rad + ratom.rad) and d > 0.01):
                    nHs.append(i)
        return nHs

    ## Gets H atoms bonded to specific atom index in molecule
    #
    #  Trivially equivalent to getHsbyAtom().
    #  @param self The object pointer
    #  @param idx Index of reference atom
    #  @return List of atom3D objects of H atoms
    def getHsbyIndex(self, idx):
        # calculates adjacent number of hydrogens
        nHs = []
        for i, atom in enumerate(self.atoms):
            if atom.sym == 'H':
                d = distance(atom.coords(), self.getAtom(idx).coords())
                if (d < 1.2 * (atom.rad + self.getAtom(idx).rad) and d > 0.01):
                    nHs.append(i)
        return nHs

    ## Gets index of closest atom to reference atom.
    #  @param self The object pointer
    #  @param atom0 Index of reference atom
    #  @return Index of closest atom
    def getClosestAtom(self, atom0):
        # INPUT
        #   - atom0: reference atom3D
        # OUTPUT
        #   - idx: index of closest atom to atom0 from molecule
        idx = 0
        cdist = 1000
        for iat, atom in enumerate(self.atoms):
            ds = atom.distance(atom0)
            if (ds < cdist):
                idx = iat
                cdist = ds
        return idx

    def getClosestAtomlist(self, atom_idx, cdist=3):
        # INPUT
        #   - atom_index: reference atom index
        #   - cdist: cutoff of neighbor distance
        # OUTPUT
        #   - neighbor_list: index of close atom to atom0 from molecule
        neighbor_list = []
        for iat, atom in enumerate(self.atoms):
            ds = atom.distance(self.atoms[atom_idx])
            if (ds < cdist):
                neighbor_list.append(neighbor_list)
        return neighbor_list

    ## Gets point that corresponds to mask
    #  @param self The object pointer    
    #  @param mask Identifier for atoms
    #  @return Center of mass of mask
    def getMask(self, mask):
        globs = globalvars()
        elements = globs.elementsbynum()
        # check center of mass
        ats = []
        # loop over entries in mask
        for entry in mask:
            # check for center of mass
            if ('com' in entry.lower()) or ('cm' in entry.lower()):
                return self.centermass()
            # check for range
            elif '-' in entry:
                at0 = entry.split('-')[0]
                at1 = entry.split('-')[-1]
                for i in range(int(at0), int(at1) + 1):
                    ats.append(i - 1)  # python indexing
            elif entry in elements:
                ats += self.findAtomsbySymbol(entry)
            else:
                # try to convert to integer
                try:
                    t = int(entry)
                    ats.append(t - 1)
                except:
                    return self.centermass()
        maux = mol3D()
        for at in ats:
            maux.addAtom(self.getAtom(at))
        if maux.natoms == 0:
            return self.centermass()
        else:
            return maux.centermass()

    ## Gets index of closest non-H atom to another atom
    #  @param self The object pointer    
    #  @param atom0 atom3D of reference atom
    #  @return Index of closest non-H atom
    def getClosestAtomnoHs(self, atom0):
        idx = 0
        cdist = 1000
        for iat, atom in enumerate(self.atoms):
            ds = atom.distance(atom0)
            if (ds < cdist) and atom.sym != 'H':
                idx = iat
                cdist = ds
        return idx

    ## Gets distance between two atoms in molecule
    #  @param self The object pointer    
    #  @param idx Index of first atom
    #  @param metalx Index of second atom
    #  @return Distance between atoms
    def getDistToMetal(self, idx, metalx):
        d = self.getAtom(idx).distance(self.getAtom(metalx))
        return d

    ## Gets index of closest non-H atom to another atom
    #
    #  Equivalent to getClosestAtomnoHs() except that the index of the reference atom is specified.
    #  @param self The object pointer    
    #  @param atidx Index of reference atom
    #  @return Index of closest non-H atom
    def getClosestAtomnoHs2(self, atidx):
        idx = 0
        cdist = 1000
        for iat, atom in enumerate(self.atoms):
            ds = atom.distance(self.getAtom(atidx))
            if (ds < cdist) and atom.sym != 'H' and iat != atidx:
                idx = iat
                cdist = ds
        return idx

    ## Initializes OBMol object from a file or SMILES string

    #
    #  Uses the obConversion tool and for files containing 3D coordinates (xyz,mol) and the OBBuilder tool otherwise (smiles).
    #  @param self The object pointer    
    #  @param fst Name of input file
    #  @param convtype Input filetype (xyz,mol,smi)
    #  @param ffclean Flag for FF cleanup of generated structure (default False)
    #  @return OBMol object
    def getOBMol(self, fst, convtype, ffclean=False):
        obConversion = openbabel.OBConversion()
        OBMol = openbabel.OBMol()
        if convtype == 'smistring':
            obConversion.SetInFormat('smi')
            obConversion.ReadString(OBMol, fst)
        else:
            obConversion.SetInFormat(convtype[:-1])
            obConversion.ReadFile(OBMol, fst)
        if 'smi' in convtype:
            OBMol.AddHydrogens()
            b = openbabel.OBBuilder()
            b.Build(OBMol)
        if ffclean:
            forcefield = openbabel.OBForceField.FindForceField('mmff94')
            forcefield.Setup(OBMol)
            forcefield.ConjugateGradients(200)
            forcefield.GetCoordinates(OBMol)
        self.OBMol = OBMol
        return OBMol

    ## Removes attributes from mol3D object
    #  @param self The object pointer    
    def initialize(self):
        self.atoms = []
        self.natoms = 0
        self.mass = 0
        self.size = 0
        self.graph = []

    ## Calculates the largest distance between atoms of two molecules
    #  @param self The object pointer    
    #  @param mol mol3D of second molecule
    #  @return Largest distance between atoms in both molecules
    def maxdist(self, mol):
        # INPUT
        #   - mol: second molecule
        # OUTPUT
        #   - maxd: maximum distance between atoms of the 2 molecules
        maxd = 0
        for atom1 in mol.atoms:
            for atom0 in self.atoms:
                if (distance(atom1.coords(), atom0.coords()) > maxd):
                    maxd = distance(atom1.coords(), atom0.coords())
        return maxd

    ## Calculates the smallest distance between atoms of two molecules
    #  @param self The object pointer    
    #  @param mol mol3D of second molecule
    #  @return Smallest distance between atoms in both molecules
    def mindist(self, mol):
        # INPUT
        #   - mol: second molecule
        # OUTPUT
        #   - mind: minimum distance between atoms of the 2 molecules
        mind = 1000
        for atom1 in mol.atoms:
            for atom0 in self.atoms:
                if (distance(atom1.coords(), atom0.coords()) < mind):
                    mind = distance(atom1.coords(), atom0.coords())
        return mind

    ## Calculates the smallest distance between atoms in the molecule
    #  @param self The object pointer    
    #  @return Smallest distance between atoms in molecule
    def mindistmol(self):
        mind = 1000
        for ii, atom1 in enumerate(self.atoms):
            for jj, atom0 in enumerate(self.atoms):
                d = distance(atom1.coords(), atom0.coords())
                if (d < mind) and ii != jj:
                    mind = distance(atom1.coords(), atom0.coords())
        return mind

    ## Calculates the smallest distance from atoms in the molecule to a given point
    #  @param self The object pointer   
    #  @param point List of coordinates of reference point 
    #  @return Smallest distance to point
    def mindisttopoint(self, point):
        mind = 1000
        for atom1 in self.atoms:
            d = distance(atom1.coords(), point)
            if (d < mind):
                mind = d
        return mind

    ## Calculates the smallest distance between non-H atoms of two molecules
    # 
    #  Otherwise equivalent to mindist().
    #  @param self The object pointer    
    #  @param mol mol3D of second molecule
    #  @return Smallest distance between non-H atoms in both molecules
    def mindistnonH(self, mol):
        mind = 1000
        for atom1 in mol.atoms:
            for atom0 in self.atoms:
                if (distance(atom1.coords(), atom0.coords()) < mind):
                    if (atom1.sym != 'H' and atom0.sym != 'H'):
                        mind = distance(atom1.coords(), atom0.coords())
        return mind

    ## Calculates the size of the molecule, as quantified by the max. distance between atoms and the COM.
    #  @param self The object pointer    
    #  @return Molecule size (max. distance between atoms and COM)
    def molsize(self):
        maxd = 0
        cm = self.centermass()
        for atom in self.atoms:
            if distance(cm, atom.coords()) > maxd:
                maxd = distance(cm, atom.coords())
        return maxd

    ## Checks for overlap with another molecule
    # 
    #  Compares pairwise atom distances to 0.85*sum of covalent radii
    #  @param self The object pointer    
    #  @param mol mol3D of second molecule
    #  @param silence Flag for printing warnings
    #  @return Flag for overlap
    def overlapcheck(self, mol, silence):
        overlap = False
        for atom1 in mol.atoms:
            for atom0 in self.atoms:
                if (distance(atom1.coords(), atom0.coords()) < 0.85 * (atom1.rad + atom0.rad)):
                    overlap = True
                    if not (silence):
                        print()
                        "#############################################################"
                        print()
                        "!!!Molecules might be overlapping. Increase distance!!!"
                        print()
                        "#############################################################"
                    break
        return overlap

    ## Checks for overlap with another molecule with increased tolerance
    # 
    #  Compares pairwise atom distances to 1*sum of covalent radii
    #  @param self The object pointer
    #  @param mol mol3D of second molecule
    #  @return Flag for overlap
    def overlapcheckh(self, mol):
        overlap = False
        for atom1 in mol.atoms:
            for atom0 in self.atoms:
                if (distance(atom1.coords(), atom0.coords()) < 1.0):
                    overlap = True
                    break
        return overlap

    ## Gets a matrix with bond orders from openbabel
    #  @param self The object pointer
    #  @return matrix of bond orders
    def populateBOMatrix(self):
        obiter = openbabel.OBMolBondIter(self.OBMol)
        n = self.natoms
        molBOMat = np.zeros((n, n))
        for bond in obiter:
            these_inds = [bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()]
            this_order = bond.GetBondOrder()
            molBOMat[these_inds[0] - 1, these_inds[1] - 1] = this_order
            molBOMat[these_inds[1] - 1, these_inds[0] - 1] = this_order
        return (molBOMat)

    ## Gets a matrix with bond orders from openbabel augmented with molecular graph
    #  @param self The object pointer
    #  @return matrix of bond orders
    def populateBOMatrixAug(self):
        obiter = openbabel.OBMolBondIter(self.OBMol)
        n = self.natoms
        molBOMat = np.zeros((n, n))
        for bond in obiter:
            these_inds = [bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()]
            this_order = bond.GetBondOrder()
            molBOMat[these_inds[0] - 1, these_inds[1] - 1] = this_order
            molBOMat[these_inds[1] - 1, these_inds[0] - 1] = this_order
        self.convert2mol3D()
        self.createMolecularGraph()
        molgraph = self.graph
        error_mat = molBOMat - molgraph
        error_idx = np.where(error_mat < 0)
        for i in range(len(error_idx)):
            if len(error_idx[i]) > 0:
                molBOMat[error_idx[i].tolist()[0], error_idx[i].tolist()[1]] = 1
        return (molBOMat)

    ## Prints xyz coordinates to stdout
    # 
    #  To write to file (more common), use writexyz() instead.
    #  @param self The object pointer
    def printxyz(self):
        for atom in self.atoms:
            xyz = atom.coords()
            ss = "%s \t%f\t%f\t%f\n" % (atom.sym, xyz[0], xyz[1], xyz[2])

            print(ss)

    ## returns string of xyz coordinates
    # 
    #  To write to file (more common), use writexyz() instead.
    #  @param self The object pointer
    def returnxyz(self):
        ss = ''
        for atom in self.atoms:
            xyz = atom.coords()
            ss += "%s \t%f\t%f\t%f\n" % (atom.sym, xyz[0], xyz[1], xyz[2])
        return (ss)

    ## Load molecule from xyz file
    # 
    #  Consider using getOBMol, which is more general, instead.
    #  @param self The object pointer    
    #  @param filename Filename
    def readfromxyz(self, filename):
        # print('!!!!', filename)
        globs = globalvars()
        amassdict = globs.amass()
        self.graph = []
        self.xyzfile = filename
        fname = filename.split('.xyz')[0]
        f = open(fname + '.xyz', 'r')
        s = f.read().splitlines()
        f.close()
        for line in s[2:]:
            line_split = line.split()
            if len(line_split) == 4 and line_split[0]:
                # this looks for unique atom IDs in files
                lm = re.search(r'\d+$', line_split[0])
                # if the string ends in digits m will be a Match object, or None otherwise.
                if lm is not None:
                    symb = re.sub('\d+', '', line_split[0])
                    number = lm.group()
                    # print('sym and number ' +str(symb) + ' ' + str(number))
                    globs = globalvars()
                    atom = atom3D(symb, [float(line_split[1]), float(line_split[2]), float(line_split[3])],
                                  name=line_split[0])
                elif line_split[0] in list(amassdict.keys()):
                    atom = atom3D(line_split[0], [float(line_split[1]), float(line_split[2]), float(line_split[3])])
                else:
                    print('cannot find atom type')
                    sys.exit()
                self.addAtom(atom)

    def readfromtxt(self, txt):
        # print('!!!!', filename)
        globs = globalvars()
        en_dict = globs.endict()
        self.graph = []
        for line in txt:
            line_split = line.split()
            if len(line_split) == 4 and line_split[0]:
                # this looks for unique atom IDs in files
                lm = re.search(r'\d+$', line_split[0])
                # if the string ends in digits m will be a Match object, or None otherwise.
                if lm is not None:
                    symb = re.sub('\d+', '', line_split[0])
                    # number = lm.group()
                    # # print('sym and number ' +str(symb) + ' ' + str(number))
                    # globs = globalvars()
                    atom = atom3D(symb, [float(line_split[1]), float(line_split[2]), float(line_split[3])],
                                  name=line_split[0])
                elif line_split[0] in list(en_dict.keys()):
                    atom = atom3D(line_split[0], [float(line_split[1]), float(line_split[2]), float(line_split[3])])
                else:
                    print('cannot find atom type')
                    sys.exit()
                self.addAtom(atom)

    ## Computes RMSD between two molecules
    # 
    #  Note that this routine does not perform translations or rotations to align molecules.
    #
    #  To do so, use geometry.kabsch().
    #  @param self The object pointer  
    #  @param mol2 mol3D of second molecule
    #  @return RMSD between molecules, NaN if molecules have different numbers of atoms
    def rmsd(self, mol2):
        Nat0 = self.natoms
        Nat1 = mol2.natoms
        if (Nat0 != Nat1):
            print("ERROR: RMSD can be calculated only for molecules with the same number of atoms..")
            return float('NaN')
        else:
            rmsd = 0
            for atom0, atom1 in zip(self.getAtoms(), mol2.getAtoms()):
                rmsd += (atom0.distance(atom1)) ** 2
            if Nat0 == 0:
                rmsd = 0
            else:
                rmsd /= Nat0
            return sqrt(rmsd)

    ## Checks for overlap within the molecule
    # 
    #  Single-molecule version of overlapcheck().
    #  @param self The object pointer      
    #  @param silence Flag for printing warning
    #  @return Flag for overlap
    #  @return Minimum distance between atoms
    def sanitycheck(self, silence=False):
        overlap = False
        mind = 1000
        for ii, atom1 in enumerate(self.atoms):
            for jj, atom0 in enumerate(self.atoms):
                if ii != jj and (distance(atom1.coords(), atom0.coords()) < 0.7 * (atom1.rad + atom0.rad)):
                    overlap = True
                    if distance(atom1.coords(), atom0.coords()) < mind:
                        mind = distance(atom1.coords(), atom0.coords())
                    if not (silence):
                        print("#############################################################")
                        print("Molecules might be overlapping. Increase distance!")
                        print("#############################################################")
                    break
        return overlap, mind

    ## Translate all atoms by given vector.
    #  @param self The object pointer      
    #  @param dxyz Translation vector
    def translate(self, dxyz):
        for atom in self.atoms:
            atom.translate(dxyz)

    ## Writes xyz file in GAMESS format
    #  @param self The object pointer   
    #  @param filename Filename    
    def writegxyz(self, filename):
        ss = ''  # initialize returning string
        ss += "Date:" + time.strftime(
            '%m/%d/%Y %H:%M') + ", XYZ structure generated by mol3D Class, " + self.globs.PROGRAM + "\nC1\n"
        for atom in self.atoms:
            xyz = atom.coords()
            ss += "%s \t%.1f\t%f\t%f\t%f\n" % (atom.sym, float(atom.atno), xyz[0], xyz[1], xyz[2])
        fname = filename.split('.gxyz')[0]
        f = open(fname + '.gxyz', 'w')
        f.write(ss)
        f.close()

    ## Writes xyz file
    #
    #  To print to stdout instead, use printxyz().
    #  @param self The object pointer   
    #  @param filename Filename  
    def writexyz(self, filename, symbsonly=False, ignoreX=False):
        ss = ''  # initialize returning string
        natoms = self.natoms
        if ignoreX:
            natoms -= sum([1 for i in self.atoms if i.sym == "X"])

        ss += str(natoms) + "\n" + time.strftime(
            '%m/%d/%Y %H:%M') + ", XYZ structure generated by mol3D Class, " + self.globs.PROGRAM + "\n"
        for atom in self.atoms:
            if not (ignoreX and atom.sym == 'X'):
                xyz = atom.coords()
                if symbsonly:
                    ss += "%s \t%f\t%f\t%f\n" % (atom.sym, xyz[0], xyz[1], xyz[2])
                else:
                    ss += "%s \t%f\t%f\t%f\n" % (atom.name, xyz[0], xyz[1], xyz[2])
        fname = filename.split('.xyz')[0]
        f = open(fname + '.xyz', 'w')
        f.write(ss)
        f.close()

    ## Writes xyz file for 2 molecules combined
    #
    #  Used when placing binding molecules.
    #  @param self The object pointer   
    #  @param mol mol3D of second molecule    
    #  @param filename Filename  
    def writemxyz(self, mol, filename):
        ss = ''  # initialize returning string
        ss += str(self.natoms + mol.natoms) + "\n" + time.strftime(
            '%m/%d/%Y %H:%M') + ", XYZ structure generated by mol3D Class, " + self.globs.PROGRAM + "\n"
        for atom in self.atoms:
            xyz = atom.coords()
            ss += "%s \t%f\t%f\t%f\n" % (atom.sym, xyz[0], xyz[1], xyz[2])
        for atom in mol.atoms:
            xyz = atom.coords()
            ss += "%s \t%f\t%f\t%f\n" % (atom.sym, xyz[0], xyz[1], xyz[2])
        fname = filename.split('.xyz')[0]
        f = open(fname + '.xyz', 'w')
        f.write(ss)
        f.close()

    ## Writes xyz file with atom numbers
    #
    #  some codes require this, e.g. packmol/moltemplate.
    #  @param self The object pointer   
    #  @param filename Filename  
    def writenumberedxyz(self, filename):
        ss = ''  # initialize returning string
        ss += str(self.natoms) + "\n" + time.strftime(
            '%m/%d/%Y %H:%M') + ", XYZ structure generated by mol3D Class, " + self.globs.PROGRAM + "\n"
        unique_types = dict()

        for atom in self.atoms:
            this_sym = atom.symbol()
            if not this_sym in list(unique_types.keys()):
                unique_types.update({this_sym: 1})
            else:
                unique_types.update({this_sym: unique_types[this_sym] + 1})
            atom_name = str(atom.symbol()) + str(unique_types[this_sym])
            xyz = atom.coords()
            ss += "%s \t%f\t%f\t%f\n" % (atom_name, xyz[0], xyz[1], xyz[2])
        fname = filename.split('.xyz')[0]
        f = open(fname + '.xyz', 'w')
        f.write(ss)
        f.close()

    ## Writes xyz file for 2 molecules separated
    #
    #  Used when placing binding molecules for computation of binding energy.
    #  @param self The object pointer   
    #  @param mol mol3D of second molecule    
    #  @param filename Filename  
    def writesepxyz(self, mol, filename):
        ss = ''  # initialize returning string
        ss += str(self.natoms) + "\n" + time.strftime(
            '%m/%d/%Y %H:%M') + ", XYZ structure generated by mol3D Class, " + self.globs.PROGRAM + "\n"
        for atom in self.atoms:
            xyz = atom.coords()
            ss += "%s \t%f\t%f\t%f\n" % (atom.sym, xyz[0], xyz[1], xyz[2])
        ss += "--\n" + str(mol.natoms) + "\n\n"
        for atom in mol.atoms:
            xyz = atom.coords()
            ss += "%s \t%f\t%f\t%f\n" % (atom.sym, xyz[0], xyz[1], xyz[2])
        fname = filename.split('.xyz')[0]
        f = open(fname + '.xyz', 'w')
        f.write(ss)
        f.close()

    def closest_H_2_metal(self, delta=0):
        min_dist_H = 3.0
        min_dist_nonH = 3.0
        for i, atom in enumerate(self.atoms):
            if atom.ismetal():
                metal_atom = atom
                break
        metal_coord = metal_atom.coords()
        for atom1 in self.atoms:
            if atom1.sym == 'H':
                if distance(atom1.coords(), metal_coord) < min_dist_H:
                    min_dist_H = distance(atom1.coords(), metal_coord)
            elif not atom1.ismetal():
                if distance(atom1.coords(), metal_coord) < min_dist_nonH:
                    min_dist_nonH = distance(atom1.coords(), metal_coord)
        if min_dist_H <= (min_dist_nonH - delta):
            flag = True
        else:
            flag = False
        return (flag, min_dist_H, min_dist_nonH)

    ## Print methods
    #  @param self The object pointer   
    #  @return String with methods
    def __repr__(self):
        # OUTPUT
        #   - ss: string with all methods
        # overloaded function
        """ when calls mol3D object without attribute e.g. t """
        ss = "\nClass mol3D has the following methods:\n"
        for method in dir(self):
            if callable(getattr(self, method)):
                ss += method + '\n'
        return ss

    def create_mol_with_inds(self, inds):
        molnew = mol3D()
        for ind in inds:
            atom = atom3D(self.atoms[ind].symbol(), self.atoms[ind].coords())
            molnew.addAtom(atom)
        return molnew

    ## Writes a psueduo-chemical formula
    #
    #  @param self The object pointer   
    def make_formula(self):
        retstr = ""
        atomorder = self.globs.elementsbynum()
        unique_symbols = dict()
        for atoms in self.getAtoms():
            if atoms.symbol() in atomorder:
                if not atoms.symbol() in unique_symbols.keys():
                    unique_symbols[atoms.symbol()] = 1
                else:
                    unique_symbols[atoms.symbol()] = unique_symbols[atoms.symbol()] + 1
        skeys = sorted(list(unique_symbols.keys()), key=lambda x: (self.globs.elementsbynum().index(x)))
        skeys = skeys[::-1]
        for sk in skeys:
            retstr += '\\textrm{' + sk + '}_{' + str(int(unique_symbols[sk])) + '}'
        return retstr

    def read_smiles(self, smiles, ff="mmff94", steps=2500):
        # used to convert from one formst (ex, SMILES) to another (ex, mol3D ) 
        obConversion = openbabel.OBConversion()

        # the input format "SMILES"; Reads the SMILES - all stacked as 2-D - one on top of the other
        obConversion.SetInFormat("SMILES")
        OBMol = openbabel.OBMol()
        obConversion.ReadString(OBMol, smiles)

        # Adds Hydrogens
        OBMol.AddHydrogens()

        # Get a 3-D structure with H's
        builder = openbabel.OBBuilder()
        builder.Build(OBMol)

        # Force field optimization is done in the specified number of "steps" using the specified "ff" force field
#        forcefield = openbabel.OBForceField.FindForceField(ff)
#        s = forcefield.Setup(OBMol)
#        if s == False:
#            print('FF setup failed')
#        forcefield.ConjugateGradients(steps)
#        forcefield.GetCoordinates(OBMol)

        # mol3D structure
        self.OBMol = OBMol
        self.convert2mol3D()

    def mols_symbols(self):
        self.symbols_dict = {}
        for atom in self.getAtoms():
            if not atom.symbol() in self.symbols_dict:
                self.symbols_dict.update({atom.symbol(): 1})
            else:
                self.symbols_dict[atom.symbol()] += 1
