Exemplo n.º 1
0
def rotateSugar(antiSugarCoords, atoms, newChi = "syn"):
    """Given a sugar in anti configuration, rotate it to a new configuration (such as syn or high-anti).
    
    ARGUMENTS:
        antiSugarCoords - a dictionary containing a sugar in anti configuration in the format atomName: [x, y, z]
                          typically, this dictionary is generated by a BuildInitSugar object
        atoms           - a dictionary containing base coordinates in the format atomName: [x, y, z]
    OPTIONAL ARGUMENTS:
        newChi          - the chi value to rotate the sugar to
                          may be "syn", "high-anti", or a number (in degrees)
                          defaults to "syn"
    RETURNS:
        synSugarCoords  - a dictionary containing a sugar in syn configuration in the format atomName: [x, y, z]
    """
    
    if newChi == "syn":
        newChi = SYN_CHI
    elif newChi == "high-anti":
        newChi = HIGH_ANTI_CHI
    
    #translate the sugar coordinates to the origin
    synSugarCoords = dict([(atom, minus(coords, atoms["C1'"])) for (atom, coords) in antiSugarCoords.iteritems()])
    
    #rotate the sugar
    if atoms.has_key("N9"):
        baseN = "N9"
    else:
        baseN = "N1"
    axis = minus(atoms[baseN], atoms["C1'"])
    
    synSugarCoords = rotateAtoms(synSugarCoords, axis, newChi - STARTING_CHI, atoms["C1'"])
    
    return synSugarCoords
Exemplo n.º 2
0
 def buildSugar(self, baseAtoms, pucker):
     """Build a sugar of the specified pucker onto a base
     
     ARGUMENTS:
         baseAtoms - a dictionary of base atoms in the form atomName:[x, y, z]
                     Note that this dictionary MUST contain the C1' atom
         pucker    - the pucker of the sugar to be built (passed as an integer, either 2 or 3)
     RETURNS:
         coordinates for a sugar of the specified pucker in anti configuration with the base
     """
     
     #fetch the appropriate sugar structure
     if pucker == 3:
         sugarAtoms = self.__c3pAtoms
     elif pucker == 2:
         sugarAtoms = self.__c2pAtoms
     else:
         raise "BuildInitSugar called with unrecognized pucker: " + str(pucker)
     #I don't have to worry about accidentally modifying the original atom dictionaries, since
     #rotateAtoms effectively makes a deep copy
     
     #figure out which base atoms to use for alignment
     if baseAtoms.has_key("N9"):
         Natom = "N9"
         Catom = "C4"
     else:
         Natom = "N1"
         Catom = "C2"
     
     
     #rotate the sugar so the glycosidic bond is at the appropriate angle
     #first, calculate an axis for this rotation
     translatedBaseN = minus(baseAtoms[Natom], baseAtoms["C1'"])
     sugarN = sugarAtoms[Natom]
     
     axis = crossProd(sugarN, translatedBaseN)
     angle = torsion(translatedBaseN, axis, (0,0,0), sugarN)
     
     #if either angle or magnitude(axis) is 0, then the glycosidic bond is already oriented appropriately
     if not(angle == 0 or magnitude(axis) == 0):
         sugarAtoms = rotateAtoms(sugarAtoms, axis, angle)
     
     
     #next, rotate the sugar so that chi is appropriate
     translatedBaseC = minus(baseAtoms[Catom], baseAtoms["C1'"])
     curChi = torsion(translatedBaseC, translatedBaseN, [0,0,0], sugarAtoms["O4'"])
     sugarAtoms = rotateAtoms(sugarAtoms, translatedBaseN, curChi - STARTING_CHI)
     
     #remove the unnecessary atoms from the sugarAtoms dict
     del sugarAtoms["N1"]
     del sugarAtoms["N9"]
     
     #translate the sugar to the C1' atom of the base
     sugarAtoms = dict([(atom, plus(coords, baseAtoms["C1'"])) for (atom, coords) in sugarAtoms.iteritems()])
     
     return sugarAtoms
Exemplo n.º 3
0
 def __readBases(self, filename):
     """Read nucleotide structures from the specified PDB file
     ARGUMENTS:
         filename - the name of a PDB file containing structures of all four bases
     RETURNS:
         struc    - a dictionary of file strucutres in the form base type => atom name => coordinates
     """
     
     input = open(filename, 'r')
     struc = dict(A = dict(), C = dict(), G = dict(), U = dict())
     
     #read in the PDB file
     for curline in input.readlines():
         if curline[0:6] != "ATOM  ":
             continue
         atomName = curline[12:16].strip()
         resName  = curline[17:20].strip()
         x = float(curline[30:38])
         y = float(curline[38:46])
         z = float(curline[46:54])
         
         #convert the atom name to PDB3 format
         #we'll need to convert it back to PDB2 later, since that's what Coot uses, but internally, RCrane uses PDB3
         atomName = atomName.replace("*", "'")
         
         struc[resName][atomName] = [x,y,z]
     input.close()
         
     #translate each base so that the C1' atom is at [0,0,0]
     for curRes in struc.keys():
         c1loc = struc[curRes]["C1'"]
         for curAtom in struc[curRes].keys():
             struc[curRes][curAtom] = minus(struc[curRes][curAtom], c1loc)
     
     return struc
Exemplo n.º 4
0
 def __readBases(self, filename):
     """Read nucleotide structures from the specified PDB file
     ARGUMENTS:
         filename - the name of a PDB file containing structures of all four bases
     RETURNS:
         struc    - a dictionary of file strucutres in the form base type => atom name => coordinates
     """
     
     input = open(filename, 'r')
     struc = dict(A = dict(), C = dict(), G = dict(), U = dict())
     
     #read in the PDB file
     for curline in input.readlines():
         if curline[0:6] != "ATOM  ":
             continue
         atomName = curline[12:16].strip()
         resName  = curline[17:20].strip()
         x = float(curline[30:38])
         y = float(curline[38:46])
         z = float(curline[46:54])
         
         #convert the atom name to PDB3 format
         #we'll need to convert it back to PDB2 later, since that's what Coot uses, but internally, RCrane uses PDB3
         atomName = atomName.replace("*", "'")
         
         struc[resName][atomName] = [x,y,z]
         
     #translate each base so that the C1' atom is at [0,0,0]
     for curRes in struc.keys():
         c1loc = struc[curRes]["C1'"]
         for curAtom in struc[curRes].keys():
             struc[curRes][curAtom] = minus(struc[curRes][curAtom], c1loc)
     
     return struc
Exemplo n.º 5
0
 def flipBase(self, curBase):
     """Flip the base anti/syn
     
     ARGUMENTS:
         curBase - the current base object in the form [baseType, baseCoordinates]
     RETURNS:
         the base object with rotated coordinates
     NOTE:
         This function does not necessarily simply rotate about chi.  When flipping a purine, it also adjusts
         the glycosidic bond position so that it the base a chance of staying in the density
     """
     
     (baseType, curBaseCoords) = curBase
     
     curC1coords = curBaseCoords["C1'"]
     
     newBaseCoords = {}
     #translate the base to the origin
     for atomName, curAtomCoords in curBaseCoords.items():
         newBaseCoords[atomName] = minus(curBaseCoords[atomName], curC1coords)
     
     #the rotation axis is the same as the alignment vector for mutating the base
     #for pyrimidines, the axis is C1'-C4
     #for purines, the axis is from C1' to the center of the C4-C5 bond
     axis = None
     if baseType == "C" or baseType == "U":
         axis = newBaseCoords["C4"]
     else:
         axis = plus(newBaseCoords["C4"], newBaseCoords["C5"])
         axis = scalarProd(1.0/2.0, axis)
     
     #rotate the base 180 degrees about the axis
     newBaseCoords = rotateAtoms(newBaseCoords, axis, 180, curC1coords)
     
     return [baseType, newBaseCoords]
Exemplo n.º 6
0
 def flipBase(self, curBase):
     """Flip the base anti/syn
     
     ARGUMENTS:
         curBase - the current base object in the form [baseType, baseCoordinates]
     RETURNS:
         the base object with rotated coordinates
     NOTE:
         This function does not necessarily simply rotate about chi.  When flipping a purine, it also adjusts
         the glycosidic bond position so that it the base a chance of staying in the density
     """
     
     (baseType, curBaseCoords) = curBase
     
     curC1coords = curBaseCoords["C1'"]
     
     newBaseCoords = {}
     #translate the base to the origin
     for atomName, curAtomCoords in curBaseCoords.items():
         newBaseCoords[atomName] = minus(curBaseCoords[atomName], curC1coords)
     
     #the rotation axis is the same as the alignment vector for mutating the base
     #for pyrimidines, the axis is C1'-C4
     #for purines, the axis is from C1' to the center of the C4-C5 bond
     axis = None
     if baseType == "C" or baseType == "U":
         axis = newBaseCoords["C4"]
     else:
         axis = plus(newBaseCoords["C4"], newBaseCoords["C5"])
         axis = scalarProd(1.0/2.0, axis)
     
     #rotate the base 180 degrees about the axis
     newBaseCoords = rotateAtoms(newBaseCoords, axis, 180, curC1coords)
     
     return [baseType, newBaseCoords]
Exemplo n.º 7
0
 def __rotateSugarCenter (self, phos5, phos3, sugarCenter):
     """rotate the sugar center by 360 degrees in ROTATE_SUGAR_INTERVAL increments
     
     ARGUMENTS:
         phos5         - the coordinates of the 5' phosphate
         phos3         - the coordinates of the 3' phosphate
         sugarCenter   - the coordinates of the sugar center to be rotated
     RETURNS:
         rotatedPoints - a list of the rotated points, each listed as [x, y, z, rotation angle]
     """
     
     #calculate a unit vector along the rotation axis
     axis = minus(phos3, phos5)
     axis = scalarProd(axis, 1/magnitude(axis))
     
     #perform the rotation
     (u, v, w) = axis
     (x, y, z) = minus(sugarCenter, phos5)
     
     #make sure that the original location appears on the list with a rotation value of 0
     sugarCenterRot = sugarCenter + [0]
     rotatedPoints = [sugarCenterRot]
     
     curAngle = SUGAR_ROTATION_INTERVAL
     while curAngle < (2*pi - 0.5*SUGAR_ROTATION_INTERVAL):
         cosTheta = cos(curAngle)
         sinTheta = sin(curAngle)
         
         a = u*x + v*y + w*z;
         newX = a*u + (x-a*u)*cosTheta + (v*z-w*y)*sinTheta + phos5[0];
         newY = a*v + (y-a*v)*cosTheta + (w*x-u*z)*sinTheta + phos5[1];
         newZ = a*w + (z-a*w)*cosTheta + (u*y-v*x)*sinTheta + phos5[2];
         
         rotatedPoints.append([newX, newY, newZ, curAngle])
         curAngle += SUGAR_ROTATION_INTERVAL
         
     return rotatedPoints
Exemplo n.º 8
0
 def __rotateSugarCenter (self, phos5, phos3, sugarCenter):
     """rotate the sugar center by 360 degrees in ROTATE_SUGAR_INTERVAL increments
     
     ARGUMENTS:
         phos5         - the coordinates of the 5' phosphate
         phos3         - the coordinates of the 3' phosphate
         sugarCenter   - the coordinates of the sugar center to be rotated
     RETURNS:
         rotatedPoints - a list of the rotated points, each listed as [x, y, z, rotation angle]
     """
     
     #calculate a unit vector along the rotation axis
     axis = minus(phos3, phos5)
     axis = scalarProd(axis, 1/magnitude(axis))
     
     #perform the rotation
     (u, v, w) = axis
     (x, y, z) = minus(sugarCenter, phos5)
     
     #make sure that the original location appears on the list with a rotation value of 0
     sugarCenterRot = sugarCenter + [0]
     rotatedPoints = [sugarCenterRot]
     
     curAngle = SUGAR_ROTATION_INTERVAL
     while curAngle < (2*pi - 0.5*SUGAR_ROTATION_INTERVAL):
         cosTheta = cos(curAngle)
         sinTheta = sin(curAngle)
         
         a = u*x + v*y + w*z;
         newX = a*u + (x-a*u)*cosTheta + (v*z-w*y)*sinTheta + phos5[0];
         newY = a*v + (y-a*v)*cosTheta + (w*x-u*z)*sinTheta + phos5[1];
         newZ = a*w + (z-a*w)*cosTheta + (u*y-v*x)*sinTheta + phos5[2];
         
         rotatedPoints.append([newX, newY, newZ, curAngle])
         curAngle += SUGAR_ROTATION_INTERVAL
         
     return rotatedPoints
Exemplo n.º 9
0
 def __init__(self, c3pStruc, c2pStruc):
     """Initialize a BuildInitSugar object.
     
     ARGUMENTS:
         c3pstruc - the filename for a PDB file containing a C3'-endo sugar
         c2pstruc - the filename for a PDB file containing a C2'-endo sugar
     RETURNS:
         an initialized BuildInitSugar object
     """
     
     self.c3pStrucFilename = c3pStruc
     self.c2pStrucFilename = c2pStruc
     self.__c3pAtoms = readSugarPDB(c3pStruc)
     self.__c2pAtoms = readSugarPDB(c2pStruc)
     
     #translate the sugars so that the C1' atom is at the origin (so we don't have to do it every time we align a sugar)
     for curSugar in (self.__c3pAtoms, self.__c2pAtoms):
         c1p = curSugar["C1'"]
         del curSugar["C1'"] #delete the C1' atom, since it's going to be zeroed anyway
                             #(and we don't want to return it since it doesn't need to be added to the pseudoMolecule object)
         for (curAtom, curCoords) in curSugar.iteritems():
             curSugar[curAtom] = minus(curCoords, c1p)
Exemplo n.º 10
0
def buildPhosOxy(curResAtoms, prevResAtoms):
    """Calculate non-bridging phosphoryl oxygen coordinates using the coordinates of the current and previous nucleotides
    
    ARGUMENTS:
        curResAtoms - a dictionary of the current nucleotide coordinates (i.e. the nucleotide to build the phosphoryl oxygens on) in the format atomName: [x, y, z]
        prevResAtoms - a dictionary of the previous nucleotide coordinates in the format atomName: [x, y, z]
    RETURNS:
        phosOxyCoords - a dictionary of the phosphoryl oxygen coordinates in the format atomName: [x, y, z]
        or None if the residue is missing the P, O5', or O3' atoms
    """
    
    try:
        P  = curResAtoms ["P"]
        O5 = curResAtoms ["O5'"]
        O3 = prevResAtoms["O3'"]
    except KeyError:
        return None
    
    #calculate a line from O5' to O3'
    norm = minus(O5, O3)
    
    #calculate the intersection of a plane (with normal $norm and point $P) and a line (from O5' to O3')
    #using formula from http://local.wasp.uwa.edu.au/~pbourke/geometry/planeline/
    #       norm dot (P - O3)
    # i = ------------------------
    #       norm dot (O5 - O3)
    #intersecting point = O3 + i(O5 - O3)
    i = dotProd(norm, minus(P, O3)) / dotProd(norm, minus(O5, O3))
    interPoint = plus(O3, scalarProd(i, minus(O5, O3)))
    
    #move $interPoint so that the distance from $P to $interPoint is 1.485 (the length of the P-OP1 bond)
    #we also reflect the point about P
    PIline = minus(P, interPoint) #here's is where the reflection occurs, because we do $P-$interPoint instead of $interPoint-$P
    #scaledPoint = scalarProd(1/magnitude(PIline) * PHOSBONDLENGTH, PIline)
    scaledPoint = normalize(PIline, PHOSBONDLENGTH)
    #to get the new point location, we would do P + scaledPoint
    #but we need to rotate the point first before translating it back
    
    #rotate this new point by 59.8 and -59.8 degrees to determine the phosphoryl oxygen locations
    #we rotate about the axis defined by $norm
    angle = radians(PHOSBONDANGLE / 2)
    (x, y, z) = scaledPoint
    
    #unitnorm = scalarProd( 1/magnitude(norm), norm)
    unitnorm = normalize(norm)
    (u, v, w) = unitnorm
    
    a = u*x + v*y + w*z
    
    phosOxyCoords = {}
    for (atomName, theta) in (("OP1", angle), ("OP2", -angle)):
        cosTheta = cos(theta)
        sinTheta = sin(theta)
        
        #perform the rotation, and then add $P to the coordinates
        newX = a*u + (x-a*u)*cosTheta + (v*z-w*y)*sinTheta + P[0]
        newY = a*v + (y-a*v)*cosTheta + (w*x-u*z)*sinTheta + P[1]
        newZ = a*w + (z-a*w)*cosTheta + (u*y-v*x)*sinTheta + P[2]
        
        phosOxyCoords[atomName] = [newX, newY, newZ]
    
    return phosOxyCoords
Exemplo n.º 11
0
 def mutateBase(self, curBase, newBaseType):
     """Change the base type.
     
     ARGUMENTS:
         curBase - the current base object in the form [baseType, baseCoordinates]
         newBaseType - the base type to mutate to
     RETURNS:
         baseObj  - a list of [baseType, baseCoordinates]
     """
     
     (curBaseType, curBaseCoords) = curBase
     
     #calculate the vectors used to align the old and new bases
     #for pyrimidines, the vector is C1'-C4
     #for purines, the vector is from C1' to the center of the C4-C5 bond
     curAlignmentVector = None
     if curBaseType == "C" or curBaseType == "U":
         curAlignmentVector = minus(curBaseCoords["C4"], curBaseCoords["C1'"])
     else:
         curBaseCenter = plus(curBaseCoords["C4"], curBaseCoords["C5"])
         curBaseCenter = scalarProd(1.0/2.0, curBaseCenter)
         curAlignmentVector = minus(curBaseCenter, curBaseCoords["C1'"])
     
     #calculate the alignment vector for the new base
     newBaseCoords = self.__baseStrucs[newBaseType]
     newAlignmentVector = None
     if newBaseType == "C" or newBaseType == "U":
         newAlignmentVector = newBaseCoords["C4"]
     else:
         newAlignmentVector = plus(newBaseCoords["C4"], newBaseCoords["C5"])
         newAlignmentVector = scalarProd(1.0/2.0, newAlignmentVector)
     
     #calculate the angle between the alignment vectors
     rotationAngle = -angle(curAlignmentVector, [0,0,0], newAlignmentVector)
     axis = crossProd(curAlignmentVector, newAlignmentVector)
     
     #rotate the new base coordinates
     newBaseCoords = rotateAtoms(newBaseCoords, axis, rotationAngle)
     
     #calculate the normals of the base planes
     curNormal = None
     if curBaseType == "C" or curBaseType == "U":
         curNormal = crossProd(minus(curBaseCoords["N3"], curBaseCoords["N1"]), minus(curBaseCoords["C6"], curBaseCoords["N1"]))
     else:
         curNormal = crossProd(minus(curBaseCoords["N3"], curBaseCoords["N9"]), minus(curBaseCoords["N7"], curBaseCoords["N9"]))
     
     newNormal = None
     if newBaseType == "C" or newBaseType == "U":
         newNormal = crossProd(minus(newBaseCoords["N3"], newBaseCoords["N1"]), minus(newBaseCoords["C6"], newBaseCoords["N1"]))
     else:
         newNormal = crossProd(minus(newBaseCoords["N3"], newBaseCoords["N9"]), minus(newBaseCoords["N7"], newBaseCoords["N9"]))
     
     #calculate the angle between the normals
     normalAngle = -angle(curNormal, [0,0,0], newNormal);
     normalAxis = crossProd(curNormal, newNormal)
     
     #rotate the new base coordinates so that it falls in the same plane as the current base
     #and translate the base to the appropriate location
     newBaseCoords = rotateAtoms(newBaseCoords, normalAxis, normalAngle, curBaseCoords["C1'"])
     
     return [newBaseType, newBaseCoords]
Exemplo n.º 12
0
 def findBase(self, mapNum, sugar, phos5, phos3, baseType, direction = 3):
     """Rotate the sugar center by 360 degrees in ROTATE_SUGAR_INTERVAL increments
     
     ARGUMENTS:
         mapNum   - the molecule number of the Coot map to use
         sugar    - the coordinates of the C1' atom
         phos5    - the coordinates of the 5' phosphate
         phos3    - the coordinates of the 3' phosphate
         baseType - the base type (A, C, G, or U)
     OPTIONAL ARGUMENTS:
         direction - which direction are we tracing the chain
                     if it is 5 (i.e. 3'->5'), then phos5 and phos3 will be flipped
                     all other values will be ignored
                     defaults to 3 (i.e. 5'->3')
     RETURNS:
         baseObj  - a list of [baseType, baseCoordinates]
     """
     
     if direction == 5:
         (phos5, phos3) = (phos3, phos5)
     
     #calculate the bisector of the phos-sugar-phos angle
     #first, calculate a normal to the phos-sugar-phos plane
     sugarPhos5Vec = minus(phos5, sugar)
     sugarPhos3Vec = minus(phos3, sugar)
     normal = crossProd(sugarPhos5Vec, sugarPhos3Vec)
     normal = scalarProd(normal, 1.0/magnitude(normal))
     
     phosSugarPhosAngle = angle(phos5, sugar, phos3)
     
     bisector = rotate(sugarPhos5Vec, normal, phosSugarPhosAngle/2.0)
     
     
     #flip the bisector around (so it points away from the phosphates) and scale its length to 5 A
     startingBasePos = scalarProd(bisector, -1/magnitude(bisector))
     
     #rotate the base baton by 10 degree increments about half of a sphere
     rotations = [startingBasePos] #a list of coordinates for all of the rotations
     for curTheta in range(-90, -1, 10) + range(10, 91, 10):
         curRotation = rotate(startingBasePos, normal, curTheta)
         rotations.append(curRotation) #here's where the phi=0 rotation is accounted for
         
         for curPhi in range(-90, -1, 10) + range(10, 91, 10):
             rotations.append(rotate(curRotation, startingBasePos, curPhi))
             
     #test electron density along all base batons
     for curBaton in rotations:
         curDensityTotal = 0
         densityList = []
         for i in range(1, 9):
             (x, y, z) = plus(sugar, scalarProd(i/2.0, curBaton))
             curPointDensity = density_at_point(mapNum, x, y, z)
             curDensityTotal += curPointDensity
             densityList.append(curPointDensity)
         curBaton.append(curDensityTotal)        #the sum of the density (equivalent to the mean for ordering purposes)
         curBaton.append(median(densityList))    #the median of the density
         curBaton.append(min(densityList))       #the minimum of the density
     
     #find the baton with the max density (as measured using the median)
     #Note that we ignore the sum and minimum of the density.  Those calculations could be commented out,
     #   but they may be useful at some point in the future.  When we look at higher resolutions maybe?
     #   Besides, they're fast calculations.)
     baseDir = max(rotations, key = lambda x: x[4])
     
     #rotate the stock base+sugar structure to align with the base baton
     rotationAngle = angle(self.__baseStrucs["C"]["C4"], [0,0,0], baseDir)
     axis = crossProd(self.__baseStrucs["C"]["C4"], baseDir[0:3])
     
     orientedBase = rotateAtoms(self.__baseStrucs["C"], axis, rotationAngle)
     
     #rotate the base about chi to find the best fit to density
     bestFitBase = None
     maxDensity = -999999
     for curAngle in range(0,360,5):
         rotatedBase = rotateAtoms(orientedBase, orientedBase["C4"], curAngle, sugar)
         curDensity = 0
         for curAtom in ["N1", "C2", "N3", "C4", "C5", "C6"]:
             curDensity += density_at_point(mapNum, rotatedBase[curAtom][0], rotatedBase[curAtom][1], rotatedBase[curAtom][2])
         
         #this is "pseudoChi" because it uses the 5' phosphate in place of the O4' atom
         pseudoChi = torsion(phos5, sugar, rotatedBase["N1"], rotatedBase["N3"])
         curDensity *= self.__pseudoChiInterp.interp(pseudoChi)
         
         if curDensity > maxDensity:
             maxDensity = curDensity
             bestFitBase = rotatedBase
     
     baseObj = ["C", bestFitBase]
     
     #mutate the base to the appropriate type
     if baseType != "C":
         baseObj = self.mutateBase(baseObj, baseType)
     
     return baseObj
Exemplo n.º 13
0
 def findSugar(self, mapNum, phos5, phos3):
     """find potential C1' locations between the given 5' and 3' phosphate coordinates
     
     ARGUMENTS:
         mapNum      - the molecule number of the Coot map to use
         phos5       - the coordinates of the 5' phosphate
         phos3       - the coordinates of the 3' phosphate
     RETURNS:
         sugarMaxima - a list of potential C1' locations, each listed as [x, y, z, score]
     """
     
     #calculate the distance between the two phosphatse
     phosPhosDist = dist(phos5, phos3)
     
     #calculate a potential spot for the sugar based on the phosphate-phosphate distance
     #projDist is how far along the 3'P-5'P vector the sugar center should be (measured from the 3'P)
     #perpDist is how far off from the 3'P-5'P vector the sugar center should be
     
     #these functions are for the sugar center, which is what we try to find here
     #since it will be more in the center of the density blob
     perpDist = -0.185842*phosPhosDist**2 + 1.62296*phosPhosDist - 0.124146
     projDist = 0.440092*phosPhosDist + 0.909732
     
     #if we wanted to find the C1' instead of the sugar center, we'd use these functions
     #however, finding the C1' directly causes our density scores to be less accurate
     #so we instead use the functions above to find the sugar center and later adjust our
     #coordinates to get the C1' location
     #perpDist = -0.124615*phosPhosDist**2 + 0.955624*phosPhosDist + 2.772573
     #projDist = 0.466938*phosPhosDist + 0.649833
     
     
     #calculate the normal to the plane defined by 3'P, 5'P, and a dummy point
     normal = crossProd([10,0,0], minus(phos3, phos5))
     
     #make sure the magnitude of the normal is not zero (or almost zero)
     #if it is zero, that means that our dummy point was co-linear with the 3'P-5'P vector
     #and we haven't calculated a normal
     #if the magnitude is almost zero, then the dummy point was almost co-linear and we have to worry about rounding error
     #in either of those cases, just use a different dummy point
     #they should both be incredibly rare cases, but it doesn't hurt to be safe
     if magnitude(normal) < 0.001:
         #print "Recalculating normal"
         normal = crossProd([0,10,0], minus(phos5, phos3))
     
     
     #scale the normal to the length of perpDist
     perpVector = scalarProd(normal, perpDist/magnitude(normal))
     
     #calculate the 3'P-5'P vector and scale it to the length of projDist
     projVector = minus(phos3, phos5)
     projVector = scalarProd(projVector, projDist/magnitude(projVector))
     
     #calculate a possible sugar location
     sugarLoc = plus(phos5, projVector)
     sugarLoc = plus(sugarLoc, perpVector)
     
     #rotate the potential sugar location around the 3'P-5'P vector to generate a list of potential sugar locations
     sugarRotationPoints = self.__rotateSugarCenter(phos5, phos3, sugarLoc)
     
     #test each potential sugar locations to find the one with the best electron density
     for curSugarLocFull in sugarRotationPoints:
         curSugarLoc = curSugarLocFull[0:3] #the rotation angle is stored as curSugarLocFull[4], so we trim that off for curSugarLoc
         curDensityTotal = 0
         #densityList = []   #if desired, this could be used to generate additional statistics on the density (such as the median or quartiles)
         
         #check density along the 5'P-sugar vector
         phosSugarVector = minus(curSugarLoc, phos5)
         phosSugarVector = scalarProd(phosSugarVector, 1.0/(DENSITY_CHECK_POINTS+1))
         for i in range(1, DENSITY_CHECK_POINTS+1):
             (x, y, z) = plus(phos5, scalarProd(i, phosSugarVector))
             curPointDensity = density_at_point(mapNum, x, y, z)
             curDensityTotal += curPointDensity
             #densityList.append(curPointDensity)
             
         
         #check at the sugar center
         (x, y, z) = curSugarLoc
         curPointDensity = density_at_point(mapNum, x, y, z)
         curDensityTotal += curPointDensity
         #densityList.append(curPointDensity)
         
         #check along the sugar-3'P vector
         sugarPhosVector = minus(phos3, curSugarLoc)
         sugarPhosVector = scalarProd(sugarPhosVector, 1.0/(DENSITY_CHECK_POINTS+1))
         for i in range(1, DENSITY_CHECK_POINTS+1):
             (x, y, z) = plus(curSugarLoc, scalarProd(i, sugarPhosVector))
             curPointDensity = density_at_point(mapNum, x, y, z)
             curDensityTotal += curPointDensity
             #densityList.append(curPointDensity)
         
         curSugarLocFull.append(curDensityTotal)
         #curSugarLocFull.extend([curDensityTotal, median(densityList), lowerQuartile(densityList), min(densityList)])#, pointList])
     
     #find all the local maxima
     sugarMaxima = []
     curPeakHeight = sugarRotationPoints[-1][4]
     nextPeakHeight = sugarRotationPoints[0][4]
     sugarRotationPoints.append(sugarRotationPoints[0]) #copy the first point to the end so that we can properly check the last point
     for i in range(0, len(sugarRotationPoints)-1):
         prevPeakHeight = curPeakHeight
         curPeakHeight  = nextPeakHeight
         nextPeakHeight = sugarRotationPoints[i+1][4]
         if prevPeakHeight < curPeakHeight and curPeakHeight >=  nextPeakHeight:
             sugarMaxima.append(sugarRotationPoints[i])
     
     #sort the local maxima by their density score
     sugarMaxima.sort(key = lambda x: x[4], reverse = True)
     
     #adjust all the sugar center coordinates so that they represent the corresponding C1' coordinates
     for i in range(0, len(sugarMaxima)):
         curSugar = sugarMaxima[i][0:3]
         #rotate a vector 148 degrees from the phosphate bisector
         phosAngle = angle(phos5, curSugar, phos3)
         phos5vector = minus(phos5, curSugar)
         axis = crossProd(minus(phos3, curSugar), phos5vector)
         axis = scalarProd(axis, 1/magnitude(axis))
         c1vec = rotate(phos5vector, axis, 148.539123-phosAngle/2)
         
         #scale the vector to the appropriate length
         c1vec = scalarProd(c1vec, 1.235367/magnitude(c1vec))
         
         #rotate the vector about the phosphate bisector
         phosBisectorAxis = rotate(phos5vector, axis, -phosAngle/2)
         phosBisectorAxis = scalarProd(phosBisectorAxis, 1/magnitude(phosBisectorAxis)) 
         c1vec = rotate(c1vec, phosBisectorAxis, -71.409162)
         
         sugarMaxima[i][0:3] = plus(c1vec, curSugar)
     
     return sugarMaxima
Exemplo n.º 14
0
def buildInitOrTerminalPhosOxy(curResAtoms, prevResAtoms = None):
    """build phosphoryl oxygens using only the O3' or O5'atom (intended for the first or last nucleotide of a chain/segment)
    
    ARGUMENTS:
        curResAtoms - a dictionary of the current nucleotide coordinates (i.e. the nucleotide to build the phosphoryl oxygens on) in the format atomName: [x, y, z]
                      this dictionary must contain at least the phosphate
    OPTIONAL ARGUMENTS:
        prevResAtoms - a dictionary of the previous nucleotide coordinates in the format atomName: [x, y, z]
                       if provided, the curResAtoms["P"] and prevResAtoms["O3'"] will be used to place the phosphoryl oxygens
                       if not provided, the curResAtoms["P"] and curResAtoms["O5'"]
    RETURNS:
        phosOxyCoords - a dictionary of the phosphoryl oxygen coordinates in the format atomName: [x, y, z]
        or None if the residue is missing the necessary atoms
    """
    
    try:
        P  = curResAtoms ["P"]
        if prevResAtoms is not None:
            O = prevResAtoms["O3'"]
            C = prevResAtoms["C3'"]
        else:
            O = curResAtoms["O5'"]
            C = curResAtoms["C5'"]
    except KeyError:
        return None
    
    #place atom along the P-O5' bond that is the appropriate distance from P
    phosOxy = minus(O, P)
    phosOxy = normalize(phosOxy, PHOSBONDLENGTH)
    
    #define plane with C5'-O5'-P
    norm = crossProd( minus(C, O), minus(P, O))
    norm = normalize(norm)
    
    #rotate dummy atom in plane about P by INITPHOSANGLE (which is the appropriate O5'-OP1 angle)
    (x, y, z) = phosOxy
    (u, v, w) = norm
    theta = -radians(INITPHOSANGLE) #use the negative angle so that we rotate away from the C5'
    
    cosTheta = cos(theta)
    sinTheta = sin(theta)
    
    #perform the rotation
    a = u*x + v*y + w*z
    phosOxy[0] = a*u + (x-a*u)*cosTheta + (v*z-w*y)*sinTheta
    phosOxy[1] = a*v + (y-a*v)*cosTheta + (w*x-u*z)*sinTheta
    phosOxy[2] = a*w + (z-a*w)*cosTheta + (u*y-v*x)*sinTheta
    
    #rotate dummy atom about O5'-P axis by 59.8 and -59.8 degrees
    norm = minus(O, P)
    norm = normalize(norm)
    
    (x, y, z) = phosOxy
    (u, v, w) = norm
    angle = radians(PHOSBONDANGLE / 2)
    
    phosOxyCoords = {}
    for (atomName, theta) in (("OP1", angle), ("OP2", -angle)):
        cosTheta = cos(theta)
        sinTheta = sin(theta)
        
        #perform the rotation, and then add $P to the coordinates
        newX = a*u + (x-a*u)*cosTheta + (v*z-w*y)*sinTheta + P[0]
        newY = a*v + (y-a*v)*cosTheta + (w*x-u*z)*sinTheta + P[1]
        newZ = a*w + (z-a*w)*cosTheta + (u*y-v*x)*sinTheta + P[2]
        
        phosOxyCoords[atomName] = [newX, newY, newZ]
    
    return phosOxyCoords
    
    
Exemplo n.º 15
0
def buildPhosOxy(curResAtoms, prevResAtoms):
    """Calculate non-bridging phosphoryl oxygen coordinates using the coordinates of the current and previous nucleotides
    
    ARGUMENTS:
        curResAtoms - a dictionary of the current nucleotide coordinates (i.e. the nucleotide to build the phosphoryl oxygens on) in the format atomName: [x, y, z]
        prevResAtoms - a dictionary of the previous nucleotide coordinates in the format atomName: [x, y, z]
    RETURNS:
        phosOxyCoords - a dictionary of the phosphoryl oxygen coordinates in the format atomName: [x, y, z]
        or None if the residue is missing the P, O5', or O3' atoms
    """

    try:
        P = curResAtoms["P"]
        O5 = curResAtoms["O5'"]
        O3 = prevResAtoms["O3'"]
    except KeyError:
        return None

    #calculate a line from O5' to O3'
    norm = minus(O5, O3)

    #calculate the intersection of a plane (with normal $norm and point $P) and a line (from O5' to O3')
    #using formula from http://local.wasp.uwa.edu.au/~pbourke/geometry/planeline/
    #       norm dot (P - O3)
    # i = ------------------------
    #       norm dot (O5 - O3)
    #intersecting point = O3 + i(O5 - O3)
    i = dotProd(norm, minus(P, O3)) / dotProd(norm, minus(O5, O3))
    interPoint = plus(O3, scalarProd(i, minus(O5, O3)))

    #move $interPoint so that the distance from $P to $interPoint is 1.485 (the length of the P-OP1 bond)
    #we also reflect the point about P
    PIline = minus(
        P, interPoint
    )  #here's is where the reflection occurs, because we do $P-$interPoint instead of $interPoint-$P
    #scaledPoint = scalarProd(1/magnitude(PIline) * PHOSBONDLENGTH, PIline)
    scaledPoint = normalize(PIline, PHOSBONDLENGTH)
    #to get the new point location, we would do P + scaledPoint
    #but we need to rotate the point first before translating it back

    #rotate this new point by 59.8 and -59.8 degrees to determine the phosphoryl oxygen locations
    #we rotate about the axis defined by $norm
    angle = radians(PHOSBONDANGLE / 2)
    (x, y, z) = scaledPoint

    #unitnorm = scalarProd( 1/magnitude(norm), norm)
    unitnorm = normalize(norm)
    (u, v, w) = unitnorm

    a = u * x + v * y + w * z

    phosOxyCoords = {}
    for (atomName, theta) in (("OP1", angle), ("OP2", -angle)):
        cosTheta = cos(theta)
        sinTheta = sin(theta)

        #perform the rotation, and then add $P to the coordinates
        newX = a * u + (x - a * u) * cosTheta + (v * z -
                                                 w * y) * sinTheta + P[0]
        newY = a * v + (y - a * v) * cosTheta + (w * x -
                                                 u * z) * sinTheta + P[1]
        newZ = a * w + (z - a * w) * cosTheta + (u * y -
                                                 v * x) * sinTheta + P[2]

        phosOxyCoords[atomName] = [newX, newY, newZ]

    return phosOxyCoords
Exemplo n.º 16
0
 def findBase(self, mapNum, sugar, phos5, phos3, baseType, direction = 3):
     """Rotate the sugar center by 360 degrees in ROTATE_SUGAR_INTERVAL increments
     
     ARGUMENTS:
         mapNum   - the molecule number of the Coot map to use
         sugar    - the coordinates of the C1' atom
         phos5    - the coordinates of the 5' phosphate
         phos3    - the coordinates of the 3' phosphate
         baseType - the base type (A, C, G, or U)
     OPTIONAL ARGUMENTS:
         direction - which direction are we tracing the chain
                     if it is 5 (i.e. 3'->5'), then phos5 and phos3 will be flipped
                     all other values will be ignored
                     defaults to 3 (i.e. 5'->3')
     RETURNS:
         baseObj  - a list of [baseType, baseCoordinates]
     """
     
     if direction == 5:
         (phos5, phos3) = (phos3, phos5)
     
     #calculate the bisector of the phos-sugar-phos angle
     #first, calculate a normal to the phos-sugar-phos plane
     sugarPhos5Vec = minus(phos5, sugar)
     sugarPhos3Vec = minus(phos3, sugar)
     normal = crossProd(sugarPhos5Vec, sugarPhos3Vec)
     normal = scalarProd(normal, 1.0/magnitude(normal))
     
     phosSugarPhosAngle = angle(phos5, sugar, phos3)
     
     bisector = rotate(sugarPhos5Vec, normal, phosSugarPhosAngle/2.0)
     
     
     #flip the bisector around (so it points away from the phosphates) and scale its length to 5 A
     startingBasePos = scalarProd(bisector, -1/magnitude(bisector))
     
     #rotate the base baton by 10 degree increments about half of a sphere
     rotations = [startingBasePos] #a list of coordinates for all of the rotations
     for curTheta in range(-90, -1, 10) + range(10, 91, 10):
         curRotation = rotate(startingBasePos, normal, curTheta)
         rotations.append(curRotation) #here's where the phi=0 rotation is accounted for
         
         for curPhi in range(-90, -1, 10) + range(10, 91, 10):
             rotations.append(rotate(curRotation, startingBasePos, curPhi))
             
     #test electron density along all base batons
     for curBaton in rotations:
         curDensityTotal = 0
         densityList = []
         for i in range(1, 9):
             (x, y, z) = plus(sugar, scalarProd(i/2.0, curBaton))
             curPointDensity = density_at_point(mapNum, x, y, z)
             curDensityTotal += curPointDensity
             densityList.append(curPointDensity)
         curBaton.append(curDensityTotal)        #the sum of the density (equivalent to the mean for ordering purposes)
         curBaton.append(median(densityList))    #the median of the density
         curBaton.append(min(densityList))       #the minimum of the density
     
     #find the baton with the max density (as measured using the median)
     #Note that we ignore the sum and minimum of the density.  Those calculations could be commented out,
     #   but they may be useful at some point in the future.  When we look at higher resolutions maybe?
     #   Besides, they're fast calculations.)
     baseDir = max(rotations, key = lambda x: x[4])
     
     #rotate the stock base+sugar structure to align with the base baton
     rotationAngle = angle(self.__baseStrucs["C"]["C4"], [0,0,0], baseDir)
     axis = crossProd(self.__baseStrucs["C"]["C4"], baseDir[0:3])
     
     orientedBase = rotateAtoms(self.__baseStrucs["C"], axis, rotationAngle)
     
     #rotate the base about chi to find the best fit to density
     bestFitBase = None
     maxDensity = -999999
     for curAngle in range(0,360,5):
         rotatedBase = rotateAtoms(orientedBase, orientedBase["C4"], curAngle, sugar)
         curDensity = 0
         for curAtom in ["N1", "C2", "N3", "C4", "C5", "C6"]:
             curDensity += density_at_point(mapNum, rotatedBase[curAtom][0], rotatedBase[curAtom][1], rotatedBase[curAtom][2])
         
         #this is "pseudoChi" because it uses the 5' phosphate in place of the O4' atom
         pseudoChi = torsion(phos5, sugar, rotatedBase["N1"], rotatedBase["N3"])
         curDensity *= self.__pseudoChiInterp.interp(pseudoChi)
         
         if curDensity > maxDensity:
             maxDensity = curDensity
             bestFitBase = rotatedBase
     
     baseObj = ["C", bestFitBase]
     
     #mutate the base to the appropriate type
     if baseType != "C":
         baseObj = self.mutateBase(baseObj, baseType)
     
     return baseObj
Exemplo n.º 17
0
 def mutateBase(self, curBase, newBaseType):
     """Change the base type.
     
     ARGUMENTS:
         curBase - the current base object in the form [baseType, baseCoordinates]
         newBaseType - the base type to mutate to
     RETURNS:
         baseObj  - a list of [baseType, baseCoordinates]
     """
     
     (curBaseType, curBaseCoords) = curBase
     
     #calculate the vectors used to align the old and new bases
     #for pyrimidines, the vector is C1'-C4
     #for purines, the vector is from C1' to the center of the C4-C5 bond
     curAlignmentVector = None
     if curBaseType == "C" or curBaseType == "U":
         curAlignmentVector = minus(curBaseCoords["C4"], curBaseCoords["C1'"])
     else:
         curBaseCenter = plus(curBaseCoords["C4"], curBaseCoords["C5"])
         curBaseCenter = scalarProd(1.0/2.0, curBaseCenter)
         curAlignmentVector = minus(curBaseCenter, curBaseCoords["C1'"])
     
     #calculate the alignment vector for the new base
     newBaseCoords = self.__baseStrucs[newBaseType]
     newAlignmentVector = None
     if newBaseType == "C" or newBaseType == "U":
         newAlignmentVector = newBaseCoords["C4"]
     else:
         newAlignmentVector = plus(newBaseCoords["C4"], newBaseCoords["C5"])
         newAlignmentVector = scalarProd(1.0/2.0, newAlignmentVector)
     
     #calculate the angle between the alignment vectors
     rotationAngle = -angle(curAlignmentVector, [0,0,0], newAlignmentVector)
     axis = crossProd(curAlignmentVector, newAlignmentVector)
     
     #rotate the new base coordinates
     newBaseCoords = rotateAtoms(newBaseCoords, axis, rotationAngle)
     
     #calculate the normals of the base planes
     curNormal = None
     if curBaseType == "C" or curBaseType == "U":
         curNormal = crossProd(minus(curBaseCoords["N3"], curBaseCoords["N1"]), minus(curBaseCoords["C6"], curBaseCoords["N1"]))
     else:
         curNormal = crossProd(minus(curBaseCoords["N3"], curBaseCoords["N9"]), minus(curBaseCoords["N7"], curBaseCoords["N9"]))
     
     newNormal = None
     if newBaseType == "C" or newBaseType == "U":
         newNormal = crossProd(minus(newBaseCoords["N3"], newBaseCoords["N1"]), minus(newBaseCoords["C6"], newBaseCoords["N1"]))
     else:
         newNormal = crossProd(minus(newBaseCoords["N3"], newBaseCoords["N9"]), minus(newBaseCoords["N7"], newBaseCoords["N9"]))
     
     #calculate the angle between the normals
     normalAngle = -angle(curNormal, [0,0,0], newNormal);
     normalAxis = crossProd(curNormal, newNormal)
     
     #rotate the new base coordinates so that it falls in the same plane as the current base
     #and translate the base to the appropriate location
     newBaseCoords = rotateAtoms(newBaseCoords, normalAxis, normalAngle, curBaseCoords["C1'"])
     
     return [newBaseType, newBaseCoords]
Exemplo n.º 18
0
def buildInitOrTerminalPhosOxy(curResAtoms, prevResAtoms=None):
    """build phosphoryl oxygens using only the O3' or O5'atom (intended for the first or last nucleotide of a chain/segment)
    
    ARGUMENTS:
        curResAtoms - a dictionary of the current nucleotide coordinates (i.e. the nucleotide to build the phosphoryl oxygens on) in the format atomName: [x, y, z]
                      this dictionary must contain at least the phosphate
    OPTIONAL ARGUMENTS:
        prevResAtoms - a dictionary of the previous nucleotide coordinates in the format atomName: [x, y, z]
                       if provided, the curResAtoms["P"] and prevResAtoms["O3'"] will be used to place the phosphoryl oxygens
                       if not provided, the curResAtoms["P"] and curResAtoms["O5'"]
    RETURNS:
        phosOxyCoords - a dictionary of the phosphoryl oxygen coordinates in the format atomName: [x, y, z]
        or None if the residue is missing the necessary atoms
    """

    try:
        P = curResAtoms["P"]
        if prevResAtoms is not None:
            O = prevResAtoms["O3'"]
            C = prevResAtoms["C3'"]
        else:
            O = curResAtoms["O5'"]
            C = curResAtoms["C5'"]
    except KeyError:
        return None

    #place atom along the P-O5' bond that is the appropriate distance from P
    phosOxy = minus(O, P)
    phosOxy = normalize(phosOxy, PHOSBONDLENGTH)

    #define plane with C5'-O5'-P
    norm = crossProd(minus(C, O), minus(P, O))
    norm = normalize(norm)

    #rotate dummy atom in plane about P by INITPHOSANGLE (which is the appropriate O5'-OP1 angle)
    (x, y, z) = phosOxy
    (u, v, w) = norm
    theta = -radians(
        INITPHOSANGLE
    )  #use the negative angle so that we rotate away from the C5'

    cosTheta = cos(theta)
    sinTheta = sin(theta)

    #perform the rotation
    a = u * x + v * y + w * z
    phosOxy[0] = a * u + (x - a * u) * cosTheta + (v * z - w * y) * sinTheta
    phosOxy[1] = a * v + (y - a * v) * cosTheta + (w * x - u * z) * sinTheta
    phosOxy[2] = a * w + (z - a * w) * cosTheta + (u * y - v * x) * sinTheta

    #rotate dummy atom about O5'-P axis by 59.8 and -59.8 degrees
    norm = minus(O, P)
    norm = normalize(norm)

    (x, y, z) = phosOxy
    (u, v, w) = norm
    angle = radians(PHOSBONDANGLE / 2)

    phosOxyCoords = {}
    for (atomName, theta) in (("OP1", angle), ("OP2", -angle)):
        cosTheta = cos(theta)
        sinTheta = sin(theta)

        #perform the rotation, and then add $P to the coordinates
        newX = a * u + (x - a * u) * cosTheta + (v * z -
                                                 w * y) * sinTheta + P[0]
        newY = a * v + (y - a * v) * cosTheta + (w * x -
                                                 u * z) * sinTheta + P[1]
        newZ = a * w + (z - a * w) * cosTheta + (u * y -
                                                 v * x) * sinTheta + P[2]

        phosOxyCoords[atomName] = [newX, newY, newZ]

    return phosOxyCoords
Exemplo n.º 19
0
 def findSugar(self, mapNum, phos5, phos3):
     """find potential C1' locations between the given 5' and 3' phosphate coordinates
     
     ARGUMENTS:
         mapNum      - the molecule number of the Coot map to use
         phos5       - the coordinates of the 5' phosphate
         phos3       - the coordinates of the 3' phosphate
     RETURNS:
         sugarMaxima - a list of potential C1' locations, each listed as [x, y, z, score]
     """
     
     #calculate the distance between the two phosphatse
     phosPhosDist = dist(phos5, phos3)
     
     #calculate a potential spot for the sugar based on the phosphate-phosphate distance
     #projDist is how far along the 3'P-5'P vector the sugar center should be (measured from the 3'P)
     #perpDist is how far off from the 3'P-5'P vector the sugar center should be
     
     #these functions are for the sugar center, which is what we try to find here
     #since it will be more in the center of the density blob
     perpDist = -0.185842*phosPhosDist**2 + 1.62296*phosPhosDist - 0.124146
     projDist = 0.440092*phosPhosDist + 0.909732
     
     #if we wanted to find the C1' instead of the sugar center, we'd use these functions
     #however, finding the C1' directly causes our density scores to be less accurate
     #so we instead use the functions above to find the sugar center and later adjust our
     #coordinates to get the C1' location
     #perpDist = -0.124615*phosPhosDist**2 + 0.955624*phosPhosDist + 2.772573
     #projDist = 0.466938*phosPhosDist + 0.649833
     
     
     #calculate the normal to the plane defined by 3'P, 5'P, and a dummy point
     normal = crossProd([10,0,0], minus(phos3, phos5))
     
     #make sure the magnitude of the normal is not zero (or almost zero)
     #if it is zero, that means that our dummy point was co-linear with the 3'P-5'P vector
     #and we haven't calculated a normal
     #if the magnitude is almost zero, then the dummy point was almost co-linear and we have to worry about rounding error
     #in either of those cases, just use a different dummy point
     #they should both be incredibly rare cases, but it doesn't hurt to be safe
     if magnitude(normal) < 0.001:
         #print "Recalculating normal"
         normal = crossProd([0,10,0], minus(phos5, phos3))
     
     
     #scale the normal to the length of perpDist
     perpVector = scalarProd(normal, perpDist/magnitude(normal))
     
     #calculate the 3'P-5'P vector and scale it to the length of projDist
     projVector = minus(phos3, phos5)
     projVector = scalarProd(projVector, projDist/magnitude(projVector))
     
     #calculate a possible sugar location
     sugarLoc = plus(phos5, projVector)
     sugarLoc = plus(sugarLoc, perpVector)
     
     #rotate the potential sugar location around the 3'P-5'P vector to generate a list of potential sugar locations
     sugarRotationPoints = self.__rotateSugarCenter(phos5, phos3, sugarLoc)
     
     #test each potential sugar locations to find the one with the best electron density
     for curSugarLocFull in sugarRotationPoints:
         curSugarLoc = curSugarLocFull[0:3] #the rotation angle is stored as curSugarLocFull[4], so we trim that off for curSugarLoc
         curDensityTotal = 0
         #densityList = []   #if desired, this could be used to generate additional statistics on the density (such as the median or quartiles)
         
         #check density along the 5'P-sugar vector
         phosSugarVector = minus(curSugarLoc, phos5)
         phosSugarVector = scalarProd(phosSugarVector, 1.0/(DENSITY_CHECK_POINTS+1))
         for i in range(1, DENSITY_CHECK_POINTS+1):
             (x, y, z) = plus(phos5, scalarProd(i, phosSugarVector))
             curPointDensity = density_at_point(mapNum, x, y, z)
             curDensityTotal += curPointDensity
             #densityList.append(curPointDensity)
             
         
         #check at the sugar center
         (x, y, z) = curSugarLoc
         curPointDensity = density_at_point(mapNum, x, y, z)
         curDensityTotal += curPointDensity
         #densityList.append(curPointDensity)
         
         #check along the sugar-3'P vector
         sugarPhosVector = minus(phos3, curSugarLoc)
         sugarPhosVector = scalarProd(sugarPhosVector, 1.0/(DENSITY_CHECK_POINTS+1))
         for i in range(1, DENSITY_CHECK_POINTS+1):
             (x, y, z) = plus(curSugarLoc, scalarProd(i, sugarPhosVector))
             curPointDensity = density_at_point(mapNum, x, y, z)
             curDensityTotal += curPointDensity
             #densityList.append(curPointDensity)
         
         curSugarLocFull.append(curDensityTotal)
         #curSugarLocFull.extend([curDensityTotal, median(densityList), lowerQuartile(densityList), min(densityList)])#, pointList])
     
     #find all the local maxima
     sugarMaxima = []
     curPeakHeight = sugarRotationPoints[-1][4]
     nextPeakHeight = sugarRotationPoints[0][4]
     sugarRotationPoints.append(sugarRotationPoints[0]) #copy the first point to the end so that we can properly check the last point
     for i in range(0, len(sugarRotationPoints)-1):
         prevPeakHeight = curPeakHeight
         curPeakHeight  = nextPeakHeight
         nextPeakHeight = sugarRotationPoints[i+1][4]
         if prevPeakHeight < curPeakHeight and curPeakHeight >=  nextPeakHeight:
             sugarMaxima.append(sugarRotationPoints[i])
     
     #sort the local maxima by their density score
     sugarMaxima.sort(key = lambda x: x[4], reverse = True)
     
     #adjust all the sugar center coordinates so that they represent the corresponding C1' coordinates
     for i in range(0, len(sugarMaxima)):
         curSugar = sugarMaxima[i][0:3]
         #rotate a vector 148 degrees from the phosphate bisector
         phosAngle = angle(phos5, curSugar, phos3)
         phos5vector = minus(phos5, curSugar)
         axis = crossProd(minus(phos3, curSugar), phos5vector)
         axis = scalarProd(axis, 1/magnitude(axis))
         c1vec = rotate(phos5vector, axis, 148.539123-phosAngle/2)
         
         #scale the vector to the appropriate length
         c1vec = scalarProd(c1vec, 1.235367/magnitude(c1vec))
         
         #rotate the vector about the phosphate bisector
         phosBisectorAxis = rotate(phos5vector, axis, -phosAngle/2)
         phosBisectorAxis = scalarProd(phosBisectorAxis, 1/magnitude(phosBisectorAxis)) 
         c1vec = rotate(c1vec, phosBisectorAxis, -71.409162)
         
         sugarMaxima[i][0:3] = plus(c1vec, curSugar)
     
     return sugarMaxima