Ejemplo n.º 1
0
def calculateBondSymmetryNumber(molecule, atom1, atom2):
    """
    Return the symmetry number centered at `bond` in the structure.
    """
    bond = atom1.edges[atom2]
    symmetryNumber = 1
    if bond.isSingle() or bond.isDouble() or bond.isTriple():
        if atom1.equivalent(atom2):
            # An O-O bond is considered to be an "optical isomer" and so no
            # symmetry correction will be applied
            if atom1.atomType.label == 'Os' and atom2.atomType.label == 'Os' and atom1.radicalElectrons == atom2.radicalElectrons == 0:
                return symmetryNumber
            # If the molecule is diatomic, then we don't have to check the
            # ligands on the two atoms in this bond (since we know there
            # aren't any)
            elif len(molecule.vertices) == 2:
                symmetryNumber = 2
            else:
                molecule.removeBond(bond)
                structure = molecule.copy(True)
                molecule.addBond(bond)

                atom1 = structure.atoms[molecule.atoms.index(atom1)]
                atom2 = structure.atoms[molecule.atoms.index(atom2)]
                fragments = structure.split()
                
                if len(fragments) != 2: return symmetryNumber

                fragment1, fragment2 = fragments
                if atom1 in fragment1.atoms: fragment1.removeAtom(atom1)
                if atom2 in fragment1.atoms: fragment1.removeAtom(atom2)
                if atom1 in fragment2.atoms: fragment2.removeAtom(atom1)
                if atom2 in fragment2.atoms: fragment2.removeAtom(atom2)
                groups1 = fragment1.split()
                groups2 = fragment2.split()

                # Test functional groups for symmetry
                if len(groups1) == len(groups2) == 1:
                    if groups1[0].isIsomorphic(groups2[0]): symmetryNumber *= 2
                elif len(groups1) == len(groups2) == 2:
                    if groups1[0].isIsomorphic(groups2[0]) and groups1[1].isIsomorphic(groups2[1]): symmetryNumber *= 2
                    elif groups1[1].isIsomorphic(groups2[0]) and groups1[0].isIsomorphic(groups2[1]): symmetryNumber *= 2
                elif len(groups1) == len(groups2) == 3:
                    if groups1[0].isIsomorphic(groups2[0]) and groups1[1].isIsomorphic(groups2[1]) and groups1[2].isIsomorphic(groups2[2]): symmetryNumber *= 2
                    elif groups1[0].isIsomorphic(groups2[0]) and groups1[1].isIsomorphic(groups2[2]) and groups1[2].isIsomorphic(groups2[1]): symmetryNumber *= 2
                    elif groups1[0].isIsomorphic(groups2[1]) and groups1[1].isIsomorphic(groups2[2]) and groups1[2].isIsomorphic(groups2[0]): symmetryNumber *= 2
                    elif groups1[0].isIsomorphic(groups2[1]) and groups1[1].isIsomorphic(groups2[0]) and groups1[2].isIsomorphic(groups2[2]): symmetryNumber *= 2
                    elif groups1[0].isIsomorphic(groups2[2]) and groups1[1].isIsomorphic(groups2[0]) and groups1[2].isIsomorphic(groups2[1]): symmetryNumber *= 2
                    elif groups1[0].isIsomorphic(groups2[2]) and groups1[1].isIsomorphic(groups2[1]) and groups1[2].isIsomorphic(groups2[0]): symmetryNumber *= 2
                
                
    return symmetryNumber
Ejemplo n.º 2
0
    def estimateRadicalThermoViaHBI(self, molecule, stableThermoEstimator ):
        """
        Estimate the thermodynamics of a radical by saturating it,
        applying the provided stableThermoEstimator method on the saturated species,
        then applying hydrogen bond increment corrections for the radical
        site(s) and correcting for the symmetry.
        
        The stableThermoEstimator should NOT have already corrected for the symmetry of the 
        stable saturated molecule, because we do not "uncorrect" it. 
        I.e. stableThermoEstimator should be a method that overestimates the entropy by R*ln(symmetry).
        """
        #TODO: check the validity of the above statement for QMThermo and databases.
        
        assert molecule.isRadical(), "Method only valid for radicals."
        
        # Make a copy of the structure so we don't change the original
        saturatedStruct = molecule.copy(deep=True)
        
        # Saturate structure by replacing all radicals with bonds to
        # hydrogen atoms
        added = {}
        for atom in saturatedStruct.atoms:
            for i in range(atom.radicalElectrons):
                H = Atom('H')
                bond = Bond(atom, H, 'S')
                saturatedStruct.addAtom(H)
                saturatedStruct.addBond(bond)
                if atom not in added:
                    added[atom] = []
                added[atom].append([H, bond])
                atom.decrementRadical()

        # Update the atom types of the saturated structure (not sure why
        # this is necessary, because saturating with H shouldn't be
        # changing atom types, but it doesn't hurt anything and is not
        # very expensive, so will do it anyway)
        saturatedStruct.updateConnectivityValues()
        saturatedStruct.sortVertices()
        saturatedStruct.updateAtomTypes()
        saturatedStruct.updateLonePairs()
        saturatedStruct.multiplicity = 1
        
        # Get thermo estimate for saturated form of structure
        try:
            thermoData = stableThermoEstimator(saturatedStruct)
        except AttributeError:
            # Probably looking for thermo in a library 
            saturatedSpec = Species(molecule=[saturatedStruct])
            thermoData = stableThermoEstimator(saturatedSpec)
            if thermoData:
                assert len(thermoData) == 3, "thermoData should be a tuple at this point: (thermoData, library, entry)"
                thermoData = thermoData[0]
        if thermoData is None:
            # logging.info("Thermo data of saturated {0} of molecule {1} is None.".format(saturatedStruct, molecule))
            return None
        assert thermoData is not None, "Thermo data of saturated {0} of molecule {1} is None!".format(saturatedStruct, molecule)
        
        # Correct entropy for symmetry number of radical structure
        molecule.calculateSymmetryNumber()
        thermoData.S298.value_si -= constants.R * math.log(molecule.symmetryNumber)
        
        # For each radical site, get radical correction
        # Only one radical site should be considered at a time; all others
        # should be saturated with hydrogen atoms
        for atom in added:
            # Remove the added hydrogen atoms and bond and restore the radical
            for H, bond in added[atom]:
                saturatedStruct.removeBond(bond)
                saturatedStruct.removeAtom(H)
                atom.incrementRadical()
            saturatedStruct.updateConnectivityValues()
            try:
                self.__addGroupThermoData(thermoData, self.groups['radical'], saturatedStruct, {'*':atom})
            except KeyError:
                logging.error("Couldn't find in radical thermo database:")
                logging.error(molecule)
                logging.error(molecule.toAdjacencyList())
                raise
            # Re-saturate
            for H, bond in added[atom]:
                saturatedStruct.addAtom(H)
                saturatedStruct.addBond(bond)
                atom.decrementRadical()
            # Subtract the enthalpy of the added hydrogens
            for H, bond in added[atom]:
                thermoData.H298.value_si -= 52.103 * 4184

        return thermoData
Ejemplo n.º 3
0
    def estimateRadicalThermoViaHBI(self, molecule, stableThermoEstimator ):
        """
        Estimate the thermodynamics of a radical by saturating it,
        applying the provided stableThermoEstimator method on the saturated species,
        then applying hydrogen bond increment corrections for the radical
        site(s) and correcting for the symmetry.
        """
        assert molecule.getRadicalCount() > 0, "Method only valid for radicals."
        
        # Make a copy of the structure so we don't change the original
        saturatedStruct = molecule.copy(deep=True)
        
        # Saturate structure by replacing all radicals with bonds to
        # hydrogen atoms
        added = {}
        for atom in saturatedStruct.atoms:
            for i in range(atom.radicalElectrons):
                H = Atom('H')
                bond = Bond(atom, H, 'S')
                saturatedStruct.addAtom(H)
                saturatedStruct.addBond(bond)
                if atom not in added:
                    added[atom] = []
                added[atom].append([H, bond])
                atom.decrementRadical()
        
        # Update the atom types of the saturated structure (not sure why
        # this is necessary, because saturating with H shouldn't be
        # changing atom types, but it doesn't hurt anything and is not
        # very expensive, so will do it anyway)
        saturatedStruct.updateConnectivityValues()
        saturatedStruct.sortVertices()
        saturatedStruct.updateAtomTypes()
        
        # Get thermo estimate for saturated form of structure
        thermoData = stableThermoEstimator(saturatedStruct)
        if thermoData is None:
            logging.info("Thermo data of saturated {0} of molecule {1} is None.".format(saturatedStruct, molecule))
            return None
        assert thermoData is not None, "Thermo data of saturated {0} of molecule {1} is None!".format(saturatedStruct, molecule)
        
        # Undo symmetry number correction for saturated structure
        saturatedStruct.calculateSymmetryNumber()
        thermoData.S298.value_si += constants.R * math.log(saturatedStruct.symmetryNumber)
        # Correct entropy for symmetry number of radical structure
        molecule.calculateSymmetryNumber()
        thermoData.S298.value_si -= constants.R * math.log(molecule.symmetryNumber)
        
        # For each radical site, get radical correction
        # Only one radical site should be considered at a time; all others
        # should be saturated with hydrogen atoms
        for atom in added:
            # Remove the added hydrogen atoms and bond and restore the radical
            for H, bond in added[atom]:
                saturatedStruct.removeBond(bond)
                saturatedStruct.removeAtom(H)
                atom.incrementRadical()
            saturatedStruct.updateConnectivityValues()
            try:
                self.__addGroupThermoData(thermoData, self.groups['radical'], saturatedStruct, {'*':atom})
            except KeyError:
                logging.error("Couldn't find in radical thermo database:")
                logging.error(molecule)
                logging.error(molecule.toAdjacencyList())
                raise
            # Re-saturate
            for H, bond in added[atom]:
                saturatedStruct.addAtom(H)
                saturatedStruct.addBond(bond)
                atom.decrementRadical()
            # Subtract the enthalpy of the added hydrogens
            for H, bond in added[atom]:
                thermoData.H298.value_si -= 52.103 * 4184

        return thermoData
Ejemplo n.º 4
0
def calculateCyclicSymmetryNumber(molecule):
    """
    Get the symmetry number correction for cyclic regions of a molecule.
    For complicated fused rings the smallest set of smallest rings is used.
    """
    from rdkit.Chem.rdmolops import SanitizeMol
    from rdkit.Chem.rdchem import Mol

    symmetryNumber = 1

    rings = molecule.getSmallestSetOfSmallestRings()

    # Get symmetry number for each ring in structure
    for ring0 in rings:

        # Make another copy structure
        structure = molecule.copy(True)
        ring = [structure.atoms[molecule.atoms.index(atom)] for atom in ring0]

        # Remove bonds of ring from structure
        for i, atom1 in enumerate(ring):
            for atom2 in ring[i + 1 :]:
                if structure.hasBond(atom1, atom2):
                    structure.removeBond(atom1.edges[atom2])

        structures = structure.split()
        groups = []
        for struct in structures:
            for atom in ring:
                if struct.hasAtom(atom):
                    struct.removeAtom(atom)
            groups.append(struct.split())
        # Find equivalent functional groups on ring
        equivalentGroups = []
        equivalentGroupCount = []
        for group in groups:
            found = False
            for i, eqGroup in enumerate(equivalentGroups):
                if not found and len(group) == len(eqGroup):
                    for g, eg in zip(group, eqGroup):
                        if not g.isIsomorphic(eg):
                            # The groups do not match
                            break
                    else:
                        # The groups match
                        found = True
                if found:
                    # We've found a matching group, so increment its count
                    equivalentGroupCount[i] += 1
                    break
            else:
                # No matching group found, so add it as a new group
                equivalentGroups.append(group)
                equivalentGroupCount.append(1)

        # Find equivalent bonds on ring
        equivalentBonds = []
        for i, atom1 in enumerate(ring0):
            for atom2 in ring0[i + 1 :]:
                if molecule.hasBond(atom1, atom2):
                    bond = molecule.getBond(atom1, atom2)
                    found = False
                    for eqBond in equivalentBonds:
                        if not found:
                            if bond.equivalent(eqBond[0]):
                                eqBond.append(group)
                                found = True
                    if not found:
                        equivalentBonds.append([bond])

        # Find maximum number of equivalent groups and bonds
        minEquivalentGroups = min(equivalentGroupCount)
        maxEquivalentGroups = max(equivalentGroupCount)
        minEquivalentBonds = None
        maxEquivalentBonds = 0
        for bonds in equivalentBonds:
            N = len(bonds)
            if minEquivalentBonds is None or N < minEquivalentBonds:
                minEquivalentBonds = N
            if N > maxEquivalentBonds:
                maxEquivalentBonds = N

        if maxEquivalentGroups == maxEquivalentBonds == len(ring):
            symmetryNumber *= len(ring) * 2
        else:
            symmetryNumber *= min(minEquivalentGroups, minEquivalentBonds)

    return symmetryNumber
Ejemplo n.º 5
0
def calculateAxisSymmetryNumber(molecule):
    """
    Get the axis symmetry number correction. The "axis" refers to a series
    of two or more cumulated double bonds (e.g. C=C=C, etc.). Corrections
    for single C=C bonds are handled in getBondSymmetryNumber().
    
    Each axis (C=C=C) has the potential to double the symmetry number.
    If an end has 0 or 1 groups (eg. =C=CJJ or =C=C-R) then it cannot 
    alter the axis symmetry and is disregarded::
    
        A=C=C=C..        A-C=C=C=C-A
        
          s=1                s=1
    
    If an end has 2 groups that are different then it breaks the symmetry 
    and the symmetry for that axis is 1, no matter what's at the other end::
    
        A\               A\         /A
          T=C=C=C=C-A      T=C=C=C=T
        B/               A/         \B
              s=1             s=1
    
    If you have one or more ends with 2 groups, and neither end breaks the 
    symmetry, then you have an axis symmetry number of 2::
    
        A\         /B      A\         
          C=C=C=C=C          C=C=C=C-B
        A/         \B      A/         
              s=2                s=2
    """

    symmetryNumber = 1

    # List all double bonds in the structure
    doubleBonds = []
    for atom1 in molecule.vertices:
        for atom2 in atom1.edges:
            if atom1.edges[atom2].isDouble() and molecule.vertices.index(atom1) < molecule.vertices.index(atom2):
                doubleBonds.append((atom1, atom2))

    # Search for adjacent double bonds
    cumulatedBonds = []
    for i, bond1 in enumerate(doubleBonds):
        atom11, atom12 = bond1
        for bond2 in doubleBonds[i + 1 :]:
            atom21, atom22 = bond2
            if atom11 is atom21 or atom11 is atom22 or atom12 is atom21 or atom12 is atom22:
                listToAddTo = None
                for cumBonds in cumulatedBonds:
                    if (atom11, atom12) in cumBonds or (atom21, atom22) in cumBonds:
                        listToAddTo = cumBonds
                if listToAddTo is not None:
                    if (atom11, atom12) not in listToAddTo:
                        listToAddTo.append((atom11, atom12))
                    if (atom21, atom22) not in listToAddTo:
                        listToAddTo.append((atom21, atom22))
                else:
                    cumulatedBonds.append([(atom11, atom12), (atom21, atom22)])

    # Also keep isolated double bonds
    for bond1 in doubleBonds:
        for bonds in cumulatedBonds:
            if bond1 in bonds:
                break
        else:
            cumulatedBonds.append([bond1])

    # For each set of adjacent double bonds, check for axis symmetry
    for bonds in cumulatedBonds:

        # Do nothing if less than two cumulated bonds
        if len(bonds) < 1:
            continue

        # Do nothing if axis is in cycle
        found = False
        for atom1, atom2 in bonds:
            if molecule.isBondInCycle(atom1.edges[atom2]):
                found = True
        if found:
            continue

        # Find terminal atoms in axis
        # Terminal atoms labelled T:  T=C=C=C=T
        axis = []
        for bond in bonds:
            axis.extend(bond)
        terminalAtoms = []
        for atom in axis:
            if axis.count(atom) == 1:
                terminalAtoms.append(atom)
        if len(terminalAtoms) != 2:
            continue

        # Remove axis from (copy of) structure
        bondlist = []
        for atom1, atom2 in bonds:
            bond = atom1.edges[atom2]
            bondlist.append(bond)
            molecule.removeBond(bond)
        structure = molecule.copy(True)
        terminalAtoms = [structure.vertices[molecule.vertices.index(atom)] for atom in terminalAtoms]
        for bond in bondlist:
            molecule.addBond(bond)

        atomsToRemove = []
        for atom in structure.vertices:
            if len(atom.edges) == 0 and atom not in terminalAtoms:  # it's not bonded to anything
                atomsToRemove.append(atom)
        for atom in atomsToRemove:
            structure.removeAtom(atom)

        # Split remaining fragments of structure
        end_fragments = structure.split()

        #
        # there can be two groups at each end     A\         /B
        #                                           T=C=C=C=T
        #                                         A/         \B

        # to start with nothing has broken symmetry about the axis
        symmetry_broken = False
        end_fragments_to_remove = []
        for fragment in end_fragments:  # a fragment is one end of the axis
            # remove the atom that was at the end of the axis and split what's left into groups
            terminalAtom = None
            for atom in terminalAtoms:
                if atom in fragment.atoms:
                    terminalAtom = atom
                    fragment.removeAtom(atom)
                    break
            else:
                continue

            groups = []
            if len(fragment.atoms) > 0:
                groups = fragment.split()

            # If end has only one group then it can't contribute to (nor break) axial symmetry
            #   Eg. this has no axis symmetry:   A-T=C=C=C=T-A
            # so we remove this end from the list of interesting end fragments
            if len(groups) == 0:
                end_fragments_to_remove.append(fragment)
                continue  # next end fragment
            elif len(groups) == 1 and terminalAtom.radicalElectrons == 0:
                if terminalAtom.atomType.label == "N3d":
                    symmetry_broken = True
                else:
                    end_fragments_to_remove.append(fragment)
                    continue  # next end fragment
            elif len(groups) == 1 and terminalAtom.radicalElectrons != 0:
                symmetry_broken = True
            elif len(groups) == 2:
                if not groups[0].isIsomorphic(groups[1]):
                    # this end has broken the symmetry of the axis
                    symmetry_broken = True

        for fragment in end_fragments_to_remove:
            end_fragments.remove(fragment)

        # If there are end fragments left that can contribute to symmetry,
        # and none of them broke it, then double the symmetry number
        # NB>> This assumes coordination number of 4 (eg. Carbon).
        #      And would be wrong if we had /B
        #                         =C=C=C=C=T-B
        #                                   \B
        #      (for some T with coordination number 5).
        if end_fragments and not symmetry_broken:
            symmetryNumber *= 2

    return symmetryNumber
Ejemplo n.º 6
0
def calculateCyclicSymmetryNumber(molecule):
    """
    Get the symmetry number correction for cyclic regions of a molecule.
    For complicated fused rings the smallest set of smallest rings is used.
    """
    from rdkit.Chem.rdmolops import SanitizeMol
    from rdkit.Chem.rdchem import Mol 
    mcopy = molecule.toRDKitMol(removeHs=True, returnMapping=False)
    SanitizeMol(mcopy)
    symmetryNumber = 1
            
    # Get symmetry number for each ring in structure
    rings = molecule.getSmallestSetOfSmallestRings()
    for ring0 in rings:

        # Make copy of structure
        structure = molecule.copy(True)
        ring = [structure.atoms[molecule.atoms.index(atom)] for atom in ring0]
        # Figure out which atoms and bonds are aromatic and reassign appropriately:
        for i, atom1 in enumerate(ring0):
            for atom2 in ring0[i+1:]:
                if molecule.hasBond(atom1, atom2):
                    if mcopy.GetBondBetweenAtoms(i,i+1) is not None:
                        if str(mcopy.GetBondBetweenAtoms(i,i+1).GetBondType()) == 'AROMATIC':
                            bond = molecule.getBond(atom1, atom2)
                            bond.applyAction(['CHANGE_BOND', atom1, 'B', atom2])
                            atom1.atomType = atom2.atomType = rmgpy.molecule.atomTypes['Cb']
                    else:
                        pass
        # Remove bonds of ring from structure
        for i, atom1 in enumerate(ring):
            for atom2 in ring[i+1:]:
                if structure.hasBond(atom1, atom2):
                    structure.removeBond(atom1.edges[atom2])

        structures = structure.split()
        groups = []
        for struct in structures:
            for atom in ring:
                if struct.hasAtom(atom): struct.removeAtom(atom)
            groups.append(struct.split())
        # Find equivalent functional groups on ring
        equivalentGroups = []; equivalentGroupCount = []
        for group in groups:
            found = False
            for i, eqGroup in enumerate(equivalentGroups):
                if not found and len(group) == len(eqGroup):
                    for g, eg in zip(group, eqGroup):
                        if not g.isIsomorphic(eg):
                            # The groups do not match
                            break
                    else:
                        # The groups match
                        found = True
                if found:
                    # We've found a matching group, so increment its count
                    equivalentGroupCount[i] += 1        
                    break
            else:
                # No matching group found, so add it as a new group
                equivalentGroups.append(group)
                equivalentGroupCount.append(1)

        # Find equivalent bonds on ring
        equivalentBonds = []
        for i, atom1 in enumerate(ring0):
            for atom2 in ring0[i+1:]:
                if molecule.hasBond(atom1, atom2):
                    bond = molecule.getBond(atom1, atom2)
                    found = False
                    for eqBond in equivalentBonds:
                        if not found:
                            if bond.equivalent(eqBond[0]):
                                eqBond.append(group)
                                found = True
                    if not found:
                        equivalentBonds.append([bond])

        # Find maximum number of equivalent groups and bonds
        minEquivalentGroups = min(equivalentGroupCount)
        maxEquivalentGroups = max(equivalentGroupCount)
        minEquivalentBonds = None
        maxEquivalentBonds = 0
        for bonds in equivalentBonds:
            N = len(bonds)
            if minEquivalentBonds is None or N < minEquivalentBonds:
                minEquivalentBonds = N
            if N > maxEquivalentBonds:
                maxEquivalentBonds = N

        if maxEquivalentGroups == maxEquivalentBonds == len(ring):
            symmetryNumber *= len(ring) * 2
        else:
            symmetryNumber *= min(minEquivalentGroups, minEquivalentBonds)

        #print len(ring), minEquivalentGroups, maxEquivalentGroups, minEquivalentBonds, maxEquivalentBonds, symmetryNumber


    return symmetryNumber