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
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
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
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
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]
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
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)
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
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]
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
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
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
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
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