From c0f2fc05f6135272b17939356790441e65d0c122 Mon Sep 17 00:00:00 2001 From: Kristaps Ermanis Date: Thu, 27 Aug 2015 09:36:47 +0100 Subject: [PATCH] Initial commit of PyDP4 v0.4 --- ConfPrune.pyx | 435 +++++++++++++++++++++++++ DP4.jar | Bin 0 -> 48500 bytes DP4.py | 229 ++++++++++++++ FiveConf.py | 259 +++++++++++++++ Gaussian.py | 437 +++++++++++++++++++++++++ InchiGen.py | 666 +++++++++++++++++++++++++++++++++++++++ LICENSE | 23 ++ MacroModel.py | 262 +++++++++++++++ NMRDP4GTF.py | 469 +++++++++++++++++++++++++++ NWChem.py | 308 ++++++++++++++++++ NucCErr.pkl | Bin 0 -> 1637 bytes NucCErrOpt.pkl | Bin 0 -> 1637 bytes NucHErr.pkl | Bin 0 -> 1322 bytes NucHErrOpt.pkl | Bin 0 -> 1322 bytes PyDP4.py | 435 +++++++++++++++++++++++++ README | 273 ++++++++++++++++ Tinker.py | 264 ++++++++++++++++ TreeRenum.py | 166 ++++++++++ default.com | 18 ++ default.key | 11 + default1.com | 17 + nmrPredictGaussian.class | Bin 0 -> 3616 bytes nmrPredictGaussian.java | 208 ++++++++++++ nmrPredictNWChem.py | 99 ++++++ sdf2tinkerxyz | Bin 0 -> 60139 bytes 25 files changed, 4579 insertions(+) create mode 100644 ConfPrune.pyx create mode 100644 DP4.jar create mode 100644 DP4.py create mode 100644 FiveConf.py create mode 100644 Gaussian.py create mode 100644 InchiGen.py create mode 100644 LICENSE create mode 100644 MacroModel.py create mode 100644 NMRDP4GTF.py create mode 100644 NWChem.py create mode 100644 NucCErr.pkl create mode 100644 NucCErrOpt.pkl create mode 100644 NucHErr.pkl create mode 100644 NucHErrOpt.pkl create mode 100755 PyDP4.py create mode 100644 README create mode 100755 Tinker.py create mode 100755 TreeRenum.py create mode 100644 default.com create mode 100644 default.key create mode 100644 default1.com create mode 100644 nmrPredictGaussian.class create mode 100644 nmrPredictGaussian.java create mode 100644 nmrPredictNWChem.py create mode 100755 sdf2tinkerxyz diff --git a/ConfPrune.pyx b/ConfPrune.pyx new file mode 100644 index 0000000..76f60fe --- /dev/null +++ b/ConfPrune.pyx @@ -0,0 +1,435 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jan 30 12:33:18 2015 + +@author: ke291 + +Cython file for conformer alignment and RMSD pruning. Called by Gaussian.py +and NWChem.py +""" + +from math import sqrt +from libc.stdlib cimport malloc, free +""" +Main function of this file. Takes an array of [x, y, z] for each molecule, +as well as a list of atom symbols in the same order as their coordinates + +Translates both molecules to origin, gets the rotation matrix from quatfit +and rotates the second molecule to align with the first +""" +def RMSDPrune(conformers, atoms, cutoff): + + ToDel = [] + cdef long int c1, c2, c + cdef long int l = len(conformers) + cdef double res + cdef double *RMSDMatrix = malloc(l * l * sizeof(double)) + + for c1 in range(0, l): + for c2 in range(c1, l): + if c1==c2: + RMSDMatrix[c2 + c1*l]=0.0 + else: + res = AlignedRMS(conformers[c1], conformers[c2], atoms) + RMSDMatrix[c2 + c1*l] = res + RMSDMatrix[c1 + c2*l] = res + #Check for similar conformations + for c1 in range(0, l): + for c2 in range(0, l): + if c1!=c2 and (not c1 in ToDel) and (not c2 in ToDel): + #if Align.AlignedRMS(conformers[c1], conformers[c2], atoms) < cutoff: + # ToDel.append(c2) + if RMSDMatrix[c2 + c1*l]malloc(l * l * sizeof(double)) + + for c1 in range(0, l): + for c2 in range(c1, l): + if c1==c2: + RMSDMatrix[c2 + c1*l]=0.0 + else: + res = AlignedRMS(conformers[c1], conformers[c2], atoms) + RMSDMatrix[c2 + c1*l] = res + RMSDMatrix[c1 + c2*l] = res + #Check for similar conformations + for c1 in range(0, l): + for c2 in range(0, l): + if c1!=c2 and (not c1 in ToDel) and (not c2 in ToDel): + if RMSDMatrix[c2 + c1*l]ConfLimit: + AdjCutoff +=0.2 + ToDel = [] + for c1 in range(0, l): + for c2 in range(0, l): + if c1!=c2 and (not c1 in ToDel) and (not c2 in ToDel): + if RMSDMatrix[c2 + c1*l]0.0): + dma = d[j] - d[i] + if (abs(dma)+abs(b) < abs(dma)): + t = b/dma + else: + q = 0.5 * dma / b + t = fsign(1.0 / (abs(q) + sqrt(1.0 + q*q)), q) + + c = 1.0 / sqrt(t*t + 1.0) + s = t * c + a[i][j] = 0.0 + + for k in range(0, i-1): + atemp = c * a[k][i] - s * a[k][j] + a[k][j] = s * a[k][i] + c * a[k][j] + a[k][i] = atemp + + for k in range(i+1, j): + atemp = c * a[i][k] - s * a[k][j] + a[k][j] = s * a[i][k] + c * a[k][j] + a[i][k] = atemp + + for k in range(j, n): + atemp = c * a[i][k] - s * a[j][k] + a[j][k] = s * a[i][k] + c * a[j][k] + a[i][k] = atemp + + for k in range(0, n): + vtemp = c * v[k][i] - s * v[k][j] + v[k][j] = s * v[k][i] + c * v[k][j] + v[k][i] = vtemp + + dtemp = c * c * d[i] + s * s * d[j] - 2.0 + c * s * b + d[j] = s * s * d[i] + c * c * d[j] + 2.0 * c * s * b + d[i] = dtemp + nrot = l + + for j in range(0, n-1): + k = j + dtemp = d[k] + for i in range(j, n): + if d[i]j: + d[k] = d[j] + d[j] = dtemp + for i in range(0, n): + dtemp = v[i][k] + v[i][k] = v[i][j] + v[i][j] = dtemp + + cdef double ret[5][4] + for i in range(0,4): + for j in range(0,4): + ret[i][j]=v[i][j] + for i in range(0,4): + ret[4][i] = d[i] + + +cdef q2mat(double q[4]): + + u = [[0.0, 0.0, 0.0],[0.0, 0.0, 0.0],[0.0, 0.0, 0.0]] + u[0][0] = q[0]*q[0] + q[1]*q[1] - q[2]*q[2] - q[3]*q[3] + u[1][0] = 2.0 *(q[1] * q[2] - q[0] * q[3]) + u[2][0] = 2.0 *(q[1] * q[3] + q[0] * q[2]) + u[0][1] = 2.0 *(q[2] * q[1] + q[0] * q[3]) + u[1][1] = q[0]*q[0] - q[1]*q[1] + q[2]*q[2] - q[3]*q[3] + u[2][1] = 2.0 *(q[2] * q[3] - q[0] * q[1]) + u[0][2] = 2.0 *(q[3] * q[1] - q[0] * q[2]) + u[1][2] = 2.0 *(q[3] * q[2] + q[0] * q[1]) + u[2][2] = q[0]*q[0] - q[1]*q[1] - q[2]*q[2] + q[3]*q[3] + + return u + +def fsign(a, b): + if (b>=0): + return a + else: + return -a \ No newline at end of file diff --git a/DP4.jar b/DP4.jar new file mode 100644 index 0000000000000000000000000000000000000000..0e223091469806046a858abf60c3a89a1ee5dd72 GIT binary patch literal 48500 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 literal 0 HcmV?d00001 diff --git a/DP4.py b/DP4.py new file mode 100644 index 0000000..79d1ca6 --- /dev/null +++ b/DP4.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed May 27 14:18:37 2015 +Updated on July 30 14:18:37 2015 +@author: ke291 + +Equivalent and compact port of DP4.jar to python. The results +produced are essentially equivalent, but not identical due to different +floating point precision used in the Python (53 bits) and Java (32 bits) +implementation. +""" +from scipy import stats +import pickle +import re +import bisect + +meanC = 0.0 +meanH = 0.0 +stdevC = 2.269372270818724 +stdevH = 0.18731058105269952 +output = [] + + +def main(Clabels, Cvalues, Hlabels, Hvalues, Cexp, Hexp, settings): + + Print(str(Cexp)) + Print(str(Hexp)) + + C_cdp4 = [] + H_cdp4 = [] + Comb_cdp4 = [] + + for isomer in range(0, len(Cvalues)): + + sortedClabels, sortedCvalues, sortedCexp =\ + AssignExpNMR(Clabels, Cvalues[isomer], Cexp) + scaledC = ScaleNMR(sortedCvalues, sortedCexp) + + sortedHlabels, sortedHvalues, sortedHexp = \ + AssignExpNMR(Hlabels, Hvalues[isomer], Hexp) + scaledH = ScaleNMR(sortedHvalues, sortedHexp) + + ScaledErrorsC = [sortedCexp[i] - scaledC[i] + for i in range(0, len(scaledC))] + ScaledErrorsH = [sortedHexp[i] - scaledH[i] + for i in range(0, len(scaledH))] + + Print("\nAssigned shifts for isomer " + str(isomer+1) + ": ") + PrintNMR('C', sortedClabels, sortedCvalues, scaledC, sortedCexp) + Print("Max C error: " + format(max(ScaledErrorsC, key=abs), "6.2f")) + PrintNMR('H', sortedHlabels, sortedHvalues, scaledH, sortedHexp) + Print("Max H error: " + format(max(ScaledErrorsH, key=abs), "6.2f")) + + if settings.PDP4: + C_cdp4.append(CalculateCDP4(ScaledErrorsC, meanC, stdevC)) + H_cdp4.append(CalculateCDP4(ScaledErrorsH, meanH, stdevH)) + elif settings.EP5: + C_cdp4.append(CalculatePDP4(ScaledErrorsC, meanC, stdevC)) + #C_cdp4.append(CalculateKDE(ScaledErrorsC, settings.ScriptDir + '/NucCErr.pkl')) + H_cdp4.append(CalculatePDP4(ScaledErrorsH, meanH, stdevH)) + #H_cdp4.append(CalculateKDE(ScaledErrorsH, settings.ScriptDir + '/NucHErr.pkl')) + Comb_cdp4.append(C_cdp4[-1]*H_cdp4[-1]) + Print("\nDP4 based on C: " + format(C_cdp4[-1], "6.2e")) + Print("DP4 based on H: " + format(H_cdp4[-1], "6.2e")) + + relCDP4 = [(100*x)/sum(C_cdp4) for x in C_cdp4] + relHDP4 = [(100*x)/sum(H_cdp4) for x in H_cdp4] + relCombDP4 = [(100*x)/sum(Comb_cdp4) for x in Comb_cdp4] + + PrintRelDP4('both carbon and proton data', relCombDP4) + PrintRelDP4('carbon data only', relCDP4) + PrintRelDP4('proton data only', relHDP4) + + return output + + +def AssignExpNMR(labels, calcShifts, exp): + + #Replace all 'or' and 'OR' with ',', remove all spaces and 'any' + exp = re.sub(r"or|OR", ',', exp, flags=re.DOTALL) + exp = re.sub(r" |any", '', exp, flags=re.DOTALL) + + #Get all assignments, split mulitassignments + ExpLabels = re.findall(r"(?<=\().*?(?=\))", exp, flags=re.DOTALL) + ExpLabels = [x.split(',') for x in ExpLabels] + + #Remove assignments and get shifts + ShiftData = (re.sub(r"\(.*?\)", "", exp, flags=re.DOTALL)).split(',') + expShifts = [float(x) for x in ShiftData] + + #Prepare sorted calculated data with labels and sorted exp data + sortedCalc = sorted(calcShifts) + sortedExp = sorted(expShifts) + sortedExpLabels = SortExpAssignments(expShifts, ExpLabels) + sortedCalcLabels = [] + for v in sortedCalc: + index = calcShifts.index(v) + sortedCalcLabels.append(labels[index]) + + assignedExpLabels = ['' for i in range(0, len(sortedExp))] + + #First pass - assign the unambiguous shifts + for v in range(0, len(sortedExp)): + if len(sortedExpLabels[v]) == 1 and sortedExpLabels[v][0] != '': + #Check that assignment exists in computational data + if sortedExpLabels[v][0] in labels: + assignedExpLabels[v] = sortedExpLabels[v][0] + else: + Print("Label " + sortedExpLabels[v][0] + + " not found in among computed shifts, please check NMR assignment.") + quit() + #Second pass - assign shifts from a limited set + for v in range(0, len(sortedExp)): + if len(sortedExpLabels[v]) != 1 and sortedExpLabels[v][0] != '': + for l in sortedCalcLabels: + if l in sortedExpLabels[v] and l not in assignedExpLabels: + assignedExpLabels[v] = l + break + #Final pass - assign unassigned shifts in order + for v in range(0, len(sortedExp)): + if sortedExpLabels[v][0] == '': + for l in sortedCalcLabels: # Take the first free label + if l not in assignedExpLabels: + assignedExpLabels[v] = l + break + sortedCalc = [] + #Rearrange calc values to match the assigned labels + for l in assignedExpLabels: + index = labels.index(l) + sortedCalc.append(calcShifts[index]) + + return assignedExpLabels, sortedCalc, sortedExp + + +def SortExpAssignments(shifts, assignments): + tempshifts = list(shifts) + tempassignments = list(assignments) + sortedassignments = [] + while len(tempassignments) > 0: + index = tempshifts.index(min(tempshifts)) + sortedassignments.append(tempassignments[index]) + tempshifts.pop(index) + tempassignments.pop(index) + return sortedassignments + + +#Scale the NMR shifts +def ScaleNMR(calcShifts, expShifts): + slope, intercept, r_value, p_value, std_err = stats.linregress(expShifts, + calcShifts) + scaled = [(x-intercept)/slope for x in calcShifts] + return scaled + + +def PrintNMR(nucleus, labels, values, scaled, exp): + Print("\nAssigned " + nucleus + + " shifts: (label, calc, corrected, exp, error)") + for i in range(0, len(labels)): + Print(format(labels[i], "6s") + ' ' + format(values[i], "6.2f") + ' ' + + format(scaled[i], "6.2f") + ' ' + format(exp[i], "6.2f") + ' ' + + format(exp[i]-scaled[i], "6.2f")) + + +def PrintRelDP4(title, RelDP4): + Print("\nResults of DP4 using " + title + ":") + for i in range(0, len(RelDP4)): + Print("Isomer " + str(i+1) + ": " + format(RelDP4[i], "4.1f") + "%") + + +def Print(s): + print s + output.append(s) + + +def CalculateCDP4(errors, expect, stdev): + cdp4 = 1.0 + for e in errors: + z = abs((e-expect)/stdev) + cdp4 = cdp4*2*stats.norm.cdf(-z) + return cdp4 + + +#Alternative function using probability density function instead of cdf +def CalculatePDP4(errors, expect, stdev): + pdp4 = 1.0 + for e in errors: + #z = (e-expect)/stdev + #pdp4 = pdp4*stats.norm.pdf(z) + pdp4 = pdp4*stats.norm(expect, stdev).pdf(e) + return pdp4 + + +#use as CalculateKDE(errors, 'NucCErr.pkl') for C or +#CalculateKDE(errors, 'NucHErr.pkl') for H +#load empirical error data from file and use KDE to construct pdf +def CalculateKDE(errors, PickleFile): + + pkl_file = open(PickleFile, 'rb') + ErrorData = pickle.load(pkl_file) + kde = stats.gaussian_kde(ErrorData) + + ep5 = 1.0 + for e in errors: + #z = (e-expect)/stdev + #pdp4 = pdp4*stats.norm.pdf(z) + ep5 = ep5*float(kde(e)[0]) + return ep5 + + +#use as CalculateRKDE(errors, 'RKDEC.pkl') for C or +#CalculateKDE(errors, 'RKDEH.pkl') for H +#load empirical error data from file and use KDE to construct several pdfs, +#one for each chemical shift region +def CalculateRKDE(errors, shifts, PickleFile): + + #Load the data + pkl_file = open(PickleFile, 'rb') + regions, RErrors = pickle.load(pkl_file) + + #Reconstruct the distributions for each region + kdes = [] + for es in RErrors: + kdes.append(stats.gaussian_kde(es)) + + ep5 = 1.0 + for i, e in enumerate(errors): + region = bisect.bisect_left(regions, shifts[i]) + ep5 = ep5*float((kdes[region])(e)[0]) + return ep5 diff --git a/FiveConf.py b/FiveConf.py new file mode 100644 index 0000000..672fddc --- /dev/null +++ b/FiveConf.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Dec 17 15:20:18 2014 + +@author: ke291 + +Gets called by PyDP4.py if automatic 5-membered cycle corner-flipping is used. +""" + +import numpy as np +from math import sqrt, pi, cos, sin, acos +import scipy.optimize as sciopt +import sys +sys.path.append( + '/home/ke291/Tools/openbabel-install/lib/python2.7/site-packages/') +from openbabel import * + + +def main(f, settings): + + """ + Find the axis atoms + Find all the atoms to be rotated + + Rotate it and the substiuents to the other side of the plane + """ + obconversion = OBConversion() + obconversion.SetInFormat("sdf") + obmol = OBMol() + + obconversion.ReadFile(obmol, f) + + obmol.ConnectTheDots() + + #Find the atoms composing furan ring + Rings = obmol.GetSSSR() + furan = [] + for ring in Rings: + if len(settings.RingAtoms) == 5: + if all(x in ring._path for x in settings.RingAtoms): + furan = ring + break + else: + if ring.Size() == 5 and not ring.IsAromatic(): + furan = ring + break + + if furan == []: + "No five membered rings to rotate. Quitting..." + quit() + #Find the plane of the 5-membered ring and the outlying atom + norm, d, outAtom = FindFuranPlane(obmol, furan) + + #Find the atoms connected to the outlying atom and sort them + #as either part of the ring(axis atoms) or as atoms to be rotated + AxisAtoms = [] + RotAtoms = [] + + for NbrAtom in OBAtomAtomIter(outAtom): + #if NbrAtom.IsInRingSize(5): + if furan.IsInRing(NbrAtom.GetIdx()): + AxisAtoms.append(NbrAtom) + else: + RotAtoms.append(NbrAtom) + FindSubstAtoms(NbrAtom, outAtom, RotAtoms) + + #Simple switch to help detect if the atoms are rotated the right way + WasAbove90 = False + angle = FindRotAngle(AxisAtoms[0], AxisAtoms[1], outAtom, norm) + if angle > 0.5*pi: + WasAbove90 = True + rotangle = 2*(angle-0.5*pi) + else: + WasAbove90 = False + rotangle = 2*(0.5*pi-angle) + OldAtomCoords = outAtom.GetVector() + print "Atom " + str(outAtom.GetAtomicNum()) + " will be rotated by " +\ + str(rotangle*57.3) + ' degrees' + RotateAtom(outAtom, AxisAtoms[0], AxisAtoms[1], rotangle) + angle2 = FindRotAngle(AxisAtoms[0], AxisAtoms[1], outAtom, norm) + + #if the atom is on the same side of the plane as it was, + # it has been rotated in the wrong direction + if ((angle2 > 0.5*pi) and WasAbove90) or ((angle2 < 0.5*pi) and not WasAbove90): + #Flip the sign of the rotation angle, restore the coords + #and rotate the atom in the opposite direction + print "Atom was rotated the wrong way, switching the direction" + rotangle = -rotangle + outAtom.SetVector(OldAtomCoords) + RotateAtom(outAtom, AxisAtoms[0], AxisAtoms[1], rotangle) + + RotatedAtoms = [] # Index to make sure that atoms are not rotated twice + for atom in RotAtoms: + if atom not in RotatedAtoms: + RotateAtom(atom, AxisAtoms[0], AxisAtoms[1], rotangle) + RotatedAtoms.append(atom) + else: + print "Atom already rotated, skipping" + + obconversion.SetOutFormat("sdf") + obconversion.WriteFile(obmol, f[:-4] + 'rot.sdf') + + +#Recursively finds all the atoms connected to the input +def FindSubstAtoms(atom, outAtom, al): + + indexes = [a.GetIdx() for a in al] + for NbrAtom in OBAtomAtomIter(atom): + + if (NbrAtom.GetIdx() not in indexes) and\ + (NbrAtom.GetIdx() != outAtom.GetIdx()): + al.append(NbrAtom) + FindSubstAtoms(NbrAtom, outAtom, al) + + +#Rotate atom around and axis by an angle +def RotateAtom(atom, AxisAtom1, AxisAtom2, angle): + + [u, v, w] = GetUnitVector(AxisAtom1, AxisAtom2) + [x, y, z] = [atom.x(), atom.y(), atom.z()] + [a, b, c] = [(AxisAtom1.x()+AxisAtom1.x())/2, (AxisAtom1.y()+AxisAtom1.y())/2,\ + (AxisAtom1.z()+AxisAtom1.z())/2] + + X = (a*(v**2 + w**2) - u*(b*v+c*w-u*x-v*y-w*z))*(1-cos(angle))+x*cos(angle)\ + +(-1*c*v+b*w-w*y+v*z)*sin(angle) + Y = (b*(u**2 + w**2) - v*(a*u+c*w-u*x-v*y-w*z))*(1-cos(angle))+y*cos(angle)\ + +(c*u-a*w+w*x-u*z)*sin(angle) #was _+_u*z)*sin(angle) + Z = (c*(u**2 + v**2) - w*(a*u+b*v-u*x-v*y-w*z))*(1-cos(angle))+z*cos(angle)\ + +(-1*b*u+a*v-v*x+u*y)*sin(angle) + + atom.SetVector(X, Y, Z) + + +def GetUnitVector(Atom1, Atom2): + vector = [] + vector.append(Atom2.x() - Atom1.x()) + vector.append(Atom2.y() - Atom1.y()) + vector.append(Atom2.z() - Atom1.z()) + + length = np.linalg.norm(vector) + return [x/length for x in vector] + + +#Finds the angle by which atoms need to be rotated by taking the angle +#the atom is out of the plane (the 2 neighbor atoms being the axis) +#and doubling it +def FindRotAngle(AxisAtom1, AxisAtom2, OutAtom, Normal): + + start = [] + start.append((AxisAtom1.x() + AxisAtom2.x())/2) + start.append((AxisAtom1.y() + AxisAtom2.y())/2) + start.append((AxisAtom1.z() + AxisAtom2.z())/2) + + vector = [] + vector.append(OutAtom.x() - start[0]) + vector.append(OutAtom.y() - start[1]) + vector.append(OutAtom.z() - start[2]) + + #Angle between plane normal and OOP atom + vangle = angle(vector, Normal) + + #print "Measured angle: " + str(vangle*57.3) + return vangle + + +def crossproduct(v1, v2): + product = [0, 0, 0] + product[0] = v1[1]*v2[2]-v1[2]*v2[1] + product[1] = v1[2]*v2[0]-v1[0]*v2[2] + product[2] = v1[0]*v2[1]-v1[1]*v2[0] + return product + + +def dotproduct(v1, v2): + return sum((a*b) for a, b in zip(v1, v2)) + + +def length(v): + return sqrt(dotproduct(v, v)) + + +def angle(v1, v2): + return acos(dotproduct(v1, v2) / (length(v1) * length(v2))) + + +"""Finds planes for every 3 atoms, calculates distances to the plane +for the other 2 atoms andchoose the plane with the smallest smallest distance +""" +def FindFuranPlane(mol, furan): + + atomIds = furan._path + atoms = [] + + for i in atomIds: + atoms.append(mol.GetAtom(i)) + + MinError = 100.0 + + for atom in atoms: + pats = [a for a in atoms if a != atom] + norm, d, error = LstSqPlane(pats[0], pats[1], pats[2], pats[3]) + if error < MinError: + MinError = error + MaxNorm = norm + MaxD = d + OutAtom = atom + + return MaxNorm, MaxD, OutAtom + + +#Given 3 atoms, finds a plane defined by a normal vector and d +def FindPlane(atom1, atom2, atom3): + + vector1 = [atom2.x() - atom1.x(), atom2.y() - atom1.y(), + atom2.z() - atom1.z()] + vector2 = [atom3.x() - atom1.x(), atom3.y() - atom1.y(), + atom3.z() - atom1.z()] + cross_product = [vector1[1] * vector2[2] - vector1[2] * vector2[1], + -1 * vector1[0] * vector2[2] - vector1[2] * vector2[0], + vector1[0] * vector2[1] - vector1[1] * vector2[0]] + + d = cross_product[0] * atom1.x() - cross_product[1] * atom1.y() + \ + cross_product[2] * atom1.z() + + return cross_product, d + + +def LstSqPlane(atom1, atom2, atom3, atom4): + + # Inital guess of the plane + [a0, b0, c0], d0 = FindPlane(atom1, atom2, atom3) + + f = lambda (a, b, c, d): PlaneError([atom1, atom2, atom3, atom4], a, b, c, d) + res = sciopt.minimize(f, (a0, b0, c0, d0), method='nelder-mead') + plane = list(res.x) + + return plane[:3], plane[3], f(plane) + + +def PlaneError(atoms, a, b, c, d): + dists = [] + for atom in atoms: + dists.append(abs(PointPlaneDist([a, b, c], d, atom))) + return sum(dists)/len(dists) + + +#Calculates distance from an atom to a plane +def PointPlaneDist(norm, d, atom): + + point = [] + + point.append(atom.x()) + point.append(atom.y()) + point.append(atom.z()) + + a = norm[0]*point[0] + norm[1]*point[1] + norm[2]*point[2] + d + b = sqrt(norm[0]**2 + norm[1]**2 + norm[2]**2) + + return a/b diff --git a/Gaussian.py b/Gaussian.py new file mode 100644 index 0000000..28200e9 --- /dev/null +++ b/Gaussian.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python +from __future__ import division +# -*- coding: utf-8 -*- +""" +Created on Wed Nov 19 15:56:54 2014 + +@author: ke291 + +Contains all of the Gaussian specific code for input generation and calculation +execution. Called by PyDP4.py. +""" + +import Tinker +import MacroModel + +import subprocess +import socket +import os +import time +import sys +import glob +import pyximport +pyximport.install() +import ConfPrune + + +def SetupGaussian(MMoutp, Gausinp, numDigits, settings, adjRMSDcutoff): + + if settings.MMTinker: + #Reads conformer geometry, energies and atom labels from Tinker output + (atoms, conformers, charge) = Tinker.ReadTinker(MMoutp, settings) + else: + (atoms, conformers, charge) = MacroModel.ReadMacromodel(MMoutp, + settings) + + #Prune similar conformations, if the number exceeds the limit + if len(conformers) > settings.PerStructConfLimit: + pruned = ConfPrune.RMSDPrune(conformers, atoms, adjRMSDcutoff) + else: + pruned = conformers + + print str(len(conformers) - len(pruned)) +\ + " or " + "{:.1f}".format(100*(len(conformers) - len(pruned)) / + len(conformers))+"% of conformations have been pruned based on " +\ + str(adjRMSDcutoff) + " angstrom cutoff" + + for num in range(0, len(pruned)): + filename = Gausinp+str(num+1).zfill(3) + if not settings.DFTOpt: + WriteGausFile(filename, pruned[num], atoms, charge, settings) + else: + WriteGausFileOpt(filename, pruned[num], atoms, charge, settings) + + print str(len(pruned)) + " .com files written" + + +#Adjust the RMSD cutoff to keep the conformation numbers reasonable +def AdaptiveRMSD(MMoutp, settings): + + if settings.MMTinker: + #Reads conformer geometry, energies and atom labels from Tinker output + (atoms, conformers, charge) = Tinker.ReadTinker(MMoutp, settings) + else: + (atoms, conformers, charge) = MacroModel.ReadMacromodel(MMoutp, + settings) + + return ConfPrune.AdaptRMSDPrune(conformers, atoms, + settings.InitialRMSDcutoff, + settings.PerStructConfLimit) + + +def WriteGausFile(Gausinp, conformer, atoms, charge, settings): + + f = file(Gausinp + '.com', 'w') + f.write('%mem=2800MB\n%chk='+Gausinp + '.chk\n') + #f.write('# b3lyp/6-31g(d,p) Opt nmr=giao\n') + if settings.Solvent != '': + f.write('# b3lyp/6-31g(d,p) nmr=giao scrf=(solvent=' + + settings.Solvent+')\n') + else: + f.write('# b3lyp/6-31g(d,p) nmr=giao\n') + f.write('\n'+Gausinp+'\n\n') + f.write(str(charge) + ' 1\n') + + natom = 0 + + for atom in conformer: + f.write(atoms[natom] + ' ' + atom[1] + ' ' + atom[2] + ' ' + + atom[3] + '\n') + natom = natom + 1 + f.write('\n') + f.close() + + +def WriteGausFileOpt(Gausinp, conformer, atoms, charge, settings): + + #write the initial DFT geometry optimisation input file first + f1 = file(Gausinp + 'a.com', 'w') + f1.write('%mem=2800MB\n%chk='+Gausinp + '.chk\n') + + if settings.Solvent != '': + f1.write('# b3lyp/6-31g(d,p) Opt=(maxcycles=30) scrf=(solvent=' + + settings.Solvent+')\n') + else: + f1.write('# b3lyp/6-31g(d,p) Opt=(maxcycles=30)\n') + + f1.write('\n'+Gausinp+'\n\n') + f1.write(str(charge) + ' 1\n') + + natom = 0 + + for atom in conformer: + f1.write(atoms[natom] + ' ' + atom[1] + ' ' + atom[2] + ' ' + + atom[3] + '\n') + natom = natom + 1 + f1.write('\n') + f1.close() + + #Write the nmr prediction input file, + #using the geometry from checkpoint file + f2 = file(Gausinp + 'b.com', 'w') + f2.write('%mem=2800MB\n%chk='+Gausinp + '.chk\n') + if settings.Solvent != '': + f2.write('# b3lyp/6-31g(d,p) nmr=giao geom=checkpoint scrf=(solvent=' + + settings.Solvent+')\n') + else: + f2.write('# b3lyp/6-31g(d,p) nmr=giao geom=checkpoint\n') + f2.write('\n'+Gausinp+'\n\n') + f2.write(str(charge) + ' 1\n') + f2.write('\n') + f2.close() + + +def GetFiles2Run(inpfiles, settings): + #Get the names of all relevant input files + GinpFiles = [] + for filename in inpfiles: + if not settings.DFTOpt: + GinpFiles = GinpFiles + glob.glob(filename + 'ginp???.com') + else: + GinpFiles = GinpFiles + glob.glob(filename + 'ginp???a.com') + Files2Run = [] + + #for every input file check that there is a completed output file, + #delete the incomplete outputs and add the inputs to be done to Files2Run + for filename in GinpFiles: + if not settings.DFTOpt: + if not os.path.exists(filename[:-3]+'out'): + Files2Run.append(filename) + else: + if IsGausCompleted(filename[:-3] + 'out'): + #print filename[:-3]+'out already exists' + continue + else: + os.remove(filename[:-3] + 'out') + Files2Run.append(filename) + else: + if not os.path.exists(filename[:-5]+'.out'): + Files2Run.append(filename) + else: + if IsGausCompleted(filename[:-5] + '.out'): + #print filename[:-3]+'out already exists' + continue + else: + os.remove(filename[:-5] + '.out') + Files2Run.append(filename) + + return Files2Run + + +def IsGausCompleted(f): + Gfile = open(f, 'r') + outp = Gfile.readlines() + Gfile.close() + if len(outp) < 10: + return False + if "Normal termination" in outp[-1]: + return True + else: + return False + + +#Still need addition of support for geometry optimisation +def RunOnZiggy(folder, queue, GausFiles, settings): + + print "ziggy GAUSSIAN job submission script\n" + + #Check that folder does not exist, create job folder on ziggy + outp = subprocess.check_output('ssh ziggy ls', shell=True) + if folder in outp: + print "Folder exists on ziggy, choose another folder name." + return + + outp = subprocess.check_output('ssh ziggy mkdir ' + folder, shell=True) + + #Write the qsub scripts + for f in GausFiles: + if not settings.DFTOpt: + WriteSubScript(f[:-4], queue, folder, settings) + else: + WriteSubScriptOpt(f[:-4], queue, folder, settings) + print str(len(GausFiles)) + ' .qsub scripts generated' + + #Upload .com files and .qsub files to directory + print "Uploading files to ziggy..." + for f in GausFiles: + if not settings.DFTOpt: + outp = subprocess.check_output('scp ' + f +' ziggy:~/' + folder, + shell=True) + else: + outp = subprocess.check_output('scp ' + f[:-4] +'a.com ziggy:~/' + + folder, shell=True) + outp = subprocess.check_output('scp ' + f[:-4] +'b.com ziggy:~/' + + folder, shell=True) + outp = subprocess.check_output('scp ' + f[:-4] +'.qsub ziggy:~/' + + folder, shell=True) + + print str(len(GausFiles)) + ' .com and .qsub files uploaded to ziggy' + + #Launch the calculations + for f in GausFiles: + job = '~/' + folder + '/' + f[:-4] + outp = subprocess.check_output('ssh ziggy qsub -q ' + queue + ' -o ' + + job + '.log -e ' + job + '.err ' + job + '.qsub', shell=True) + + print str(len(GausFiles)) + ' jobs submitted to the queue on ziggy' + + outp = subprocess.check_output('ssh ziggy showq', shell=True) + if settings.user in outp: + print "Jobs are running on ziggy" + + Jobs2Complete = list(GausFiles) + n2complete = len(Jobs2Complete) + + #Check and report on the progress of calculations + while len(Jobs2Complete) > 0: + JustCompleted = [job for job in Jobs2Complete if + IsZiggyGComplete(job[:-3] + 'out', folder, settings)] + Jobs2Complete[:] = [job for job in Jobs2Complete if + not IsZiggyGComplete(job[:-3] + 'out', folder, settings)] + if n2complete != len(Jobs2Complete): + n2complete = len(Jobs2Complete) + print str(n2complete) + " remaining." + + time.sleep(60) + + #When done, copy the results back + print "\nCopying the output files back to localhost..." + print 'ssh ziggy scp /home/' + settings.user + '/' + folder + '/*.out ' +\ + socket.getfqdn() + ':' + os.getcwd() + outp = subprocess.check_output('ssh ziggy scp /home/' + settings.user + + '/' + folder + '/*.out ' + socket.getfqdn() + + ':' + os.getcwd(), shell=True) + + #Delete the *.chk files from /sharedscratch/ + print "\nCleaning up - deleting the *.chk files form /sharedscratch/" + for f in GausFiles: + outp = subprocess.check_output('ssh ziggy rm -r /sharedscratch/' + + settings.user + '/' + f[:-4], shell=True) + + +def WriteSubScript(GausJob, queue, ZiggyJobFolder, settings): + + if not (os.path.exists(GausJob+'.com')): + print "The input file " + GausJob + ".com does not exist. Exiting..." + return + + #Create the submission script + QSub = open(GausJob + ".qsub", 'w') + + #Choose the queue + QSub.write('#PBS -q ' + queue + '\n#PBS -l nodes=1:ppn=1\n#\n') + + #define input files and output files + QSub.write('file=' + GausJob + '\n\n') + QSub.write('inpfile=${file}.com\noutfile=${file}.out\n') + + #define cwd and scratch folder and ask the machine + #to make it before running the job + QSub.write('HERE=/home/' + settings.user +'/' + ZiggyJobFolder + '\n') + QSub.write('SCRATCH=/sharedscratch/' + settings.user + '/' + + GausJob + '\n') + QSub.write('mkdir ${SCRATCH}\n') + + #Setup GAUSSIAN environment variables + QSub.write('set OMP_NUM_THREADS=1\n') + QSub.write('export GAUSS_EXEDIR=/usr/local/shared/gaussian/em64t/09-D01/g09\n') + QSub.write('export g09root=/usr/local/shared/gaussian/em64t/09-D01\n') + QSub.write('export PATH=/usr/local/shared/gaussian/em64t/09-D01/g09:$PATH\n') + QSub.write('export GAUSS_SCRDIR=$SCRATCH\n') + + #copy the input file to scratch + QSub.write('cp ${HERE}/${inpfile} $SCRATCH\ncd $SCRATCH\n') + + #write useful info to the job output file (not the gaussian) + QSub.write('echo "Starting job $PBS_JOBID"\necho\n') + QSub.write('echo "PBS assigned me this node:"\ncat $PBS_NODEFILE\necho\n') + + QSub.write('ln -s $HERE/$outfile $SCRATCH/$outfile\n') + QSub.write('$GAUSS_EXEDIR/g09 < $inpfile > $outfile\n') + + #Cleanup + QSub.write('mkdir ${HERE}/${file}\n') + QSub.write('cp ${SCRATCH}/*.chk $HERE/${file}/\n') + QSub.write('rm -f ${SCRATCH}/*\n') + QSub.write('cp $HERE/${file}/*.chk ${SCRATCH}/\n') + QSub.write('rm -r ${HERE}/${file}\n') + QSub.write('qstat -f $PBS_JOBID\n') + + QSub.close() + +#Function to write ziggy script when dft optimisation is used +def WriteSubScriptOpt(GausJob, queue, ZiggyJobFolder, settings): + + if not (os.path.exists(GausJob+'a.com')): + print "The input file " + GausJob + "a.com does not exist. Exiting..." + return + if not (os.path.exists(GausJob+'b.com')): + print "The input file " + GausJob + "b.com does not exist. Exiting..." + return + + #Create the submission script + QSub = open(GausJob + ".qsub", 'w') + + #Choose the queue + QSub.write('#PBS -q ' + queue + '\n#PBS -l nodes=1:ppn=1\n#\n') + + #define input files and output files + QSub.write('file=' + GausJob + '\n\n') + QSub.write('inpfile1=${file}a.com\ninpfile2=${file}b.com\n') + QSub.write('outfile1=${file}temp.out\noutfile2=${file}.out\n') + + #define cwd and scratch folder and ask the machine + #to make it before running the job + QSub.write('HERE=/home/' + settings.user +'/' + ZiggyJobFolder + '\n') + QSub.write('SCRATCH=/sharedscratch/' + settings.user + '/' + GausJob + '\n') + QSub.write('mkdir ${SCRATCH}\n') + + #Setup GAUSSIAN environment variables + QSub.write('set OMP_NUM_THREADS=1\n') + QSub.write('export GAUSS_EXEDIR=/usr/local/shared/gaussian/em64t/09-D01/g09\n') + QSub.write('export g09root=/usr/local/shared/gaussian/em64t/09-D01\n') + QSub.write('export PATH=/usr/local/shared/gaussian/em64t/09-D01/g09:$PATH\n') + QSub.write('export GAUSS_SCRDIR=$SCRATCH\n') + + #copy the input files to scratch + QSub.write('cp ${HERE}/${inpfile1} $SCRATCH\n') + QSub.write('cp ${HERE}/${inpfile2} $SCRATCH\ncd $SCRATCH\n') + + #write useful info to the job output file (not the gaussian) + QSub.write('echo "Starting job $PBS_JOBID"\necho\n') + QSub.write('echo "PBS assigned me this node:"\ncat $PBS_NODEFILE\necho\n') + + QSub.write('ln -s $HERE/$outfile2 $SCRATCH/$outfile2\n') + QSub.write('$GAUSS_EXEDIR/g09 < $inpfile1 > $outfile1\n') + QSub.write('$GAUSS_EXEDIR/g09 < $inpfile2 > $outfile2\n') + + #Cleanup + QSub.write('mkdir ${HERE}/${file}\n') + QSub.write('cp ${SCRATCH}/*.chk $HERE/${file}/\n') + QSub.write('rm -f ${SCRATCH}/*\n') + QSub.write('cp $HERE/${file}/*.chk ${SCRATCH}/\n') + QSub.write('rm -r ${HERE}/${file}\n') + QSub.write('qstat -f $PBS_JOBID\n') + + QSub.close() + + +def IsZiggyGComplete(f, folder, settings): + + path = '/home/' + settings.user + '/' + folder + '/' + try: + outp1 = subprocess.check_output('ssh ziggy ls ' + path, shell=True) + except subprocess.CalledProcessError, e: + print "ssh ziggy ls failed: " + str(e.output) + return False + if f in outp1: + try: + outp2 = subprocess.check_output('ssh ziggy cat ' + path + f, + shell=True) + except subprocess.CalledProcessError, e: + print "ssh ziggy cat failed: " + str(e.output) + return False + if "Normal termination" in outp2: + return True + return False + + +""" +Change to support tautomers - treat a tautomer as a few extra conformers with +different file names +Correction: Treat them as diastereomers and submit to nmrpredict, but remember +that they are tautomers, so that their populations can be optimized +""" +def RunNMRPredict(numDS, *args): + + TautInputs = [] + Ntaut = [] + NumFiles = [] + arg = 0 + + #Pick all tautomer counts and filenames + for ds in range(0, numDS): + Ntaut.append(int(args[arg])) + arg = arg+1 + TautInputs.append([]) + for taut in range(0, Ntaut[ds]): + TautInputs[ds].append('') + NumFiles.append(0) + for f in glob.glob(args[arg] + 'ginp*.out'): + TautInputs[ds][taut] = TautInputs[ds][taut] + f[:-4] + ' ' + NumFiles[-1] = NumFiles[-1] + 1 + arg = arg+1 + + outputs = [] + + #This loop runs nmrPredict for each diastereomer and collects + #the outputs + """ To change: run nmrpredict for each tautomer seperately""" + for ds in range(0, numDS): + for taut in range(0, Ntaut[ds]): + + #Prepares input for nmrPredict + javafolder = getScriptPath() + jinp = 'CLASSPATH=' + javafolder + \ + ' java nmrPredictGaussian ' + TautInputs[ds][taut] + print jinp + + #Runs java nmrPredict JagName001, ... and collects output + outputs.append(subprocess.check_output(jinp, shell=True)) + #print '\n\n' + outputs[isomer] + + return (outputs, NumFiles, Ntaut) + + +def getScriptPath(): + return os.path.dirname(os.path.realpath(sys.argv[0])) diff --git a/InchiGen.py b/InchiGen.py new file mode 100644 index 0000000..fd8b7b6 --- /dev/null +++ b/InchiGen.py @@ -0,0 +1,666 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Dec 17 15:20:18 2014 + +@author: ke291 + +Code for diastereomer, tautomer and protomer generation via InChI strings. +This file gets called by PyDP4.py if diastereomer and/or tautomer and/or +protomer generation is used. +""" +import sys +sys.path.append('/home/ke291/Tools/openbabel-install/lib/python2.7/site-packages/') +from openbabel import * +import subprocess +import itertools + +MolConPath = '/home/ke291/ChemAxon/MarvinBeans/bin/molconvert' + + +def main(f): + + inchi, aux = GetInchi(f) + + print inchi + + ds_inchis = GenDiastereomers(inchi) + ds_inchis = [FixTautProtons(f, i, aux) for i in ds_inchis] + + for ds in range(0, len(ds_inchis)): + Inchi2Struct(ds_inchis[ds], f[:-4] + str(ds+1), aux) + RestoreNumsSDF(f[:-4] + str(ds+1) + '.sdf', f, aux) + + """taut_inchis = GenTautomers(inchi) + + for taut in range(0, len(taut_inchis)): + Inchi2Struct(taut_inchis[taut], 'taut' + str(taut), aux)""" + + +def GetInchiRenumMap(AuxInfo): + + for l in AuxInfo.split('/'): + if 'N:' in l: + RenumLayer = l + break + amap = [int(x) for x in RenumLayer[2:].split(',')] + return amap + + +def FixTautProtons(f, inchi, AuxInfo): + + #Get tautomeric protons and atoms they are connected to from Inchi + TautProts = GetTautProtons(inchi) + amap = GetInchiRenumMap(AuxInfo) + + #get the correspondence of the Inchi numbers to the source numbers + hmap = [] + for taut in TautProts: + for heavyatom in range(1, len(taut)): + hmap.append([int(taut[heavyatom]), amap[int(taut[heavyatom])-1]]) + + #Read molecule from file + obconversion = OBConversion() + obconversion.SetInFormat("sdf") + obmol = OBMol() + obconversion.ReadFile(obmol, f) + + Fixprotpos = [] + for heavyatom in hmap: + atom = obmol.GetAtom(heavyatom[1]) + for nbratom in OBAtomAtomIter(atom): + if nbratom.GetAtomicNum() == 1: + Fixprotpos.append(heavyatom[0]) + draftFH = [] + for i in range(0, len(Fixprotpos)): + if Fixprotpos[i] not in [a[0] for a in draftFH]: + draftFH.append([Fixprotpos[i], Fixprotpos.count(Fixprotpos[i])]) + + fixedlayer = '/f/h' + for h in draftFH: + if h[1] == 1: + fixedlayer = fixedlayer + str(h[0])+'H,' + else: + fixedlayer = fixedlayer + str(h[0])+'H' + str(h[1]) + ',' + + resinchi = inchi + fixedlayer[:-1] + + return resinchi + + +#Get H connections from sdf file +def GetHcons(f): + obconversion = OBConversion() + obconversion.SetInFormat("sdf") + obmol = OBMol() + obconversion.ReadFile(obmol, f) + Hcons = [] + for atom in OBMolAtomIter(obmol): + idx = atom.GetIdx() + anum = atom.GetAtomicNum() + if anum == 1: + for NbrAtom in OBAtomAtomIter(atom): + Hcons.append([idx, NbrAtom.GetIdx()]) + return Hcons + + +def RestoreNumsSDF(f, fold, AuxInfo): + + #Read molecule from file + obconversion = OBConversion() + obconversion.SetInFormat("sdf") + obmol = OBMol() + obconversion.ReadFile(obmol, f) + #Get the atoms Hs are connected to + oldHcons = GetHcons(fold) + #translate the H connected atoms to the new numbering system + amap = GetInchiRenumMap(AuxInfo) + for i in range(0, len(oldHcons)): + oldHcons[i][1] = amap.index(oldHcons[i][1])+1 + + newHcons = [] + temp = [] + i = 0 + for atom in OBMolAtomIter(obmol): + idx = atom.GetIdx() + anum = atom.GetAtomicNum() + #If atom is hydrogen, check what it is connected to + if anum == 1: + for NbrAtom in OBAtomAtomIter(atom): + newHcons.append([idx, NbrAtom.GetIdx()]) + #Pick the temporary atom + temp.append(atom) + + for i in range(0, len(newHcons)): + conatom = newHcons[i][1] + for b in range(0, len(oldHcons)): + if conatom == oldHcons[b][1]: + amap.append(oldHcons[b][0]) + #remove the number, so that it doesn't get added twice + oldHcons[b][1] = 0 + + newmol = OBMol() + added = [] + + for i in range(1, len(amap)+1): + newn = amap.index(i) + newmol.AddAtom(temp[newn]) + added.append(newn) + + #Final runthrough to check that all atoms have been added, + #tautomeric protons can be missed. If tautomeric proton tracking + #is implemented this can be removed + for i in range(0, len(temp)): + if not i in added: + newmol.AddAtom(temp[i]) + + #Restore the bonds + newmol.ConnectTheDots() + newmol.PerceiveBondOrders() + #Write renumbered molecule to file + obconversion.SetOutFormat("sdf") + obconversion.WriteFile(newmol, f) + + +def GetInchi(f): + + outp = subprocess.check_output(MolConPath + ' inchi ' + f, shell=True) + idata = outp.split('\n') + + aux = idata[1][:] + return idata[0], aux + + +def Inchi2Struct(inchi, f, aux): + + infile = open(f + '.inchi', 'w') + infile.write(inchi) + infile.close() + + outp = subprocess.check_output(MolConPath + ' sdf ' + f + + '.inchi -3:S{fine}[prehydrogenize] -o ' + f + '.sdf', shell=True) + + +def GenProtomers(structf, atoms): + + f = structf + ".sdf" + inchi, aux = GetInchi(f) + print inchi + amap = GetInchiRenumMap(aux) + + prot_atoms = [] + for atom in atoms: + prot_atoms.append(amap.index(atom)) + + finchi = FixTautProtons(f, inchi, aux) + prot_inchis = GenProtInchis(finchi, prot_atoms) + + print prot_inchis + filenames = [] + for prot in range(0, len(prot_inchis)): + Inchi2Struct(prot_inchis[prot], f[:-4] + 'p' + str(prot+1), aux) + RestoreNumsSDF(f[:-4] + 'p' + str(prot+1) + '.sdf', f, aux) + filenames.append(f[:-4] + 'p' + str(prot+1)) + return len(filenames), filenames + + +def GenProtInchis(inchi, atoms): + + #Read and interpret proton counts on heavy atoms from inchi, + # including tautomeric layers + protons, formula, tautomerics, fprotons = ReadProtonCounts(inchi) + + #Construct list of heavy atoms with tautomeric protons and which system + #they belong to + tlist = [[], []] + i = 0 + for tlayer in tautomerics: + temp = tlayer[1:-1].split(',') + tlist[0].extend([int(x) for x in temp[1:]]) + tlist[1].extend([i for x in temp[1:]]) + print tlist + #Increase H count and regenerate the formula + for i in range(0, len(formula)): + if formula[i][0] == 'H': + formula[i][1] += 1 + formula[i][1] = str(formula[i][1]) + formula = [''.join(x) for x in formula] + formula = ''.join(formula) + + #For each basic atom in atoms, generate a copy of original protons + #add atom and save it + print "Protonating atoms with these InChI numbers: " +\ + str([x+1 for x in atoms]) + protlayers = [] + fprotlayers = [] + for atom in atoms: + if atom+1 not in tlist[0]: + extraprotons = list(protons) + extrafprotons = list(fprotons) + extraprotons[atom] += 1 + if tautomerics == []: + protlayers.append(WriteProtonCounts(extraprotons)) + else: + protlayers.append(WriteProtonCounts(extraprotons) + + ',' + ','.join(tautomerics)) + fprotlayers.append(WriteProtonCounts(extrafprotons)) + else: + extraprotons = list(protons) + extrafprotons = list(fprotons) + extratautomerics = list(tautomerics) + extrafprotons[atom] += 1 + #which tautomeric system atom belongs to? + tindex = tlist[0].index(atom+1) + tindex = tlist[1][tindex] + temp = tautomerics[tindex].split(',') + #Get the proton count and increase by 1 + protcount = int(temp[0][2:]) + 1 + #Write the proton count back + temp[0] = temp[0][:2] + str(protcount) + extratautomerics[tindex] = ','.join(temp) + protlayers.append(WriteProtonCounts(extraprotons)+',' + + ','.join(extratautomerics)) + fprotlayers.append(WriteProtonCounts(extrafprotons)) + protinchis = [] + protinchis.append(inchi) + for l in range(0, len(protlayers)): + layers = inchi.split('/') + MainLayerPassed = False + ChargeAdded = False + i = 1 + while (i < len(layers)): + if 'h' in layers[i]: + if not MainLayerPassed: + layers[i] = protlayers[l] + MainLayerPassed = True + if 'q' not in inchi: + layers.insert(i+1, 'q+1') + ChargeAdded = True + else: + layers[i] = fprotlayers[l] + if ('q' in layers[i]) and ChargeAdded is False: + charge = int(layers[i][1:]) + layers[i] = 'q'+"%+d" % charge + if 'C' in layers[i] and 'H' in layers[i]: + layers[i] = formula + i += 1 + #insert charge layer here + protinchis.append('/'.join(layers)) + return protinchis + + +def WriteProtonCounts(protons): + collectedprotons = [[], [], [], []] + + i = 1 + lastcount = protons[0] + start = 0 + while i < len(protons): + if protons[i] != lastcount: + if start == i-1: + collectedprotons[lastcount].append(str(start+1)) + else: + collectedprotons[lastcount].append(str(start+1)+'-'+str(i)) + lastcount = protons[i] + start = i + i += 1 + + if start == i-1: + collectedprotons[lastcount].append(str(start+1)) + else: + collectedprotons[lastcount].append(str(start+1)+'-'+str(i)) + + hlayer = 'h' + if len(collectedprotons[1]) > 0: + hlayer += ','.join(collectedprotons[1]) + hlayer += 'H,' + if len(collectedprotons[2]) > 0: + hlayer += ','.join(collectedprotons[2]) + hlayer += 'H2,' + if len(collectedprotons[3]) > 0: + hlayer += ','.join(collectedprotons[3]) + hlayer += 'H3,' + hlayer = hlayer[:-1] + return hlayer + + +def ReadProtonCounts(inchi): + import re + + #Get inchi layers + layers = inchi.split('/') + ProtLayer = '' + FixedLayer = '' + for l in layers[1:]: + if 'C' in l and 'H' in l: + atoms = re.findall(r"[a-zA-Z]+", l) + indexes = [int(x) for x in re.findall(r"\d+", l)] + formula = [list(x) for x in zip(atoms, indexes)] + if 'h' in l and ProtLayer != '': + FixedLayer = l[1:] + if 'h' in l and ProtLayer == '': + ProtLayer = l[1:] + + #initialize proton list + nheavy = sum([x[1] for x in formula if x[0] != 'H']) + + #Find, save and remove tautomeric portions from main proton layer + tautomerics = re.findall(r"\(.*?\)", ProtLayer) + ProtLayer = re.sub(r"\(.*?\)", "", ProtLayer) + if ProtLayer[-1] == ',': + ProtLayer = ProtLayer[:-1] + + #Read the main and the fixed proton layer + protons = ReadPSections(ProtLayer, nheavy) + fprotons = ReadPSections(FixedLayer, nheavy) + + return protons, formula, tautomerics, fprotons + + +def ReadPSections(ProtLayer, nheavy): + import re + protons = [0 for x in range(0, nheavy)] + #seperate the 1proton, 2proton and 3proton atoms, then seperate the records + psections = [x for x in re.findall(r".*?(?=H)", ProtLayer) if x != ''] + secvals = [0 for x in range(0, len(psections))] + psections[0] = psections[0].split(',') + + #interpret each record and fill in the proton table + #start by finding the proton count value for each section + for i in range(1, len(psections)): + if psections[i][0] == ',': + secvals[i-1] = 1 + psections[i] = psections[i][1:].split(',') + else: + secvals[i-1] = int(psections[i][0]) + psections[i] = psections[i][2:].split(',') + if ProtLayer[-1] != 'H': + secvals[-1] = int(ProtLayer[-1]) + else: + secvals[-1] = 1 + + #now expand each entry in the sections and fill the corresponding value + #in proton table + for i in range(0, len(psections)): + for s in psections[i]: + if '-' in s: + [start, finish] = [int(x) for x in s.split('-')] + protons[start-1:finish] = [secvals[i] for x in + range(0, len(protons[start-1:finish]))] + else: + protons[int(s)-1] = secvals[i] + return protons + + +def GetTautProtons(inchi): + #get the tautomer layer and pickup the data + layers = inchi.split('/') + + for l in layers: + if 'h' in l: + ProtLayer = l + ProtList = list(ProtLayer) + starts = [] + ends = [] + for i in range(0, len(ProtList)): + if ProtList[i] == '(': + starts.append(i) + if ProtList[i] == ')': + ends.append(i) + TautProts = [] + for i in range(0, len(starts)): + TautProts.append((ProtLayer[starts[i]+1:ends[i]]).split(',')) + + return TautProts + + +def GenTautomers(structf): + + f = structf + ".sdf" + inchi, aux = GetInchi(f) + + abc = 'abcdefghijklmnopqrstuvwxyz' + + t_inchis = GenTautInchis(inchi, aux, structf) + filenames = [] + for ds in range(0, len(t_inchis)): + Inchi2Struct(t_inchis[ds], f[:-4] + abc[ds], aux) + RestoreNumsSDF(f[:-4] + abc[ds] + '.sdf', f, aux) + filenames.append(f[:-4] + abc[ds]) + return len(filenames), filenames + + +#For now only works on one tautomeric system - for nucleosides don't need more +def GenTautInchis(inchi, aux, structf): + + resinchis = [] # Inchis of all tautomers, including the parent structure + + #Get tautomeric protons and atoms they are connected to + TautProts = GetTautProtons(inchi) + + #The total number of tautomeric protons in the system + if str(TautProts[0][0][1:]) != '': + totprotons = int(str(TautProts[0][0][1:])) + else: + totprotons = 1 + + #Check for and remove non-hetero atoms + temp = [int(x) for x in TautProts[0][1:] if IsHetero(int(x), inchi)] + + #Get the numbering map, to see which atomes are the tautomeric ones in the + #original structure file. From there determine their type and valency + #based on connectivity + amap = GetInchiRenumMap(aux) + OldTautProts = [amap[prot-1] for prot in temp] + valencies = GetTautValency(structf, OldTautProts) + + #the multivalent atoms will always have at least one proton + superfixed = [] + for i in range(0, len(valencies)): + if valencies[i] > 1: + superfixed.append(TautProts[0][i+1]) + #TautProts.append(['H', TautProts[0][i+1]]) + + #Generate all the possible proton positions + #with repetitions for multivalency + fixedprotons = list(itertools.combinations(TautProts[0][1:], + r=totprotons-len(superfixed))) + + for i in range(0, len(fixedprotons)): + fixedprotons[i] = superfixed + list(fixedprotons[i]) + fixedprotons[i].sort(key=int) + + #Count the occurences of protons positions, save the counts and + #remove redundant positions + counts = [] + for i in range(0, len(fixedprotons)): + counts.append([]) + j = 0 + while j < len(fixedprotons[i]): + counts[i].append(fixedprotons[i].count(fixedprotons[i][j])) + if counts[i][-1] > 1: + for _ in range(0, counts[i][-1]-1): + fixedprotons[i].remove(fixedprotons[i][j]) + #j+=counts[i][-1] + j += 1 + + fixprefix = '/f/h' + tauts = [] + for i in range(0, len(fixedprotons)): + tauts.append(fixprefix) + for j in range(0, len(fixedprotons[i])): + if j > 0: + tauts[i] += ',' + tauts[i] += fixedprotons[i][j] + 'H' + if counts[i][j] > 1: + tauts[i] += str(counts[i][j]) + + for taut in tauts: + resinchis.append(inchi + taut) + + return resinchis + + +def GetTautValency(structf, tautatoms): + #Read molecule from file + obconversion = OBConversion() + obconversion.SetInFormat("sdf") + obmol = OBMol() + obconversion.ReadFile(obmol, structf + ".sdf") + + protvalency = [] + for idx in tautatoms: + atom = obmol.GetAtom(idx) + corenum = atom.GetAtomicNum() + #If atom is hydrogen, check what it is connected to + nheavynbr = 0 + for NbrAtom in OBAtomAtomIter(atom): + nbrnum = NbrAtom.GetAtomicNum() + if nbrnum > 1: + nheavynbr += 1 + if corenum == 8: + protvalency.append(1) + if corenum == 7: + protvalency.append(3-nheavynbr) + + return protvalency + + +#utility function that determines if an atom is a heteroatom +def IsHetero(n, inchi): + layers = inchi.split('/') + nC = int((layers[1].split('H'))[0][1:]) + if (n > nC): + return True + else: + return False + + +def GenSelectDiastereomers(structf, atoms): + + f = structf + ".sdf" + inchi, aux = GetInchi(f) + amap = GetInchiRenumMap(aux) + + translated_atoms = [] + for atom in atoms: + translated_atoms.append(amap.index(atom)+1) + + ds_inchis = GenSelectDSInchis(inchi, translated_atoms) + ds_inchis = [FixTautProtons(f, i, aux) for i in ds_inchis] + filenames = [] + for ds in range(0, len(ds_inchis)): + Inchi2Struct(ds_inchis[ds], f[:-4] + str(ds+1), aux) + RestoreNumsSDF(f[:-4] + str(ds+1) + '.sdf', f, aux) + filenames.append(f[:-4] + str(ds+1)) + return len(filenames), filenames + + +def GenSelectDSInchis(inchi, atoms): + #Inchis of all diastereomers, including the parent structure + resinchis = [] + + #get the number of potential diastereomers + layers = inchi.split('/') + for l in layers: + if 't' in l: + slayer = l + sc = l[1:].split(',') + + ignore = [] + for i in range(0, len(sc)): + if not int(sc[i][:-1]) in atoms: + ignore.append(sc[i]) + sc = [x for x in sc if x not in ignore] + + if len(sc) == 0: + "No stereocentres remaining, no diastereomers will be generated." + return 0 + + numds = 2**(len(sc)) + print "Number of diastereomers to be generated: " + str(numds) + temps = [] + #Generate inversion patterns - essentially just binary strings + for i in range(0, numds): + template = bin(i)[2:].zfill(len(sc)) + temps.append(template) + + #For each 1 in template, invert the corresponding stereocentre + #and add the resulting diastereomer to the list + invert = {'+': '-', '-': '+'} + + reslayers = [] + for ds in range(0, numds): + newds = list(sc) + for stereocentre in range(0, len(sc)): + if temps[ds][stereocentre] == '1': + tlist = list(newds[stereocentre]) + tlist[-1] = invert[tlist[-1]] + newds[stereocentre] = "".join(tlist) + newlayer = str(slayer) + for stereocentre in range(0, len(sc)): + newlayer = newlayer.replace(sc[stereocentre], newds[stereocentre]) + reslayers.append(newlayer) + print reslayers + resinchis = [] + for layer in reslayers: + resinchis.append(inchi.replace(slayer, layer)) + return resinchis + + +def GenDiastereomers(structf): + + f = structf + ".sdf" + inchi, aux = GetInchi(f) + + print inchi + + ds_inchis = GenDSInchis(inchi) + ds_inchis = [FixTautProtons(f, i, aux) for i in ds_inchis] + filenames = [] + for ds in range(0, len(ds_inchis)): + Inchi2Struct(ds_inchis[ds], f[:-4] + str(ds+1), aux) + RestoreNumsSDF(f[:-4] + str(ds+1) + '.sdf', f, aux) + filenames.append(f[:-4] + str(ds+1)) + return len(filenames), filenames + + +def GenDSInchis(inchi): + + ilist = list(inchi) + #Inchis of all diastereomers, including the parent structure + resinchis = [] + + #get the number of potential diastereomers + layers = inchi.split('/') + for l in layers: + if 't' in l: + numds = 2**(len(l.translate(None, 't,1234567890'))-1) + + print "Number of diastereomers to be generated: " + str(numds) + + #find configuration sites (+ and -) + bs = ilist.index('t') + es = ilist[bs:].index('/') + spos = [] + for s in range(bs, bs+es): + if ilist[s] == '+' or ilist[s] == '-': + spos.append(s) + + temps = [] + #Generate inversion patterns - essentially just binary strings + for i in range(0, numds): + template = bin(i)[2:].zfill(len(spos)-1) + temps.append(template) + + #For each 1 in template, invert the corresponding stereocentre + #and add the resulting diastereomer to the list + invert = {'+': '-', '-': '+'} + + for ds in range(0, numds): + t = list(ilist) + for stereocentre in range(1, len(spos)): + if temps[ds][stereocentre-1] == '1': + t[spos[stereocentre]] = invert[t[spos[stereocentre]]] + resinchis.append(''.join(t)) + + return resinchis diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a83d17d --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +PyDP4 integrated workflow for the running of MM, DFT GIAO calculations and +DP4 analysis +v0.4 + +Copyright (c) 2015 Kristaps Ermanis, Jonathan M. Goodman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MacroModel.py b/MacroModel.py new file mode 100644 index 0000000..2fd9981 --- /dev/null +++ b/MacroModel.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jun 12 13:15:47 2015 + +@author: ke291 + +Contains all of the MacroModel specific code for input generation, calculation +execution and output interpretation. Called by PyDP4.py. +""" + +import os +import sys +import subprocess +import shutil +import time + + +def SetupMacromodel(numDS, settings, *args): + + for f in args: + if settings.Rot5Cycle is True: + if not os.path.exists(f+'rot.sdf'): + import FiveConf + #Generate the flipped fivemembered ring, + #result is in '*rot.sdf' file + FiveConf.main(f + '.sdf', settings) + + scriptdir = getScriptPath() + cwd = os.getcwd() + + #Convert input structure to mae file + convinp = settings.SCHRODINGER + '/utilities/sdconvert -isd ' + outp = subprocess.check_output(convinp + f + '.sdf -omae ' + f + + '.mae', shell=True) + + #Copy default com file to directory + shutil.copyfile(scriptdir + '/default.com', cwd + '/' + f + '.com') + #Change input and output file names in com file + comf = open(f + '.com', 'r+') + com = comf.readlines() + com[0] = f + '.mae\n' + com[1] = f + '-out.mae\n' + + #Change the molecular mechanics step count in the com file + cycles = (str(settings.MMstepcount)).rjust(6) + temp = list(com[7]) + temp[7:13] = list(cycles) + com[7] = "".join(temp) + comf.truncate(0) + comf.seek(0) + comf.writelines(com) + + #Change the forcefield in the com file + if (settings.ForceField).lower() == "opls": + temp = list(com[3]) + + temp[11:13] = list('14') + com[3] = "".join(temp) + comf.truncate(0) + + comf.seek(0) + comf.writelines(com) + + comf.close() + + if settings.Rot5Cycle is True: + #Convert input structure to mae file + convinp = settings.SCHRODINGER + '/utilities/sdconvert -isd ' + outp = subprocess.check_output(convinp + f + 'rot.sdf -omae ' + f + + 'rot.mae', shell=True) + + #Copy default com file to directory + shutil.copyfile(scriptdir + '/default.com', cwd + '/' + f + + 'rot.com') + #Change input and output file names in com file + comf = open(f + 'rot.com', 'r+') + com = comf.readlines() + com[0] = f + 'rot.mae\n' + com[1] = f + 'rot-out.mae\n' + + #Change the molecular mechanics step count in the com file + cycles = (str(settings.MMstepcount)).rjust(6) + temp = list(com[7]) + temp[7:13] = list(cycles) + com[7] = "".join(temp) + comf.truncate(0) + comf.seek(0) + comf.writelines(com) + comf.close() + print "Macromodel input for " + f + " prepared." + + +def RunMacromodel(numDS, settings, *args): + #Run Macromodel conformation search for all diastereomeric inputs + NCompleted = 0 + + for ds in args: + if not os.path.exists(ds+'.log'): + print settings.SCHRODINGER + '/bmin ' + ds + outp = subprocess.check_output(settings.SCHRODINGER + '/bmin ' + + ds, shell=True) + else: + print ds + ".log exists, skipping" + continue + + time.sleep(60) + while(not IsMMCompleted(ds + '.log')): + time.sleep(30) + NCompleted = NCompleted + 1 + + if settings.Rot5Cycle is True: + print "Macromodel job " + str(NCompleted) + " of " + str(numDS*2)\ + + " completed." + + print settings.SCHRODINGER + '/bmin ' + ds + 'rot' + outp = subprocess.check_output(settings.SCHRODINGER + '/bmin ' + + ds+'rot', shell=True) + time.sleep(60) + while(not IsMMCompleted(ds + 'rot.log')): + time.sleep(30) + NCompleted = NCompleted + 1 + print "Macromodel job " + str(NCompleted) + " of " + str(numDS*2)\ + + " completed." + else: + print "Macromodel job " + str(NCompleted) + " of " + str(numDS) +\ + " completed." + + +def ReadMacromodel(MMoutp, settings): + + conformers = [] + conformer = -1 + AbsEs = [] + + atoms = [] + charge = 0 + MaeInps = [] + + MaeFile = file(MMoutp + '-out.mae', 'r') + MaeInps.append(MaeFile.readlines()) + MaeFile.close() + + if settings.Rot5Cycle is True: + MaeFile = file(MMoutp + 'rot-out.mae', 'r') + MaeInps.append(MaeFile.readlines()) + MaeFile.close() + + for MaeInp in MaeInps: + index = 0 + AbsEOffsets = [] + #find conformer description blocks + blocks = [] + for i in range(0, len(MaeInp)): + if 'f_m_ct' in MaeInp[i]: + blocks.append(i) + if 'p_m_ct' in MaeInp[i]: + blocks.append(i) + + #find absolute energy offsets + for block in blocks: + for i in range(block, len(MaeInp)): + if 'mmod_Potential_Energy' in MaeInp[i]: + AbsEOffsets.append(i-block) + break + + #Get absolute energies for conformers + for i in range(0, len(blocks)): + for line in range(blocks[i], len(MaeInp)): + if ':::' in MaeInp[line]: + AbsEs.append(float(MaeInp[line+AbsEOffsets[i]])) + break + + #find geometry descriptions for each block + for i in range(0, len(blocks)): + for line in (MaeInp[blocks[i]:]): + if 'm_atom' in line: + blocks[i] = blocks[i] + MaeInp[blocks[i]:].index(line) + break + + #find start of atom coordinates for each block + for i in range(0, len(blocks)): + for line in (MaeInp[blocks[i]:]): + if ':::' in line: + blocks[i] = blocks[i] + MaeInp[blocks[i]:].index(line) + break + + #Read the atom numbers and coordinates + for block in blocks: + conformers.append([]) + conformer = conformer + 1 + index = block+1 + atom = 0 + while not ':::' in MaeInp[index]: + line = MaeInp[index].split(' ') + line = [word for word in line if word != ''] + conformers[conformer].append([]) + if conformer == 0: + atoms.append(GetMacromodelSymbol(int(line[1]))) + conformers[0][atom].append(line[0]) # add atom number + conformers[0][atom].append(line[2]) # add X + conformers[0][atom].append(line[3]) # add Y + conformers[0][atom].append(line[4]) # add Z + charge = charge + int(line[21]) + else: + if blocks.index(block) == 0: + conformers[conformer][atom].append(line[0]) # add atom number + conformers[conformer][atom].append(line[2]) # add X + conformers[conformer][atom].append(line[3]) # add Y + conformers[conformer][atom].append(line[4]) # add Z + else: + conformers[conformer][atom].append(line[0]) # add atom number + conformers[conformer][atom].append(line[1]) # add X + conformers[conformer][atom].append(line[2]) # add Y + conformers[conformer][atom].append(line[3]) # add Z + + index = index + 1 # Move to next line + atom = atom + 1 # Move to next atom + #Pick only the conformers in the energy window + MinE = min(AbsEs) + + conformers2 = [] + for i in range(0, len(conformers)): + if AbsEs[i] < MinE+settings.MaxCutoffEnergy: + conformers2.append(conformers[i]) + + return atoms, conformers2, charge + + +def GetMacromodelSymbol(atomType): + + Lookup = ['C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', + 'C', 'C', 'C', 'O', 'O', 'O', ' ', 'O', ' ', 'O', + 'O', ' ', 'O', 'N', 'N', 'N', ' ', ' ', ' ', ' ', + 'N', 'N', ' ', ' ', ' ', ' ', ' ', 'N', 'N', 'N', + 'H', 'H', 'H', 'H', 'H', ' ', ' ', 'H', 'S', ' ', + 'S', 'S', 'P', 'B', 'B', 'F', 'Cl', 'Br', 'I', 'Si', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'S', + 'S', 'Cl', 'B', 'F', ' ', ' ', ' ', ' ', 'S', 'S', + ' ', ' ', 'S', 'S'] + + if Lookup[atomType-1] == ' ': + print 'Unknown atom type' + + return Lookup[atomType-1] + + +def getScriptPath(): + return os.path.dirname(os.path.realpath(sys.argv[0])) + + +def IsMMCompleted(f): + Gfile = open(f, 'r') + outp = Gfile.readlines() + Gfile.close() + + if "normal termination" in outp[-3]: + return True + else: + return False diff --git a/NMRDP4GTF.py b/NMRDP4GTF.py new file mode 100644 index 0000000..b8df8af --- /dev/null +++ b/NMRDP4GTF.py @@ -0,0 +1,469 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Jan 12 14:42:47 2015 + +@author: ke291 + +Takes care of all the NMR description interpretation, equivalent atom +averaging, Boltzmann averaging, tautomer population optimisation (if used) +and DP4 input preparation and running either DP4.jar or DP4.py. Called by +PyDP4.py +""" + +import Gaussian +import NWChem + +import subprocess +import sys +import scipy.optimize as sciopt +import math +import os + +TMS_SC_C13 = 191.69255 +TMS_SC_H1 = 31.7518583 + + +def main(numDS, settings, *args): + + #This function runs nmrPredict for each diastereomer and collects + #the outputs + print '\nRunning NMRpredict script...' + + if settings.DFT == 'z' or settings.DFT == 'g': + (outputs, NumFiles, Ntaut) = Gaussian.RunNMRPredict(numDS, *args[:-1]) + Noutp = len(outputs) + elif settings.DFT == 'n' or settings.DFT == 'w': + (RelEs, populations, labels, BoltzmannShieldings, Ntaut) = \ + NWChem.RunNMRPredict(numDS, *args) + Noutp = len(BoltzmannShieldings) + + #Reads the experimental NMR data from the file + ExpNMR = args[-1] + ExpNMR_file = open(ExpNMR, 'r') + Cexp = ExpNMR_file.readline() + ExpNMR_file.readline() + Hexp = ExpNMR_file.readline() + + expHlabels = [] + expHvalues = [] + + #Loops through the experimental proton NMR data and picks out values + # and atom numbers and places them in expHlabels, expHvalues, expClabels, + #expCvalues + + expbuf = Hexp.split(',') + + for i in range(0, len(expbuf)): + shiftend = expbuf[i].find('(') + expHvalues.append(float(expbuf[i][0:shiftend])) + labelend = expbuf[i].find(')') + expHlabels.append(expbuf[i][shiftend+1:labelend]) + + #Check if exp NMR file contains info about equivalent atoms and read it + #into an array + #Also reads a list of atoms to omit from analysis + + equivalents = [] + omits = [] + + ExpNMR_file.readline() + for line in ExpNMR_file: + if not 'OMIT' in line and len(line) > 1: + equivalents.append(line[:-1].split(',')) + else: + omits.append(line[5:-1].split(',')) + ExpNMR_file.close() + + Clabels = [] + Hlabels = [] + Cvalues = [] + Hvalues = [] + + #This loops through predictNMR outputs for each diastereomer and collects + #NMR data + + flatequiv = [val for sublist in equivalents for val in sublist] + flatomits = [val for sublist in omits for val in sublist] + + if settings.DFT == 'z' or settings.DFT == 'g': + for DS in range(0, len(outputs)): + + Cvalues.append([]) + Hvalues.append([]) + + nmrdata = outputs[DS].split('\n') + + RelEnergies = nmrdata[2].split(',') + buf = '' + for val in RelEnergies[1:NumFiles[DS]]: + num = float(val) + buf = buf + "{:5.2f}".format(num) + ', ' + buf = buf + "{:5.2f}".format(float(RelEnergies[NumFiles[DS]])) + print '\nConformer relative energies (kJ/mol): ' + buf + + #Print population(%) for diagnostic purposes + Populations = nmrdata[3].split(',') + buf = '' + for val in Populations[1:NumFiles[DS]]: + num = float(val) + buf = buf + "{:4.1f}".format(num) + ', ' + buf = buf + "{:4.1f}".format(float(Populations[NumFiles[DS]])) + print '\nPopulations (%): ' + buf + + #loops through particular output and collects shielding constants + #and calculates shifts relative to TMS + for row in nmrdata[4:]: + data = row.split(',') + shift = 0 + if len(data) > NumFiles[DS]: + if data[0][23] == 'C' and not data[0][23:] in flatomits: + # only read labels once, i.e. the first diastereomer + if DS == 0: + Clabels.append(str(data[0][23:])) + shift = (TMS_SC_C13-float(data[NumFiles[DS]+1])) / \ + (1-(TMS_SC_C13/10**6)) + Cvalues[DS].append(shift) + + if data[0][23] == 'H' and not data[0][23:] in flatomits: + # only read labels once, i.e. the first diastereomer + if DS == 0: + Hlabels.append(str(data[0][23:])) + shift = (TMS_SC_H1-float(data[NumFiles[DS]+1])) / \ + (1-(TMS_SC_H1/10**6)) + Hvalues[DS].append(shift) + + elif settings.DFT == 'n' or settings.DFT == 'w': + for DS in range(0, numDS): + + Cvalues.append([]) + Hvalues.append([]) + + buf = '' + for val in RelEs[DS]: + num = float(val) + buf = buf + "{:5.2f}".format(num) + ', ' + print '\nConformer relative energies (kJ/mol): ' + buf[:-2] + + buf = '' + for val in populations[DS]: + num = float(val) + buf = buf + "{:4.1f}".format(num*100) + ', ' + print '\nPopulations (%): ' + buf[:-2] + + #loops through particular output and collects shielding constants + #and calculates shifts relative to TMS + for atom in range(0, len(BoltzmannShieldings[DS])): + shift = 0 + if labels[atom][0] == 'C' and not labels[atom] in flatomits: + # only read labels once, i.e. the first diastereomer + if DS == 0: + Clabels.append(labels[atom]) + shift = (TMS_SC_C13-BoltzmannShieldings[DS][atom]) / \ + (1-(TMS_SC_C13/10**6)) + Cvalues[DS].append(shift) + + if labels[atom][0] == 'H' and not labels[atom] in flatomits: + # only read labels once, i.e. the first diastereomer + if DS == 0: + Hlabels.append(labels[atom]) + shift = (TMS_SC_H1-BoltzmannShieldings[DS][atom]) / \ + (1-(TMS_SC_H1/10**6)) + Hvalues[DS].append(shift) + + #Looks for equivalent atoms in the computational data, averages the shifts + #and removes the redundant signals + for eqAtoms in equivalents: + + eqSums = [0.0]*Noutp + eqAvgs = [0.0]*Noutp + + if eqAtoms[0][0] == 'H': + #print eqAtoms, Hlabels + for atom in eqAtoms: + eqIndex = Hlabels.index(atom) + for ds in range(0, Noutp): + eqSums[ds] = eqSums[ds] + Hvalues[ds][eqIndex] + for ds in range(0, Noutp): + eqAvgs[ds] = eqSums[ds]/len(eqAtoms) + + #Place the new average value in the first atom shifts place + target_index = Hlabels.index(eqAtoms[0]) + for ds in range(0, Noutp): + Hvalues[ds][target_index] = eqAvgs[ds] + + #Delete the redundant atoms from the computed list + #start with second atom - e.g. don't delete the original one + for atom in range(1, len(eqAtoms)): + del_index = Hlabels.index(eqAtoms[atom]) + del Hlabels[del_index] + for ds in range(0, Noutp): + del Hvalues[ds][del_index] + + if eqAtoms[0][0] == 'C': + for atom in eqAtoms: + eqIndex = Clabels.index(atom) + for ds in range(0, Noutp): + eqSums[ds] = eqSums[ds] + Cvalues[ds][eqIndex] + for ds in range(0, Noutp): + eqAvgs[ds] = eqSums[ds]/len(eqAtoms) + + #Place the new average value in the first atom shifts place + target_index = Clabels.index(eqAtoms[0]) + for ds in range(0, Noutp): + Cvalues[ds][target_index] = eqAvgs[ds] + + #Delete the redundant atoms from the computed list + #start with second atom - e.g. don't delete the original one + for atom in range(1, len(eqAtoms)): + del_index = Clabels.index(eqAtoms[atom]) + del Clabels[del_index] + for ds in range(0, Noutp): + del Cvalues[ds][del_index] + + tstart = 0 + OptCvalues = [] + OptHvalues = [] + + for tindex in range(0, len(Ntaut)): + print 'looking at tautomers ' + str(tstart) + ' to ' + \ + str(tstart+Ntaut[tindex]) + if Ntaut[tindex] == 1: + print "Only one tautomer found, skipping optimisation." + OptCvalues.append(Cvalues[tstart]) + OptHvalues.append(Hvalues[tstart]) + tstart = tstart + Ntaut[tindex] + else: + (BuffC, BuffH) = OptTautPop(Clabels, + Cvalues[tstart:tstart+Ntaut[tindex]], + Hlabels, + Hvalues[tstart:tstart+Ntaut[tindex]], + Cexp, Hexp) + OptCvalues.append(BuffC) + OptHvalues.append(BuffH) + tstart = tstart + Ntaut[tindex] + + #Output the seperated shifts to terminal and DP4 input file + #along with the experimental NMR data + if (not settings.PDP4) and (not settings.EP5): + + WriteDP4input(Clabels, OptCvalues, Cexp, Hlabels, OptHvalues, Hexp) + #Run the DP4 java file and collect the output + javafolder = getScriptPath() + DP4outp = subprocess.check_output('CLASSPATH=' + javafolder + + ' java -jar ' + javafolder + '/DP4.jar DP4inp.inp', + shell=True) + print '\n' + DP4outp + + else: + import DP4 + DP4outp = DP4.main(Clabels, OptCvalues, Hlabels, OptHvalues, Cexp, + Hexp, settings) + + return '\n'.join(DP4outp) + '\n' + + +def WriteDP4input(Clabels, Cvalues, Cexp, Hlabels, Hvalues, Hexp): + + print '\nWriting input file for DP4...' + DP4_file = open('DP4inp.inp', 'w') + + DP4_file.write(','.join(Clabels) + '\n') + print '\n' + ','.join(Clabels) + for line in Cvalues: + print ','.join(format(v, "4.2f") for v in line) + DP4_file.write(','.join(format(v, "4.2f") for v in line) + '\n') + + DP4_file.write('\n' + Cexp) + print '\n' + Cexp + + DP4_file.write('\n' + ','.join(Hlabels) + '\n') + print '\n' + ','.join(Hlabels) + for line in Hvalues: + print ','.join(format(v, "4.2f") for v in line) + DP4_file.write(','.join(format(v, "4.2f") for v in line) + '\n') + + DP4_file.write('\n' + Hexp + '\n') + print '\n' + Hexp + + DP4_file.close() + + +def OptTautPop(Clabels, Cvalues, Hlabels, Hvalues, Cexp, Hexp): + #Pairwise match exp signals to computed ones based on assignments first, + #on erorrs afterwards + ExpCvalues = [-1 for i in range(0, len(Clabels))] + ExpHvalues = [-1 for i in range(0, len(Hlabels))] + + Hdata = Hexp.split(',') + for s in range(0, len(Hdata)): + Hdata[s] = Hdata[s].strip() + + Cdata = Cexp.split(',') + for s in range(0, len(Cdata)): + Cdata[s] = Cdata[s].strip() + + UAExpCshifts = list(Cdata) + UAExpHshifts = list(Hdata) + + #Assign known(experimentally assigned) signals first + for l in range(0, len(Clabels)): + for s in Cdata: + if (Clabels[l] + ')') in s and (not 'or' in s) and (not 'OR' in s): + shiftend = s.find('(') + ExpCvalues[l] = float(s[:shiftend]) + UAExpCshifts.remove(s) + break + for l in range(0, len(Hlabels)): + for s in Hdata: + if (Hlabels[l] + ')') in s and (not 'or' in s) and (not 'OR' in s): + shiftend = s.find('(') + ExpHvalues[l] = float(s[:shiftend]) + UAExpHshifts.remove(s) + break + + #Prepare unassigned experimental values for matching + for i in range(0, len(UAExpHshifts)): + shiftend = UAExpHshifts[i].find('(') + UAExpHshifts[i] = float(UAExpHshifts[i][:shiftend]) + + for i in range(0, len(UAExpCshifts)): + shiftend = UAExpCshifts[i].find('(') + UAExpCshifts[i] = float(UAExpCshifts[i][:shiftend]) + + #Try to assign unassigned values based on every calculated tautomer + MinMAE = 1000 + for k in range(0, len(Cvalues)): + #Pick out unassigned computational values for matching + UACompCshifts = [] + UACompHshifts = [] + for i in range(0, len(ExpCvalues)): + if ExpCvalues[i] == -1: + UACompCshifts.append(Cvalues[k][i]) + for i in range(0, len(ExpHvalues)): + if ExpHvalues[i] == -1: + UACompHshifts.append(Hvalues[k][i]) + + #Sort both sets of data - this essentially pairs them up + UAExpCshifts.sort() + UAExpHshifts.sort() + UACompCshifts.sort() + UACompHshifts.sort() + + #Go through half-assigned experimental data and fill in the holes with + #paired data + for i in range(0, len(ExpCvalues)): + if ExpCvalues[i] == -1 and len(UAExpCshifts) > 0: + j = UACompCshifts.index(Cvalues[k][i]) + ExpCvalues[i] = UAExpCshifts[j] + for i in range(0, len(ExpHvalues)): + if ExpHvalues[i] == -1 and len(UAExpHshifts) > 0: + j = UACompHshifts.index(Hvalues[k][i]) + ExpHvalues[i] = UAExpHshifts[j] + + #Optimize tautomer populations for + #tpops is 1 shorter than the number of tautomers, + #the remaining weight is 1-sum(rest) + tpops = [1.0/len(Cvalues) for i in range(len(Cvalues)-1)] + f = lambda w: TautError(Cvalues, ExpCvalues, Hvalues, ExpHvalues, w) + res = sciopt.minimize(f, tpops, method='nelder-mead') + if float(res.fun) < MinMAE: + print "New min MAE: " + str(res.fun) + MinMAE = float(res.fun) + NewPops = list(res.x) + NewPops.append(1-sum(NewPops)) + print NewPops + + NewCvalues = [] + NewHvalues = [] + + #calculate the new C values + for atom in range(0, len(Clabels)): + C = 0 + for taut in range(0, len(Cvalues)): + C = C + Cvalues[taut][atom]*NewPops[taut] + NewCvalues.append(C) + + for atom in range(0, len(Hlabels)): + H = 0 + for taut in range(0, len(Hvalues)): + H = H + Hvalues[taut][atom]*NewPops[taut] + NewHvalues.append(H) + + #Return the new Cvalues and Hvalues + return (NewCvalues, NewHvalues) + + +def TautError(Cs, CExp, Hs, HExp, TPopsIn): + + if len(Cs) != len(TPopsIn)+1 or len(Hs) != len(TPopsIn)+1: + print len(Cs), len(Hs), len(TPopsIn) + print ("Input dimensions in TautError don't match, exiting...") + return 1000 + TPops = list(TPopsIn) + TPops.append(1-sum(TPops)) + SumC = [] + SumH = [] + #print len(Cs), len(Hs), len(TPops) + #print TPops + for i in range(0, len(TPops)): + if TPops[i] < 0: + return 100 + if sum(TPops) > 1: + s = sum(TPops) + for i in range(0, len(TPops)): + TPops[i] = TPops[i]/s + + for atom in range(0, len(CExp)): + C = 0 + for taut in range(0, len(Cs)): + C = C + Cs[taut][atom]*TPops[taut] + SumC.append(C) + + for atom in range(0, len(HExp)): + H = 0 + for taut in range(0, len(Hs)): + H = H + Hs[taut][atom]*TPops[taut] + SumH.append(H) + + ErrC = MAE(SumC, CExp) + #ErrC = RMSE(SumC, CExp) + ErrH = MAE(SumH, HExp) + #ErrH = RMSE(SumH, HExp) + #print 'MAE for C: ' + str(ErrC) + #print 'MAE for H: ' + str(ErrH) + + return ErrC + 20*ErrH + + +def getScriptPath(): + return os.path.dirname(os.path.realpath(sys.argv[0])) + + +def MAE(L1, L2): + + if len(L1) != len(L2): + return -1 + else: + L = [] + for i in range(0, len(L1)): + L.append(abs(L1[i]-L2[i])) + return sum(L)/len(L) + + +def RMSE(L1, L2): + + if len(L1) != len(L2): + return -1 + else: + L = [] + for i in range(0, len(L1)): + L.append((L1[i]-L2[i])**2) + return math.sqrt(sum(L)/len(L)) + +if __name__ == '__main__': + #print sys.argv + cpargs = sys.argv[2:] + numDS = int(sys.argv[1]) + for ds in range(0, numDS): + cpargs[ds*2+1] = int(cpargs[ds*2+1]) + main(numDS, *cpargs) diff --git a/NWChem.py b/NWChem.py new file mode 100644 index 0000000..98e8e27 --- /dev/null +++ b/NWChem.py @@ -0,0 +1,308 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Nov 19 15:56:54 2014 + +@author: ke291 + +Contains all of the NWChem specific code for input generation and calculation +execution. Called by PyDP4.py. +""" + +import Tinker +import MacroModel +import nmrPredictNWChem + +import pyximport +pyximport.install() +import ConfPrune +import glob +import os +import subprocess +import time +import socket + + +def SetupNWChem(MMoutp, NWCheminp, numDigits, settings, adjRMSDcutoff): + + #Reads conformer geometry, energies and atom labels from Tinker output + #(atoms, conformers) = ReadConformers(MMoutp, MaxEnergy) + + if settings.MMTinker: + #Reads conformer geometry, energies and atom labels from Tinker output + (atoms, conformers, charge) = Tinker.ReadTinker(MMoutp, settings) + else: + (atoms, conformers, charge) = MacroModel.ReadMacromodel(MMoutp, + settings) + + #Prune similar conformations, if the number exceeds the limit + if len(conformers) > settings.PerStructConfLimit: + pruned = ConfPrune.RMSDPrune(conformers, atoms, adjRMSDcutoff) + else: + pruned = conformers + + print str(len(conformers) - len(pruned)) +\ + " or " + "{:.1f}".format(100 * (len(conformers) - len(pruned)) / + len(conformers)) + "% of conformations have been pruned based on " + \ + str(adjRMSDcutoff) + " angstrom cutoff" + + for num in range(0, len(pruned)): + filename = NWCheminp+str(num+1).zfill(3) + WriteNWChemFile(filename, pruned[num], atoms, charge, settings) + + print str(len(pruned)) + " .nw files written" + + +#Adjust the RMSD cutoff to keep the conformation numbers reasonable +def AdaptiveRMSD(MMoutp, settings): + + if settings.MMTinker: + #Reads conformer geometry, energies and atom labels from Tinker output + (atoms, conformers, charge) = Tinker.ReadTinker(MMoutp, settings) + else: + (atoms, conformers, charge) = MacroModel.ReadMacromodel(MMoutp, + settings) + + return ConfPrune.AdaptRMSDPrune(conformers, atoms, + settings.InitialRMSDcutoff, + settings.PerStructConfLimit) + + +def WriteNWChemFile(NWCheminp, conformer, atoms, charge, settings): + + f = file(NWCheminp + '.nw', 'w') + f.write('memory stack 1500 mb heap 1500 mb global 3000 mb\n') + if settings.DFT == 'w': + f.write('scratch_dir /scratch/' + settings.user + '/' + NWCheminp + '\n') + f.write('echo\n\nstart molecule\n\ntitle "'+NWCheminp+'"\n') + f.write('echo\n\nstart\n\n') + f.write('charge ' + str(charge) + '\n\n') + f.write('geometry units angstroms print xyz autosym\n') + + natom = 0 + for atom in conformer: + f.write(' ' + atoms[natom] + ' ' + atom[1] + ' ' + atom[2] + ' ' + + atom[3] + '\n') + natom = natom + 1 + + f.write('end\n\nbasis\n * library 6-31G**\nend\n\n') + f.write('dft\n xc b3lyp\n mult 1\nend\n\n') + if settings.Solvent != "": + f.write('cosmo\n do_cosmo_smd true\n solvent ' + settings.Solvent + '\n') + f.write('end\n\n') + f.write('task dft energy\n\n') + f.write('property\n shielding\nend\n') + f.write('task dft property\n') + f.close() + + +def GetFiles2Run(inpfiles, settings): + #Get the names of all relevant input files + NinpFiles = [] + for filename in inpfiles: + NinpFiles = NinpFiles + glob.glob(filename + 'nwinp???.nw') + + Files2Run = [] + + #for every input file check that there is a completed output file, + #delete the incomplete outputs and add the inputs to be done to Files2Run + for filename in NinpFiles: + if not os.path.exists(filename[:-3]+'.nwo'): + Files2Run.append(filename) + else: + if IsNWChemCompleted(filename[:-3] + '.nwo'): + continue + else: + os.remove(filename[:-3] + '.nwo') + Files2Run.append(filename) + + return Files2Run + + +def RunNWChem(inpfiles, settings): + + NCompleted = 0 + NWChemPrefix = "nwchem " + + for f in inpfiles: + print NWChemPrefix + f + ' > ' + f[:-2] + 'nwo' + outp = subprocess.check_output(NWChemPrefix + f + ' > ' + f[:-2] + + 'nwo', shell=True) + NCompleted += 1 + print "NWChem job " + str(NCompleted) + " of " + str(len(inpfiles)) + \ + " completed." + + +def RunNMRPredict(numDS, *args): + + NWNames = [] + NTaut = [] + + for val in range(0, numDS): + NTaut.append(args[val*2]) + NWNames.append(args[val*2+1]) + + RelEs = [] + populations = [] + BoltzmannShieldings = [] + + print NWNames + print NTaut + #This loop runs nmrPredict for each diastereomer and collects + #the outputs + for isomer in NWNames: + + NWFiles = glob.glob(isomer + 'nwinp*.nwo') + for f in range(0, len(NWFiles)): + NWFiles[f] = NWFiles[f][:-4] + + #Runs nmrPredictNWChem Name001, ... and collects output + (x, y, labels, z) = nmrPredictNWChem.main(*NWFiles) + RelEs.append(x) + populations.append(y) + BoltzmannShieldings.append(z) + + return (RelEs, populations, labels, BoltzmannShieldings, NTaut) + + +def IsNWChemCompleted(f): + Nfile = open(f, 'r') + outp = Nfile.readlines() + Nfile.close() + outp = "".join(outp) + if "AUTHORS" in outp: + return True + else: + return False + + +def RunOnZiggy(folder, queue, NWFiles, settings): + + print "ziggy NWChem job submission script\n" + + #Check that folder does not exist, create job folder on ziggy + outp = subprocess.check_output('ssh ziggy ls', shell=True) + if folder in outp: + print "Folder exists on ziggy, choose another folder name." + return + + outp = subprocess.check_output('ssh ziggy mkdir ' + folder, shell=True) + #Write the qsub scripts + for f in NWFiles: + WriteSubScript(f[:-3], queue, folder, settings) + print str(len(NWFiles)) + ' .qsub scripts generated' + + #Upload .com files and .qsub files to directory + print "Uploading files to ziggy..." + for f in NWFiles: + outp = subprocess.check_output('scp ' + f +' ziggy:~/' + folder, + shell=True) + outp = subprocess.check_output('scp ' + f[:-3] +'.qsub ziggy:~/' + + folder, shell=True) + + print str(len(NWFiles)) + ' .nw and .qsub files uploaded to ziggy' + + #Launch the calculations + for f in NWFiles: + job = '~/' + folder + '/' + f[:-3] + outp = subprocess.check_output('ssh ziggy qsub -q ' + queue + ' -o ' + + job + '.log -e ' + job + '.err -l nodes=1:ppn=1:ivybridge ' + + job + '.qsub', shell=True) + time.sleep(3) + + print str(len(NWFiles)) + ' jobs submitted to the queue on ziggy' + + outp = subprocess.check_output('ssh ziggy showq', shell=True) + if settings.user in outp: + print "Jobs are running on ziggy" + + Jobs2Complete = list(NWFiles) + n2complete = len(Jobs2Complete) + + #Check and report on the progress of calculations + while len(Jobs2Complete) > 0: + JustCompleted = [job for job in Jobs2Complete if + IsZiggyGComplete(job[:-2] + 'nwo', folder, settings)] + Jobs2Complete[:] = [job for job in Jobs2Complete if + not IsZiggyGComplete(job[:-2] + 'nwo', folder, settings)] + if n2complete != len(Jobs2Complete): + n2complete = len(Jobs2Complete) + print str(n2complete) + " remaining." + + time.sleep(60) + + #When done, copy the results back + print "\nCopying the output files back to localhost..." + print 'ssh ziggy scp /home/' + settings.user + '/' + folder + '/*.nwo ' +\ + socket.getfqdn() + ':' + os.getcwd() + outp = subprocess.check_output('ssh ziggy scp /home/' + settings.user + '/' + + folder + '/*.nwo ' + socket.getfqdn() + ':' + + os.getcwd(), shell=True) + + +def WriteSubScript(NWJob, queue, ZiggyJobFolder, settings): + + if not (os.path.exists(NWJob+'.nw')): + print "The input file " + NWJob + ".nw does not exist. Exiting..." + return + + #Create the submission script + QSub = open(NWJob + ".qsub", 'w') + + #Choose the queue + QSub.write('#PBS -q ' + queue + '\n#PBS -l nodes=1:ppn=1\n#\n') + + #define input files and output files + QSub.write('file=' + NWJob + '\n\n') + QSub.write('inpfile=${file}.nw\noutfile=${file}.nwo\n') + + #define cwd and scratch folder and ask the machine + #to make it before running the job + QSub.write('HERE=/home/' + settings.user + '/' + ZiggyJobFolder + '\n') + QSub.write('SCRATCH=/sharedscratch/' + settings.user + '/' + NWJob + '\n') + QSub.write('LSCRATCH=/scratch/' + settings.user + '/' + NWJob + '\n') + QSub.write('mkdir ${SCRATCH}\n') + QSub.write('mkdir ${LSCRATCH}\n') + + #load relevant modules + QSub.write('set OMP_NUM_THREADS=1\n') + QSub.write('module load anaconda\n') + QSub.write('module load gcc/4.8.3\n') + QSub.write('module load mpi/openmpi/gnu/1.8.1\n') + QSub.write('module load nwchem\n') + + #copy the input file to scratch + QSub.write('cp ${HERE}/${inpfile} $SCRATCH\ncd $SCRATCH\n') + + #write useful info to the job output file (not the gaussian) + QSub.write('echo "Starting job $PBS_JOBID"\necho\n') + QSub.write('echo "PBS assigned me this node:"\ncat $PBS_NODEFILE\necho\n') + + QSub.write('ln -s $HERE/$outfile $SCRATCH/$outfile\n') + QSub.write('nwchem $inpfile > $outfile\n') + + #Cleanup + QSub.write('rm -rf ${SCRATCH}/\n') + QSub.write('rm -rf ${LSCRATCH}/\n') + QSub.write('qstat -f $PBS_JOBID\n') + + QSub.close() + + +def IsZiggyGComplete(f, folder, settings): + + path = '/home/' + settings.user + '/' + folder + '/' + try: + outp1 = subprocess.check_output('ssh ziggy ls ' + path, shell=True) + except subprocess.CalledProcessError, e: + print "ssh ziggy ls failed: " + str(e.output) + return False + if f in outp1: + try: + outp2 = subprocess.check_output('ssh ziggy cat ' + path + f, + shell=True) + except subprocess.CalledProcessError, e: + print "ssh ziggy cat failed: " + str(e.output) + return False + if "AUTHORS" in outp2: + return True + return False diff --git a/NucCErr.pkl b/NucCErr.pkl new file mode 100644 index 0000000000000000000000000000000000000000..f6f629b1d2391ece5bf2ae191b3b19e37cf132c1 GIT binary patch literal 1637 zcmYjRPe_ze5dYf6ja^MwciT0yug}cbE};Y=k-VYHbm)*>x)^MSqT|{r?6O2cH$B!( zu=C>4L-2+Of-u;EQasdd5hyAwwwtA4%^T-c?=;_g^Y8bY-+blR&B>sE6yKN*7soe| z(s$gsi}u~jM4|PfjL=q^Urvp&G4#}(CrC%wbz?e0NY$~DovNW-;qP!UKuU1T%mg4M zHHzY?F*ZBUHpHih=95c2!wD0C9E5h4rVePo$rB*Nrd;YG+FgFYN`Usai-QRm$OGzZ}vwYkLSO@NS>ej>E1(kkBslDW(YwGI1ITeWou;yRbuQh=0j&`Wb@ z8fw@rb%xOUbSN%J3Wqhc@7>cQ&htwF+8ii~vCVSm8b`#j-k?b&{jJnYkwqMRpm zLf9xHWW`|YyHcWv)S+#_fOhy-2{fcdArz6!vJ%_~mI&=zWffAY4ZANhjkqXJvd|(P ziiFQ0WE|h~HKe=(q_~$vw>nd|c1THSWB5OSrzE_J)TZH(lYZvVepew;mojhG{kZp~ zI+O5AX)zzY>Y9FVUzH!|@tOSyC@ntB&k1>o5WGR1g0O*hPiDbvc)}qkYz*^$2|(V9 zrc~!P_C+LRPU!jR)y#BUo<UxjhXwFjHE&uTX~l^o3eX5o zD*@6;!Ty*RP4#1%x?gr(sADDcE8)M;%Sa8ONV!7;>m-Q&18T$h*6{)z$`XilB`xg0L3fBB52eREQX` zMM21{$|CqKZYqdyp&3OnLWE?IrH=hrbnl(}^xMpv_wLU*=brb%y0e#pB2s*8X1RHA z45?#KRs$Gq^WT}}0HHB6zJjxPghCj|hwj@*aclloh|!{)WH_dYf5uk;DIrzv21uQ< zfrPYAgGVs>nwuM5n*elgwMB^TuQuhyQ>iz?b?RTrN9f+`5%0wKptvP&NK(w0tFW&kVEsAgyu&OtVH2veMybfYC4C^rtnYma! zAsiA3XWJ3dn$9~Z(R;kYYIs>v9(%46Qq0Q<< zs^KETNcr>)Wm;lc1lMo{-M{nlah+au1glzA!3m+)C_~Z~(JPaV0zZPPA}pa>uyo3J z%2`!o5+SLJ5Th3|+bPBq_hNR*&h!*gZC+(`k7eUFkGr6Tyn>X~5dcEB9I-bbqsmv1 z8Vo9>Er#MIX7s~$+@^Pe_Xe~H_cPogr_wF(m`p=Rc#~9=1w&2vp`>)m-RLTwX^V*f zh2JtuhT5yc9c3f=7T$+$O)GOj?=i|>7Oa~Hr*tR}kWQc|%C5Q;rzCsk(RHQ{H~5-ClZg^9XqZ27wWzE}IT?_(g+a z<{P)}(_1~Tj558Blr#hE)^T4nJwzy8l%Q3lR*k731nRMnS!h!>|AtPGRqeFfO=%q* zKTYcf4C?LXLjQ)G^&_3=5ofZEYhsoWI#k0kesWb_#(ZBwCb@DT=~sDCi@Cs$r%DD0 zefN~1?_Tlm51{*~LUC4;zl=Ku9$J(jA=TLq9aaN8L^!TH*N|d5N~T=HQ^MNg@R}+A YG*zit->86}Zo}+8d#JVd8O|O14=)tEJ^%m! literal 0 HcmV?d00001 diff --git a/NucHErr.pkl b/NucHErr.pkl new file mode 100644 index 0000000000000000000000000000000000000000..8b7f33f5c9bf1624ceeb9ca2e5b8d23d0137629c GIT binary patch literal 1322 zcmYjRJ!q3r6#eKVnH)Me)r`SG1Q(ZbvmHk(juEmbj&YN^WJtj+i%Uc#kS;Ez1;Gg7 z{iqe&QY9K|VyT)K|1cFkgrKAM-S_VM`liQy_vhSm&wcf|t2dJpcw@S<&|MmVhqKLQ zrSuQ<{<=Fizz@1h@>mBwE-cnMg9Oy;4CLAdn}wgLPYS-fJjU959lRPYNjRXj;6vU= z@~Gu>0^SPF(LxQgq*klv6M-JKdI$7Y$enFUQD41{oH}!k z;OT`M#ViH;%R1vug6g!RK zakxiH%Pr6w)kX$3!pSn{N;s6RniV0zFq27wcAX+_C*V)>E@Kh&Tp25aV$nSfFCqXF z@KIV`2@lQ1v-ts<#`{8J5V)a6n?aNu20 z(*_NV-o9Icj%2n@c_6N1tb_O6v{z2S!?>J)GL`Wva)f<@G0{9j&{HL$3?3`^BeN7i zQ9s58eReu(R>9v?0NaRb#eNp0W8vQ&oP{pJlZGZoT(P`F$>`yV6QbQJ&q literal 0 HcmV?d00001 diff --git a/NucHErrOpt.pkl b/NucHErrOpt.pkl new file mode 100644 index 0000000000000000000000000000000000000000..ccda636f5ca61103807913920d4399196b663b38 GIT binary patch literal 1322 zcmY+EKWJ1z5XK)?a#&cz_DuN-k$`oW>psiGI>I%IWo(1RD{^2VB!xu;k-*v51Oq`| zKr-h)65*7KQ7?%>?o15P1iUB#3kzpwclJHEdbc|}^L@XWz3kN0n@I{@t&C1KyQjgQ z8`XUL@HVJ_&#gJ|g8Xh&TcBm#$v~TKBOe3X2Tf1)u`rY%2@CuzJ$W%RF${XDndQUs z@R9~g9v@Q+CC-Ea@Wp+wp8kDI1=%N zuvq<(#LiwykHacT_9hxklSB+t8Wo&1r=czMaK8u*99 zGt~omymEcri61`#d4m#ceCVxoqB1H5J-RZMPuJb5g9mN5I-qUK9D#3W(Gly81a}Ob zF)TV54Vu(YT@?0J;X#SBS&lB~ms)-T-qU{F0(bxX0#fnsx)46nCS2YaVIP<*Gm+V0Kb3DdCw{~HXl?a*( 0: + numDS, inpfiles = InchiGen.GenSelectDiastereomers(filename, + settings.SelectedStereocentres) + else: + numDS, inpfiles = InchiGen.GenDiastereomers(filename) + if settings.GenTaut: + newinpfiles = [] + for ds in inpfiles: + print "Generating tautomers for " + ds + settings.NTaut, files = InchiGen.GenTautomers(ds) + newinpfiles.extend(files) + inpfiles = list(newinpfiles) + if settings.GenProt: + newinpfiles = [] + for ds in inpfiles: + print "Generating protomers for " + ds + settings.NTaut, files = InchiGen.GenProtomers(ds, + settings.BasicAtoms) + newinpfiles.extend(files) + inpfiles = list(newinpfiles) + else: + inpfiles = filename + if settings.GenTaut: + numDS = nfiles + import InchiGen + newinpfiles = [] + for ds in inpfiles: + print "Generating tautomers for " + ds + settings.NTaut, files = InchiGen.GenTautomers(ds) + newinpfiles.extend(files) + inpfiles = list(newinpfiles) + else: + numDS = int(nfiles/settings.NTaut) + if (numDS == 1): + import InchiGen + for f in filename: + tdiastereomers = [] + numDS, tinpfiles = InchiGen.GenDiastereomers(f) + tdiastereomers.append(tinpfiles) + tinpfiles = zip(*tdiastereomers) + inpfiles = [] + for ds in tinpfiles: + inpfiles.extend(list(ds)) + + print inpfiles + + #Check the existence of mm output files + MMRun = False + + if settings.MMTinker: + #Check if there already are Tinker output files with the right names + tinkfiles = glob.glob('*.tout') + mminpfiles = [] + for filename in inpfiles: + if filename + '.tout' in tinkfiles and (filename + 'rot.tout' in + tinkfiles or settings.Rot5Cycle is False): + if len(mminpfiles) == 0: + MMRun = True + else: + MMRun = False + mminpfiles.append(filename) + else: + #Check if there already are Tinker output files with the right names + mmfiles = glob.glob('*.log') + mminpfiles = [] + for filename in inpfiles: + if filename + '.log' in mmfiles and (filename + 'rot.log' in + mmfiles or settings.Rot5Cycle is False): + if len(mminpfiles) == 0: + MMRun = True + else: + MMRun = False + mminpfiles.append(filename) + + if MMRun: + print 'Conformation search has already been run for these inputs.\ + \nSkipping...' + if settings.GenOnly: + print "Input files generated, quitting..." + quit() + else: + if settings.MMTinker: + print 'Some Tinker files missing.' + print '\Seting up Tinker files...' + Tinker.SetupTinker(len(inpfiles), settings, *mminpfiles) + if settings.GenOnly: + print "Input files generated, quitting..." + quit() + print '\nRunning Tinker...' + Tinker.RunTinker(len(inpfiles), settings, *mminpfiles) + else: + print 'Some Macromodel files missing.' + print '\nSetting up Macromodel files...' + MacroModel.SetupMacromodel(len(inpfiles), settings, *mminpfiles) + if settings.GenOnly: + print "Input files generated, quitting..." + quit() + print '\nRunning Macromodel...' + MacroModel.RunMacromodel(len(inpfiles), settings, *mminpfiles) + + if not settings.AssumeDone: + if settings.DFT == 'z' or settings.DFT == 'g': + adjRMSDcutoff = Gaussian.AdaptiveRMSD(inpfiles[0], settings) + elif settings.DFT == 'n' or settings.DFT == 'w': + adjRMSDcutoff = NWChem.AdaptiveRMSD(inpfiles[0], settings) + print 'RMSD cutoff adjusted to ' + str(adjRMSDcutoff) + #Run NWChem setup script for every diastereomer + print '\nRunning DFT setup...' + i = 1 + for ds in inpfiles: + if settings.DFT == 'z' or settings.DFT == 'g': + print "\nGaussian setup for file " + ds + " (" + str(i) +\ + " of " + str(len(inpfiles)) + ")" + Gaussian.SetupGaussian(ds, ds + 'ginp', 3, settings, + adjRMSDcutoff) + elif settings.DFT == 'n' or 'w': + print "\nNWChem setup for file " + ds +\ + " (" + str(i) + " of " + str(len(inpfiles)) + ")" + NWChem.SetupNWChem(ds, ds + 'nwinp', 3, settings, + adjRMSDcutoff) + i += 1 + QRun = False + else: + QRun = True + + if settings.DFT == 'z' or settings.DFT == 'g': + Files2Run = Gaussian.GetFiles2Run(inpfiles, settings) + elif settings.DFT == 'n' or 'w': + Files2Run = NWChem.GetFiles2Run(inpfiles, settings) + print Files2Run + if len(Files2Run) == 0: + QRun = True + + if len(Files2Run) > settings.HardConfLimit: + print "Hard conformation count limit exceeded, DFT calculations aborted." + quit() + + if QRun: + print 'DFT has already been run for these inputs. Skipping...' + else: + if settings.DFT == 'z': + print '\nRunning Gaussian on Ziggy...' + + #Run Gaussian jobs on Ziggy cluster in folder named after date + #and time in the short 1processor job queue + #and wait until the last file is completed + now = datetime.datetime.now() + MaxCon = settings.MaxConcurrentJobs + if settings.DFTOpt: + for i in range(len(Files2Run)): + Files2Run[i] = Files2Run[i][:-5] + '.com' + if len(Files2Run) < MaxCon: + Gaussian.RunOnZiggy(now.strftime('%d%b%H%M'), settings.queue, + Files2Run, settings) + else: + print "The DFT calculations will be done in " +\ + str(math.ceil(len(Files2Run)/MaxCon)) + " batches" + i = 0 + while((i+1)*MaxCon < len(Files2Run)): + print "Starting batch nr " + str(i+1) + Gaussian.RunOnZiggy(now.strftime('%d%b%H%M')+str(i+1), + settings.queue, Files2Run[(i*MaxCon):((i+1)*MaxCon)], settings) + i += 1 + print "Starting batch nr " + str(i+1) + Gaussian.RunOnZiggy(now.strftime('%d%b%H%M')+str(i+1), + settings.queue, Files2Run[(i*MaxCon):], settings) + + elif settings.DFT == 'n': + print '\nRunning NWChem locally...' + NWChem.RunNWChem(Files2Run, settings) + elif settings.DFT == 'w': + print '\nRunning NWChem on Ziggy...' + + #Run NWChem jobs on Ziggy cluster in folder named after date + #and time in the short 1 processor job queue + #and wait until the last file is completed + now = datetime.datetime.now() + MaxCon = settings.MaxConcurrentJobs + if len(Files2Run) < MaxCon: + NWChem.RunOnZiggy(now.strftime('%d%b%H%M'), settings.queue, + Files2Run, settings) + else: + print "The DFT calculations will be done in " +\ + str(math.ceil(len(Files2Run)/MaxCon)) + " batches" + i = 0 + while((i+1)*MaxCon < len(Files2Run)): + print "Starting batch nr " + str(i+1) + NWChem.RunOnZiggy(now.strftime('%d%b%H%M')+str(i+1), + settings.queue, Files2Run[(i*MaxCon):((i+1)*MaxCon)], settings) + i += 1 + print "Starting batch nr " + str(i+1) + NWChem.RunOnZiggy(now.strftime('%d%b%H%M')+str(i+1), + settings.queue, Files2Run[(i*MaxCon):], settings) + + if (numDS < 2): + print "DP4 requires at least 2 candidate structures!" + else: + allargs = [] + for i in range(numDS): + allargs.append(settings.NTaut) + allargs.extend(inpfiles[i*settings.NTaut:(i+1)*settings.NTaut]) + allargs.append(ExpNMR) + DP4outp = NMRDP4GTF.main(numDS, settings, *allargs) + print '\nWriting the DP4 output to DP4outp' + if not settings.EP5: + if nfiles == 1: + DP4_ofile = open(filename + '.dp4', 'w') + else: + DP4_ofile = open(filename[0] + '.dp4', 'w') + else: + if nfiles == 1: + DP4_ofile = open(filename + '_emp.dp4', 'w') + else: + DP4_ofile = open(filename[0] + '_emp.dp4', 'w') + DP4_ofile.write(DP4outp) + DP4_ofile.close() + print 'DP4 process completed successfully.' + + +def getScriptPath(): + return os.path.dirname(os.path.realpath(sys.argv[0])) + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='PyDP4 script to setup\ + and run Tinker, Gaussian (on ziggy) and DP4') + parser.add_argument('-m', '--mm', help="Select molecular mechanics program,\ + t for tinker or m for macromodel, default is t", choices=['t', 'm'], + default='t') + parser.add_argument('-d', '--dft', help="Select DFT program, j for Jaguar,\ + g for Gaussian, n for NWChem, z for Gaussian on ziggy, w for NWChem on\ + ziggy, default is z", choices=['j', 'g', 'n', 'z', 'w'], default='z') + parser.add_argument('--StepCount', help="Specify\ + stereocentres for diastereomer generation") + parser.add_argument('StructureFiles', nargs='+', default=['-'], help= + "One or more SDF file for the structures to be verified by DP4. At least one\ + is required, if automatic diastereomer and tautomer generation is used.\ + One for each candidate structure, if automatic generation is not used") + parser.add_argument("ExpNMR", help="Experimental NMR description, assigned\ + with the atom numbers from the structure file") + parser.add_argument("-s", "--solvent", help="Specify solvent to use\ + for dft calculations") + parser.add_argument("-q", "--queue", help="Specify queue for job submission\ + on ziggy", default='s1') + parser.add_argument("-t", "--ntaut", help="Specify number of explicit\ + tautomers per diastereomer given in structure files, must be a multiple\ + of structure files", type=int, default=1) + parser.add_argument("-r", "--rot5", help="Manually generate conformers for\ + 5-memebered rings", action="store_true") + parser.add_argument('--ra', help="Specify ring atoms, for the ring to be\ + rotated, useful for molecules with several 5-membered rings") + parser.add_argument("--AssumeDFTDone", help="Assume RMSD pruning, DFT setup\ + and DFT calculations have been run already", action="store_true") + parser.add_argument("-g", "--GenOnly", help="Only generate diastereomers\ + and tinker input files, but don't run any calculations", action="store_true") + parser.add_argument('-c', '--StereoCentres', help="Specify\ + stereocentres for diastereomer generation") + parser.add_argument('-T', '--GenTautomers', help="Automatically generate\ + tautomers", action="store_true") + parser.add_argument('-o', '--DFTOpt', help="Optimize geometries at DFT\ + level before NMR prediction", action="store_true") + parser.add_argument('--pd', help="Use python port of DP4", action="store_true") + parser.add_argument('--ep5', help="Use EP5", action="store_true") + parser.add_argument('-n', '--Charge', help="Specify\ + charge of the molecule. Do not use when input files have different charges") + parser.add_argument('-b', '--BasicAtoms', help="Generate protonated states\ + on the specified atoms and consider as tautomers") + parser.add_argument('-f', '--ff', help="Selects force field for the \ + conformational search, implemented options 'mmff' and 'opls' (2005\ + version)", choices=['mmff', 'opls'], default='mmff') + args = parser.parse_args() + print args.StructureFiles + print args.ExpNMR + settings.NTaut = args.ntaut + settings.DFT = args.dft + settings.queue = args.queue + settings.ScriptDir = getScriptPath() + settings.ForceField = args.ff + + if args.pd: + settings.PDP4 = True + settings.EP5 = False + else: + settings.PDP4 = False + + if args.ep5: + settings.EP5 = True + settings.PDP4 = False + else: + settings.EP5 = False + + if args.mm == 't': + settings.MMTinker = True + settings.MMMacromodel = False + else: + settings.MMMacromodel = True + settings.MMTinker = False + if args.DFTOpt: + settings.DFTOpt = True + if args.BasicAtoms is not None: + settings.BasicAtoms =\ + [int(x) for x in (args.BasicAtoms).split(',')] + settings.GenProt = True + if args.StepCount is not None: + settings.MMstepcount = int(args.StepCount) + if args.Charge is not None: + settings.charge = int(args.Charge) + if args.GenTautomers: + settings.GenTaut = True + if args.StereoCentres is not None: + settings.SelectedStereocentres =\ + [int(x) for x in (args.StereoCentres).split(',')] + if args.GenOnly: + settings.GenOnly = True + if args.AssumeDFTDone: + settings.AssumeDone = True + if args.solvent: + settings.Solvent = args.solvent + if args.rot5: + settings.Rot5Cycle = True + if args.ra is not None: + settings.RingAtoms =\ + [int(x) for x in (args.ra).split(',')] + if len(args.StructureFiles) == 1: + main(args.StructureFiles[0], args.ExpNMR, 1) + else: + main(args.StructureFiles, args.ExpNMR, len(args.StructureFiles)) + #main() diff --git a/README b/README new file mode 100644 index 0000000..515d494 --- /dev/null +++ b/README @@ -0,0 +1,273 @@ +=============================================================== + +PyDP4 workflow integrating MacroModel/TINKER, Gaussian/NWChem +and DP4 analysis + +version 0.4 + +Copyright (c) 2015 Kristaps Ermanis, Jonathan M. Goodman + +distributed under MIT license + +=============================================================== + +CONTENTS +1) Requirements and Setup +2) Usage +3) NMR Description Format +4) Included Utilites +5) Code Organization + +=============================================================== + +REQUIREMENTS AND SETUP + +All the python and Java files and one utility to convert to and from TINKER +nonstandard xyz file are in the attached archive. They are set up to work +from a centralised location. It is not a requirement, but it is probably +best to add the location to the PATH variable. + +The script currently is set up to use MacroModel for molecular mechanics and +NWChem for DFT and it runs NWChem locally. Gaussian and TINKER is also supported +This setup has several requirements. + +1) One should have MacroModel or TINKER and NWChem or Gaussian. +The beginning PyDP4.py file contains a structure "Settings", where the location +of the TINKER scan executable or MacroModel bmin executable should be specified. + +2) The various manipulations of sdf files (renumbering, ring corner flipping) +requires OpenBabel, including Python bindings. The following links provide +instructions for building OpenBabel with Python bindings: +http://openbabel.org/docs/dev/UseTheLibrary/PythonInstall.html +http://openbabel.org/docs/dev/Installation/install.html#compile-bindings +The Settings structure contain path to OpenBabel, but currently it also +needs to be specified in InchiGen.py and FiveConf.py +This dependency can be ignored, if no diastereomer generation or +5 membered ring flipping is done. + +3) Finally, to run calculations on a computational cluster, a passwordless +ssh connection should be set up in both directions - +desktop -> cluster and cluster -> desktop. In most cases the modification +of the relevant functions in Gaussian.py or NWChem.py will be required +to fit your situation. + +4) All development and testing was done on Linux. However, both the scripts +and all the backend software should work equally well on windows with little +modification. + +=================== + +USAGE + +To call the script: +1) With all diastereomer generation: + +PyDP4.py Candidate CandidateNMR + +where Candidate is the sdf file containing 3D coordinates of the candidate +structure (without the extension), +and CandidateNMR contains the NMR description. The NMR description largely +follows the DP4 format, but see bellow for differences. + +Alternatively: + +PyDP4.py -s chloroform Candidate CandidateNMR + +specifies solvent for DFT calculation. If solvent is not given, no solvent is used. + +2) With explicit diastereomer/other candidate structures: + +PyDP4.py Candidate1 Candidate2 Candidate3 ... CandidateNMR + +The script does not attempt to generate diastereomers, simply carries out the +DP4 on the specified candidate structures. + +Script has several other switches, including switching the molecular mechanics and dft software etc. + + -m {t,m}, --mm {t,m} Select molecular mechanics program, t for tinker or m + for macromodel, default is t + + -d {j,g,n,z,w}, --dft {j,g,n,z,w} + Select DFT program, j for Jaguar, g for Gaussian, n + for NWChem, z for Gaussian on ziggy, w for NWChem on + ziggy, default is z (jaguar is not yet implemented) + + --StepCount STEPCOUNT + Specify stereocentres for diastereomer generation + + -s SOLVENT, --solvent SOLVENT + Specify solvent to use for dft calculations + + -q QUEUE, --queue QUEUE + Specify queue for job submission on ziggy + (default is s1) + + -t NTAUT, --ntaut NTAUT + Specify number of explicit tautomers per diastereomer + given in structure files, must be a multiple of + structure files + + -r, --rot5 Manually generate conformers for 5-memebered rings + + --ra RA Specify ring atoms, for the ring to be rotated, useful + for molecules with several 5-membered rings + + --AssumeDFTDone Assume RMSD pruning, DFT setup and DFT calculations + have been run already (saves time when repeating DP4 + analysis) + + -g, --GenOnly Only generate diastereomers and tinker input files, + but don't run any calculations (useful for diastereomer + generation for calculations ran on computers + without OpenBabel) + + -c STEREOCENTRES, --StereoCentres STEREOCENTRES + Specify stereocentres for diastereomer generation + + -T, --GenTautomers Automatically generate tautomers + + -o, --DFTOpt Optimize geometries at DFT level before NMR prediction + + --pd Use python port of DP4 + + -b BASICATOMS, --BasicAtoms BASICATOMS + Generate protonated states on the specified atoms and + consider as tautomers + +More information on those can be obtained by running PyDP4.py -h + +====================== + +NMR DESCRIPTION FORMAT + +NMRFILE example begins: +59.58(C3),127.88(C11),127.52(C10),115.71(C9),157.42(C8),133.98(C23),118.22(C22),115.79(C21),158.00(C20),167.33(C1),59.40(C2),24.50(C31),36.36(C34),71.05(C37),142.14(C42),127.50(C41),114.64(C40),161.02(C39) + +4.81(H5),7.18(H15),6.76(H14),7.22(H28),7.13(H27),3.09(H4),1.73(H32 or H33),1.83(H32 or H33),1.73(H36 or H35),1.73(H36 or H35),4.50(H38),7.32(H47),7.11(H46) + +H15,H16 +H14,H17 +H28,H29 +H27,H30 +H47,H48 +H46,H49 +C10,C12 +C9,C13 +C22,C24 +C21,C25 +C41,C43 +C40,C44 + +OMIT H19,H51 + +:example ends + +Sections are seperated by empty lines. +1) The first section is assigned C shifts, can also be (any). +2) Second section is (un)assigned H shifts. +3) This section defines chemically equivalent atoms. Each line is a new set, +all atoms in a line are treated as equivalent, their computed shifts averaged. +4) Final section, starting with a keyword OMIT defines atoms to be ignored. +Atoms defined in this section do not need a corresponding shift in the NMR +description + + +===================== + +UTILITIES + +There are 2 utilities included, not necessary for the process, but sometimes +useful. + +If the DP4 workflow fails at the TINKER stage, the 2 likely reasons are either +lack of 1gb of free memory or TINKER not accepting the numbering of the sdf +file (this is a bug in TINKER). The latter can be fixed by running the following +script: + +TreeRenum.py Candidate CandidateNMR + +It takes the sdf file and performs a spanning tree renumbering - making sure, +that there are as many connected atoms in sequence as possible. So far this +has always solved the TINKER problem. +The script also renumbers the NMR description file, if it contains any atom numbers. +The renumbered files are saved as Candidater and CandidateNMRr (r appended to their +original name). + +---------------------- +Another utility is NMRhelper (called by simply typing NMRhelper.py in shell). +It is a script with GUI interface, that assists in describing and assigning the +NMR. In the top textbox a structure file can be chosen. This allows the utility +to automatically detect protons attached to heteroatoms and add them to the +OMIT list, as well as detect the chemically eqivalent atoms (currently only +implemented for methyl groups). It also lets the script to help tracking which +atoms are yet to be assigned (show in the bottom 2 text boxes). +The next 2 large textboxes are for pasting raw NMR descriptions.Based on the pasted +text, the script will try to detect the shifts and make up a rough draft of the +description file. After this the final version can be prepared in the main textbox. +At any point the button to generate the NMR file can be pressed and this will write +the file to the NMRhelper folder with the name CandidateNMR, where Candidate is the +name of the structure file. +IMPORTANT NOTE: Do not edit the raw data textboxes, if you have done any work in the +main textbox, as this will cause the main textbox to revert to the rough +automatically generated version + + +===================== + +CODE ORGANIZATION + +The code is organized in several python script files, as well as several java +files. + +PyDP4.py +Main file, that should be called to start the PyDP4 workflow. Interprets the +arguments and takes care of the general workflow logic. + +InchiGen.py +Gets called if diastereomer and/or tautomer and/or protomer generation is +used. Called by PyDP4.py. + +FiveConf.py +Gets called if automatic 5-membered cycle corner-flipping is used. Called by +PyDP4.py. + +MacroModel.py +Contains all of the MacroModel specific code for input generation, calculation +execution and output interpretation. Called by PyDP4.py. + +Tinker.py +Contains all of the Tinker specific code for input generation, calculation +execution and output interpretation. Called by PyDP4.py. + +ConfPrune.pyx +Cython file for conformer alignment and RMSD pruning. Called by Gaussian.py +and NWChem.py + +Gaussian.py +Contains all of the Gaussian specific code for input generation and calculation +execution. Called by PyDP4.py. + +NWChem.py +Contains all of the NWChem specific code for input generation and calculation +execution. Called by PyDP4.py. + +NMRDP4GTF.py +Takes care of all the NMR description interpretation, equivalent atom +averaging, Boltzmann averaging, tautomer population optimisation (if used) +and DP4 input preparation and running either DP4.jar or DP4.py. Called by +PyDP4.py + +nmrPredictNWChem.py +Extracts NMR shifts from NWChem output files + +nmrPredictGaussian.java +Extracts NMR shifts from Gaussian output files + +DP4.jar +Original DP4 implementation as in J. Am. Chem. Soc. 2010, 132, 12946. + +DP4.py +Equivalent and compact port to python of the same DP4 process. The results +produced are essentially equivalent, but not identical due to different +floating point precision used in the Python (53 bits) and Java (32 bits) +implementation. diff --git a/Tinker.py b/Tinker.py new file mode 100755 index 0000000..8edbb08 --- /dev/null +++ b/Tinker.py @@ -0,0 +1,264 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jun 12 12:52:21 2015 + +@author: ke291 + +Contains all of the Tinker specific code for input generation, calculation +execution and output interpretation. Called by PyDP4.py. +""" + +import os +import sys +import subprocess + + +def SetupTinker(numDS, settings, *args): + + print args + for ds in args: + if settings.Rot5Cycle is True: + if not os.path.exists(ds+'rot.sdf'): + import FiveConf + #Generate the flipped fivemembered ring, + #result is put in '*rot.sdf' file + FiveConf.main(ds + '.sdf', settings) + + #Makes sure that the name in the title line matches filename + #sdf2tinkerxyz uses this as the output file name + f = open(ds + '.sdf', 'r+') + sdf = f.readlines() + sdf[0] = ds + '\n' + f.seek(0) + f.writelines(sdf) + f.close() + + if settings.Rot5Cycle is True: + f = open(ds + 'rot.sdf', 'r+') + sdf = f.readlines() + sdf[0] = ds + 'rot\n' + if ds in sdf[-3]: + sdf[-3] = ds + 'rot.1\n' + f.seek(0) + f.writelines(sdf) + f.close() + + scriptdir = getScriptPath() + convinp = 'sdf2tinkerxyz -k ' + scriptdir + '/default.key <' + + outp = subprocess.check_output(convinp + ds + '.sdf', shell=True) + + print "Tinker input for " + ds + " prepared." + + if settings.Rot5Cycle is True: + outp = subprocess.check_output(convinp + ds + 'rot.sdf', + shell=True) + print "Tinker input for " + ds + "rot prepared." + + +def RunTinker(numDS, settings, *args): + #Run Tinker scan for all diastereomeric inputs + + NCompleted = 0 + + for ds in args: + print settings.TinkerPath + ds + ' 0 10 20 0.00001 | tee ./' + ds + \ + '.tout' + outp = subprocess.check_output(settings.TinkerPath + ds + + ' 0 10 20 0.00001 | tee ./' + ds + '.tout', shell=True) + NCompleted = NCompleted + 1 + print "Tinker job " + str(NCompleted) + " of " + str(numDS) + \ + " completed." + + if settings.Rot5Cycle is True: + print settings.TinkerPath + ds + 'rot 0 10 20 0.00001 | tee ./' + \ + ds + 'rot.tout' + outp = subprocess.check_output(settings.TinkerPath + ds + + 'rot 0 10 20 0.00001 | tee ./' + ds + 'rot.tout', shell=True) + NCompleted = NCompleted + 1 + print "Tinker job " + str(NCompleted) + " of " + str(numDS*2) + \ + " completed." + + +#Reads the relevant tinker geometry files +#v0.2 - reads seperate rot file as well +def ReadTinker(TinkerOutput, settings): + + #Get conformer energies + ETable, charge = GetEnergiesCharge(TinkerOutput, settings) + + if settings.Rot5Cycle is True: + #Get conformer energies for the flipped 5-membered ring + ETableRot, charge = GetEnergiesCharge(TinkerOutput + 'rot', settings) + + #Determine which conformers we want + MinE = 10000 + MinE = min([float(x[1]) for x in ETable]) + + if settings.Rot5Cycle is True: + MinERot = 10000 + MinERot = min([float(x[1]) for x in ETableRot]) + if MinE > MinERot: + MinE = MinERot + + FileNums = [] + RotFileNums = [] + + AcceptedEs = [] + + for conf in ETable: + if float(conf[1]) < MinE + settings.MaxCutoffEnergy: + #Dealing with special case when nconf>100 000 + if 'Minimum' in conf[0]: + data = conf[0].strip() + FileNums.append(data[7:].strip()) + else: + FileNums.append(conf[0].strip()) + AcceptedEs.append(float(conf[1])) + + if settings.Rot5Cycle is True: + for conf in ETableRot: + if float(conf[1]) < MinE + settings.MaxCutoffEnergy: + RotFileNums.append(conf[0].strip()) + AcceptedEs.append(float(conf[1])) + + print "Number of accepted conformers by energies: " + str(len(AcceptedEs)) + + Files = [] + #Generate conformer filenames + for num in FileNums: + Files.append(TinkerOutput + '.' + num.zfill(3)) + if settings.Rot5Cycle is True: + for num in RotFileNums: + Files.append(TinkerOutput + 'rot.' + num.zfill(3)) + + conformers = [] + conformer = 0 + atoms = [] + + for f in Files: + conformers.append([]) + + atom = 0 + infile = open(f, 'r') + inp = infile.readlines() + + for line in inp[1:]: + data = line.split(' ') + data = filter(None, data) + if conformer == 0: + atoms.append(GetTinkerSymbol(int(data[5]))) # Add atom symbol + conformers[conformer].append([]) # Add new atom + conformers[conformer][atom].append(data[0]) # add atom number + conformers[conformer][atom].append(data[2]) # add X + conformers[conformer][atom].append(data[3]) # add Y + conformers[conformer][atom].append(data[4]) # add Z + atom = atom + 1 # Move to the next atom + + infile.close() + conformer = conformer + 1 # Move to the next conformer + + return atoms, conformers, charge + + +# Get energies of conformers from tinker output file +def GetEnergiesCharge(TinkerOutput, settings): + + infile = open(TinkerOutput + '.tout', 'r') + + inp = infile.readlines() + if len(inp) < 56: + print "Tinker output " + TinkerOutput + " is corrupted, aborting." + quit() + + #Get the conformer energies from the file + ETable = [] + for line in inp[13:]: + data = line.split(' ') + data = filter(None, data) + if len(data) >= 3: + if 'Map' in data[0] and 'Minimum' in data[1]: + ETable.append(data[-2:]) + #print data + + infile.close() + if settings.charge is None: + if os.path.exists(TinkerOutput+'.inchi'): + return ETable, GetInchiCharge(TinkerOutput) + else: + return ETable, GetSDFCharge(TinkerOutput) + else: + return ETable, settings.charge + + +#translate Tinker atom types to element symbols for NWChem file +def GetTinkerSymbol(atomType): + + Lookup = ['C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', ' ', 'C', + 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', + 'C', 'C', 'H', 'H', 'O', 'O', 'O', 'O', 'O', 'O', + 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', + 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'N', 'N', + 'N', 'N', 'N', 'N', 'N', 'F', 'Cl', 'Br', 'I', 'S', + 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', + 'Si', 'C', 'H', 'H', 'H', 'C', 'H', 'H', 'H', 'H', + 'H', 'H', 'H', 'H', 'P', 'P', 'P', 'P', 'P', 'P', + 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', + 'H', 'H', 'H', 'H', 'C', 'H', 'O', 'O', 'O', 'O', + 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', + 'O', 'H', 'N', 'O', 'O', 'H', 'H', 'H', 'H', 'H', + 'H', 'H', 'C', 'N', 'N', 'N', 'N', 'N', 'N', 'C', + 'C', 'N', 'N', 'N', 'N', 'N', 'N', 'S', 'N', 'N', + 'N', 'N', 'N', 'O', 'H', 'O', 'H', 'N', 'N', 'N', + 'N', 'N', 'C', 'C', 'N', 'O', 'C', 'N', 'N', 'C', + 'C', 'N', 'N', 'N', 'N', 'N', 'O', 'H', 'H', 'H', + 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'P', 'N', 'Cl', + 'C', 'N', 'C', 'N', 'N', 'N', 'N', 'N', 'N', 'N', + 'Fe', 'Fe', 'F', 'Cl', 'Br', 'Li', 'Na', 'K', 'Zn', 'Zn', + 'Ca', 'Cu', 'Cu', 'Mg'] + + if Lookup[atomType-1] == ' ': + print 'Unknown atom type' + + return Lookup[atomType-1] + + +def GetInchiCharge(inchifile): + + infile = open(inchifile + '.inchi', 'r') + inp = infile.readlines() + infile.close() + + ChargeFound = False + #Get inchi layers + layers = inp[0].split('/') + for l in layers[1:]: + if 'q' in l: + charge = int(l[1:]) + ChargeFound = True + break + if 'p' in l: + charge = int(l[1:]) + ChargeFound = True + break + if not ChargeFound: + charge = 0 + + return charge + + +def GetSDFCharge(sdf): + import sys + sys.path.append('/home/ke291/Tools/openbabel-install/lib/python2.7/site-packages/') + import openbabel + + obconversion = openbabel.OBConversion() + obconversion.SetInFormat("sdf") + obmol = openbabel.OBMol() + obconversion.ReadFile(obmol, sdf+'.sdf') + + return obmol.GetTotalCharge() + + +def getScriptPath(): + return os.path.dirname(os.path.realpath(sys.argv[0])) diff --git a/TreeRenum.py b/TreeRenum.py new file mode 100755 index 0000000..0eecdde --- /dev/null +++ b/TreeRenum.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on Tue Mar 31 14:54:17 2015 + +@author: ke291 +""" +import sys +sys.path.append('/home/ke291/Tools/openbabel-install/lib/python2.7/site-packages/') +from openbabel import * + + +def FindAllPaths(molgraph, start, end, path=[]): + path = path + [start] + if start == end: + return [path] + if start not in [x[0] for x in molgraph]: + print "No such node in graph" + return [] + paths = [] + for node in molgraph[start-1][1:]: + if node not in path: + newpaths = FindAllPaths(molgraph, node, end, path) + for newpath in newpaths: + paths.append(newpath) + return paths + + +def FindTerminatingPaths(molgraph, start, trunk, path=[]): + path = path + [start] + if start not in [x[0] for x in molgraph]: + print "No such node in graph" + return [] + paths = [] + for node in molgraph[start-1][1:]: + if node not in path and node not in trunk: + newpaths = FindTerminatingPaths(molgraph, node, trunk, path) + for newpath in newpaths: + paths.append(newpath) + if paths == []: + return [path] + else: + return paths + + +def FindTreeMap(f): + #get molecular graph + molgraph = GenMolGraph(f) + + #Find potential endpoints - atoms with only one neighbour + endpoints = [] + for node in molgraph: + if len(node) < 3: + endpoints.append(node[0]) + + #get the longest paths for all endpoint combinations + maxpaths = [] + for i in range(0, len(endpoints)): + for j in range(0, len(endpoints)): + if i != j: + maxpaths.append(max(FindAllPaths(molgraph, endpoints[i], + endpoints[j]), key=len)) + #get the absolute longest path possible in the molecule + molmap = max(maxpaths, key=len) + #Add longest branches to the longest trunk + for atom in molmap: + for node in molgraph[atom-1]: + if node not in molmap: + maxbranch = [] + branches = FindTerminatingPaths(molgraph, node, molmap) + if branches != []: + maxbranch = max(branches, key=len) + if maxbranch != []: + molmap.extend(maxbranch) + return molmap + + +def TreeRenumSDF(f, ExpNMR): + + molmap = FindTreeMap(f) + + #Read molecule from file + obconversion = OBConversion() + obconversion.SetInFormat("sdf") + obmol = OBMol() + obconversion.ReadFile(obmol, f) + + temp = [] + anums = [] + for atom in OBMolAtomIter(obmol): + temp.append(atom) + anums.append(atom.GetAtomicNum()) + + newmol = OBMol() + for atom in molmap: + newmol.AddAtom(temp[atom-1]) + + #Restore the bonds + newmol.ConnectTheDots() + newmol.PerceiveBondOrders() + #Write renumbered molecule to file + obconversion.SetOutFormat("sdf") + obconversion.WriteFile(newmol, f[:-4] + 'r.sdf') + + #Prepare NMR translation + NMRmap = [] + i = 1 + for atom in molmap: + anum = anums[atom-1] + if anum == 1: + NMRmap.append(['H' + str(atom), 'H' + str(i)]) + if anum == 6: + NMRmap.append(['C' + str(atom), 'C' + str(i)]) + i += 1 + print NMRmap + RenumNMR(ExpNMR, NMRmap) + + +def RenumNMR(ExpNMR, NMRmap): + f = open(ExpNMR, 'r') + NMRfile = f.read(1000000) + f.close() + + print '\nOld NMR file:\n' + NMRfile + + #Replace old atom labels with new atom labels + #tag replacements with '_' to avoid double replacement + for atom in NMRmap: + NMRfile = NMRfile.replace(atom[0] + ')', atom[1] + '_)') + NMRfile = NMRfile.replace(atom[0] + ' ', atom[1] + '_ ') + NMRfile = NMRfile.replace(atom[0] + ',', atom[1] + '_,') + NMRfile = NMRfile.replace(atom[0] + '\n', atom[1] + '_\n') + + #Strip temporary udnerscore tags + NMRfile = NMRfile.replace('_', '') + + print '\nNew NMR file:\n' + NMRfile + f = open(ExpNMR + 'r', 'w') + f.write(NMRfile) + f.close() + + +def GenMolGraph(f): + obconversion = OBConversion() + obconversion.SetInFormat("sdf") + obmol = OBMol() + obconversion.ReadFile(obmol, f) + + molgraph = [] + + for atom in OBMolAtomIter(obmol): + idx = atom.GetIdx() + molgraph.append([]) + molgraph[idx-1].append(idx) + + for NbrAtom in OBAtomAtomIter(atom): + molgraph[idx-1].append(NbrAtom.GetIdx()) + + return molgraph + +if __name__ == '__main__': + #print sys.argv + ExpNMR = sys.argv[2] + filename = sys.argv[1] + '.sdf' + TreeRenumSDF(filename, ExpNMR) + #main() \ No newline at end of file diff --git a/default.com b/default.com new file mode 100644 index 0000000..a8e1a29 --- /dev/null +++ b/default.com @@ -0,0 +1,18 @@ +confsearch.mae +confsearch-out.maegz + MMOD 0 1 0 0 0.0000 0.0000 0.0000 0.0000 + FFLD 10 1 0 0 1.0000 0.0000 0.0000 0.0000 + BDCO 0 0 0 0 41.5692 99999.0000 0.0000 0.0000 + READ 0 0 0 0 0.0000 0.0000 0.0000 0.0000 + CRMS 0 0 0 0 0.0000 0.2500 0.0000 0.0000 + LMCS 5000 0 0 0 0.0000 0.0000 3.0000 6.0000 + NANT 0 0 0 0 0.0000 0.0000 0.0000 0.0000 + MCNV 0 0 0 0 0.0000 0.0000 0.0000 0.0000 + MCSS 2 0 0 0 21.0000 0.0000 0.0000 0.0000 + MCOP 1 0 0 0 0.5000 0.0000 0.0000 0.0000 + DEMX 0 333333 0 0 21.0000 42.0000 0.0000 0.0000 + MSYM 0 0 0 0 0.0000 0.0000 0.0000 0.0000 + AUOP 0 0 0 0 500.0000 0.0000 0.0000 0.0000 + AUTO 0 2 1 1 0.0000 -1.0000 0.0000 2.0000 + CONV 2 0 0 0 0.0010 0.0000 0.0000 0.0000 + MINI 1 0 999999 0 0.0000 0.0000 0.0000 0.0000 diff --git a/default.key b/default.key new file mode 100644 index 0000000..2feb51d --- /dev/null +++ b/default.key @@ -0,0 +1,11 @@ + +# Force Field Selection +PARAMETERS /home/ke291/tinker/params/mmff.prm + +# Random Number +RANDOMSEED 123456789 + +# Constriant And Restraint +ENFORCE-CHIRALITY + + diff --git a/default1.com b/default1.com new file mode 100644 index 0000000..9468638 --- /dev/null +++ b/default1.com @@ -0,0 +1,17 @@ +default1.mae +default1-out.maegz + MMOD 0 1 0 0 0.0000 0.0000 0.0000 0.0000 + FFLD 10 1 0 0 1.0000 0.0000 0.0000 0.0000 + BDCO 0 0 0 0 41.5692 99999.0000 0.0000 0.0000 + READ 0 0 0 0 0.0000 0.0000 0.0000 0.0000 + CRMS 0 0 0 0 0.0000 0.2500 0.0000 0.0000 + LMCS 6000 0 0 0 0.0000 0.0000 3.0000 6.0000 + MCNV 0 0 0 0 0.0000 0.0000 0.0000 0.0000 + MCSS 2 0 0 0 21.0000 0.0000 0.0000 0.0000 + MCOP 1 0 0 0 0.5000 0.0000 0.0000 0.0000 + DEMX 0 333333 0 0 21.0000 42.0000 0.0000 0.0000 + MSYM 0 0 0 0 0.0000 0.0000 0.0000 0.0000 + AUOP 0 0 0 0 300.0000 0.0000 0.0000 0.0000 + AUTO 0 2 1 1 0.0000 -1.0000 0.0000 3.0000 + CONV 2 0 0 0 0.0010 0.0000 0.0000 0.0000 + MINI 1 0 999999 0 0.0000 0.0000 0.0000 0.0000 diff --git a/nmrPredictGaussian.class b/nmrPredictGaussian.class new file mode 100644 index 0000000000000000000000000000000000000000..197792a86e2b6cb449bd6f36802333bc4f7012c6 GIT binary patch literal 3616 zcma)8Yj6|S75?t7q+PF9k|kSUVG|V0Lx7EAlb9ew>Vkoo00tXKj2n_gUK=5K*GMae zN7Fp&&=yEYN*+K+Xj~GKP$q*(WLgq_G(+38Kj`#FGed{TbUJCLGwqZ=ZD$x0_uQ3) z1ei1$`<{ExJ+J%SbI#u1e)WqB02bhsf@Z9j&>^8yfr#~d*dSvgr<-IvEa4FeT@p4c zC`Gpd1>aFnf=BtVg%6L(cwB)1jW4!Jh$^T+j|5#tuY^7Ym0Yb7+Z4pGoeyyZ3H0;9 zC_x0KtL0z|1q0HLV1N%h6eN*ywRG8R;#)||7*y~CcFNc#W4DYwGQP`$d{V~uWIQF~ zX})@f%RK993BBc-aizY>)sh_Uy!^tI{!t$K2U zwlzTmVwy3Zdr{GXZ!?tL>X9^VWi3s| z5Pn>H}8HG*rC8x5Vylx(~T?*@{rB zY0$ZYwrtvpC2~-1Fyp!r+s)7MD)viwO~qjxQE?QnOE|_CZ>V?^$5ng_OH?f8!vk0% z;U_BI!mxy&syKnS83K76LrFcAPFSf>ubHfs@Q#X;I6#R}ONU=-Cam54nqh>rK}wK5 zotR8v@MV)DR3FMEOD%`>U52U-J)v2#K|Q3~=|d9nRiS<}QP)7K-gV|cn(v!NC_Hlx zaVB?ei|L6T%8^h$CwUwpZtguE$@>(=?RS6Ejkj;zu16`A%O^-Y2BEu+4+N`A8yL`y z9)`N`1Q~i8E0-_LbRU+P&0~g&@SNKONg^u%zTcCdu2uaM7ehNbuMvYGvV4CVwmxD<229396~ zFtZ!?w%TrH6J|D64Ao?{ z=$zSWF5G-|xb5q~>y2hIFFBi15!yT8eTw?bQd&(VN=*mOxualb zXu`0JMwiX3pq}4~0RR%~(v996@psS7yHL+l!bV{X9F zn1R#pq9FsRbI8F?j$iT15$Ac7be)Gn|D|1ZV&f>(QMgCpi8z9e47^8B#c5f@8Fc!6 z8Tf_c<9{C#Nc%6GUw#rw#1$m7fogIcaaDh;M5LPPX3wSn8MliR;Aahz)W(&_sIzj6Vc}FveaU(N*c4p4aW+n;QDu3cC^Jv9sZI0FznV3~D zL9qB5GMMkb#m41g-TFJ|D&Ii&o;&Dr5WmiE9=CKMRzE2?DaGtVQ|kHVXV6#_cph7R z?i@mSaBuO%;r*PX>Hsg@c zidTe24}@ud?dCJZO5qiFwTjO;pgHuoEM)Xx*wm2hj2kWhF^-m!l&XT zTonIB^xyck1NeMWrHIz=)ZDB_O6L!7WJs?coq7^ZN>82mR8g^i(+ zIP)kP`bw_7@p)Z9rNDpX6}a~-rdOcpENUun|5;2cd~7{bA;4t%=vG2SkUqEVv=*@L Ef3jCx@&Et; literal 0 HcmV?d00001 diff --git a/nmrPredictGaussian.java b/nmrPredictGaussian.java new file mode 100644 index 0000000..1d7da3e --- /dev/null +++ b/nmrPredictGaussian.java @@ -0,0 +1,208 @@ +import java.io.*; +import java.util.*; +import java.math.*; + + +/* +This program reads Gaussian output files and pulls out the NMR shielding constants for each atom in each conformer. It also extracts energies and calculates a Boltzmann-averaged shielding constant for each atom. + +Input syntax is the names of the output files without the .out: + +java nmrPredictGaussian myMoleculeCnf001 myMoleculeCnf002 myMoleculeCnf003 .... + +(or java nmrPredictGaussian `ls myMoleculeCnf*.out | cut -d"." -f1`) + +The program prints the output as a comma separated list suitable for pasting into a spreadsheet. + +*/ + + +class nmrPredictGaussian +{ + + static double gasConstant = 8.3145; + static double temperature = 298.15; + static double hartreeEnergy = 2625.499629554010; + + + public static void main(String[] args) + { + int nstructs = args.length; + int natoms = 0; + + String[] fileName = new String[nstructs]; + for(int i=0;i0 zuBxuC?wOuh6v!X#@p!su?q7=5Me_h|TINm7!nNdWm64_Sv{bFXc7}GE)(uI_%c?{f z{F4f()D>x7{3Ed};8|73$G;PS^B?m_oB!mGb@6Z3)0)PAtgofJ`KtgA?wZPfeNNZ3 zvG~V!nV*772J$nI$A4RZ^B?os2LFvgBV*7u|K$PazeL{HuZ$U6opO-t~3HnTC{`OAwmv<`vey9A~I_0;^ z)_lmjuTwt6pZsHctJ_r|etjprI9Xcp_xDbCv=hF+Q~d><@*|z{-|mEWrxM&h@^g8o z{Qv2MKbk0vE6Z=}lz&Ypytz~PPdeq_)Cu3;sr;#(@`rcIU)>46suO-*C*16WKi7$V zW~clqo$wz!;X^vr@7@U?)Cs??Q~lFA<^Q=8{&FYWbyGrx^3Uy*e^)2`tWNmxo$!Bk z;^#k|@TyMb`**@?JK-mF!i`S!=5)#**D3!Z9^|YPwbidOo*&Pn7>7^7Go^Y3=GWUz;-JmfPmdol<5@D>bG}(cluN(-mH# zO}XM4q|TTGx%tM{orP@rRc+MQwnafILvu~L@Z4L^S-l~-rPn&@fQ%beqTsUA!m@{YI zbQDIL+AYOK$!xY-Casnh-#L5k3~eTo%SdhxS^_mNWGpBvHniJ{Z{r0y_#b^LE-jrq zZwkmwGiJ}5tCij+%#;-u->OZYMR-QAYfmq0Pi~y8M=DGo=&SZPQ$? zN$-kLb4A@_JWiXV9D*^~FuiZ5*mDf~FOXC&v(|PTIxPsSXHLW+V zX`0rDSM+y(fc-1wnKd)iz3$Z1yXKR|l z>p7Y>pI5)8mGjzH(<*sASJOhg_S3X#Ui)j>5?(QZ)$n@0rq%L#fu=3z^+HWs!Rr7` ztLJr~rajK(_r)g_>&C;}2c)b|07q6GVPx5*x;zeHJ zXt(j2gBXd|A)2<6*P)uWi`QY$Kd+dO_wss~rtRbP_nH>tHCNLLt*^$L`@Jz}SesO} zaq1JAWE<~iF1r$x=8={ik<)3RnkbDFbCa9Y?bAMtv@HE=eoKk*vDy~H6Y&8ZYTl{lvu zr$q2H;Wz z;349JiB}3 zc)H+^6E7jI3H~(k`NR)!ob-K;cqQ?@g0CW8O??)0I?n(k4bCY*c(Z+~zt`>O(9$I{xxxe>g=_ng+GEU%_(EuD1_4=9C zx_#@$&iq_|9lT*5yC}^Lrmp`o#p>ga4T05%o2+iecx$phHJay#JJdAG>yNSjb|5X> zv@nqQNj#$_v&s5!od=RJH+l{}n^Tmwa6n|(N|x?&8QS#HkM4&JS>5e!cE{S~yA}ju+DEErnh^dZD3ip*~XR;iDJowcDe=n zv%n=sDe$6O;6fHSsa*j`O=LE+rh_mecW`bK`CVuSQte<`q)(WtqfTckVo@uw&zRI% zt*0D@S_@OdI~L|bt!K=vu?&AIRN7wk|88k*%_&MEHt9Wj{h`RSwnk@S8092xS8_m|nGsMJC+@V-~w- z!`m>F$oUE|M?oS?)Qk@v<1#6-vKfYmLaB&5G!}+cXt0E-r=w1uKP}b|`SD&gQhIaX zXNmyn-q`P((bId_u3brj(nDNTerl{0`QfHuw0#*G{@3WeN{dQ8G^e#OUDKZ5{V1B? zR}Y;GhuMi|#7ymZ&PwoHhK|Nht>JL~4d3_v%=Rh1qw{@vlJB-M&SNPmofk=$N1Lr1K^kC0F3*W90N1)HH=tLP_q z5v$_Ys?C(F9$JEOqU;9<%l3e#y>kXHIT&@QfN2n;oAg1(k zjXj2+C<>s+W+wgs8fJ5=F_@*lW@+%W0*TfJWgnSM2;xq*!3kKZ_|@89r1&&1>uJ1+ z5W0sT|gY^kN?LHuz&^Q^ecFC8zkpybzD8@m{rmOe*>mp!}f-$Uoeq zhivvUfPoStzpnBBk(3*mGRU|sQWwJ*u`>Kt-9DsHqODz^r+n(%CZ>LR}Gk$RO^XpF_*j^*xJF@Z{v>AV0{DZmQ$LWV2 zE2Aq-C;W+?w+pS`6x++XFd8)t{qIHtw{$;>$L8S2HJXj|RdZlOAeLrs@^P+#O776i zp*@Z7ks$UUX-Rz_bv)3{YX2>Cz9alJXu9J-DY$L^h5xPEKdh?~e{f%r9aP z)e?M;NZD)CaXp2_ri#65a)eL(UGPH*HwH@TrHm zwrEQ|8<s_@}QxrO%#qhf2sMZ zYJWbGB5T;sXm~rg0?T63sB^lAg<|6-ZZy2p%~pbS$7v+64_!)%a`r(bO3Dut5(mT! z0DmLObiLs^&r#(VJY8=X?HQu_*AsAx>YPz0eh)DaObbnVW1lLug%+T{?55$*=q&X% zX(#26j)fqXHu-)aEC(+!rGp;le2~LD4EYU119pVn4sQ*fYfFCBCQ3}rvJ(C!b$J)! zlK4xQ3hVFjXt9zX;V?y_9 zB=qpdRF$&&cK}jQshk)wfbSO37#m$-8K?;Hz0M};Xu@_6bAvZh)q3Q}k)Pi^YfIls z{2AX^>(@K!f32*9+Vj>&u1wMM-qo5E3G?GFY#yQn-xt)eSD?5#bW#&-D|U;7c6#_4 zsK9L#!$_3|UovHpKXGudI+RfqVm%K?4HtV496CLcdNhE>bd*5dRCoIGp5;#dZae7L zTi>^9)#Z`>eOE#*qbRnG1y-_25b8}K;o8A9lDU2)EPg*jagH9R#!66^xiWXcwsJ-K zh73+G;Hphjcbi=mz9im41(o?~k>Cb;hcvMJqf{JRwMYbvZ}yNJ=BM} z@Q<)5kpT+Xft|D;_F@V9hzj3;TILRp^0&YTX6i8*g>CZ@I?mFfF7$cMcbEkcbl%#Y z3>ZUIE%vA|7JMyphf@XoztKq)_kq^Y$V^mZn&SU-#~IpB#Iw{N=A$ZdUIX}O^?M`z zCG;KMYo}GXokd?qQ+DBYb5lOlV{KQ(gMJZ%+7flb%!FZcl zqxA4i3RL0Th?K-4Q2AeT|Als88iE|VA|3J5AUlwO72fHYhiaJYeF5BxoJWEMbF(KW zn6cK)P8!n47(b zvp?kOA&w*{?L^GW_V4KNK1PCdF#JjIKyoS37LRecnvQTYe8@P@?uJ%RvJTdemB&eW zl-Jxy!rxiJRC7~0rgmou(iOOo>yhO8(v}saT_X|44x=|&)yS%(w}s23J%+SRE-j;y z3(aU9Yi@2Ukz63K=JArw^=4G+|F$Oivt>ovvCW2M{avdO)1f#=2WTa_pwdp@~>mfJ#8gA)I3+xxt5K}lJ~bYA8#wsq2~TL`^iOa z)MS1Bb!_twQd3HFsQChGJ}}tFn#|=N>~4W=r}_YGU09TPNR2-jab(yU$!{`plhjY6 z&eMcCso37^jw%?JY(SBWda>UOhlVlmcKNfcJP0MCCM)yLHnV)nosGvFVw>4677%Qh zi(LxLUBWvyqHP(t?*5HC|6)rCcAEJ~)qXE_&?6!Ls{Wvx=O1Q8tz8G7jZE;5MeFct zsktCZ@224|HtrixTjE~@UuNyH^8ML%xj$P-vc^H-_+gG%gqcslGe=hVhYdj4KK{rn z{?*vKxAw$_J8Fv1L6C#72;yM_9v3eFzRm~a;1iKr|0=131)MiQVS+z53VxB^w9cCf zFbgg4kKD@D>*sV4%K)=%U|UQzAPWrOw1@T$?glOm+8~Wa6PzBs6|&N{E}+m>WDfg{k_|6F~rsfVo_| z&x7pA{VIfrJxdmm%--Azrma1%)go&h?tN(opVULwBTaAUqBk7(2y5i|^K-TY`zRJ3 zMM@B1Hk+>ptcR{Z>DL}(eHfsuDD}|AOnaJXv=FW`_0Tw>dfY0c=H&bH^-zCAQfTAF zL_+v`=uNe<8fkj?Gliq2HG;nbJTlUg6F~3I05!WHDQ4-Vw~Dg1L;e{M1!eu?zf?!f z4Jj1KvTrd{+AAa*0fTV?g_4-XyFo8%&%q^Nua$`ead2BJ4Ce2=6P9MQr|8s`_@2p>aXXiF*0q5J4>sa2tx!IN9LMXozm&Bs75@PKAhA z0~Jw%@l-Ml-pN{!UIyAg$r6@CA6PQKW67`FlHcqTxsoP1Y-Tcs=Zhi%CDh;mC%0#^ zWytCua47EH7iEG@h)YDfb3pAeJ}d$Lm`0KuN|wkyHFhERLPW4S(F+I`cUf(jNyv8^ zweI#%jjVQ)Kt7}au}?%9Dhiy&UO{_M)G^pD)58Za2v}gF1a*2iM4MB5P?D%~3*+6y z+P?8vO_RMnin|0oBC{BHw1Txh89ZsRThUm=fLcKJDr}Rih2gh4+@Gp}{WpLI4hUoV zYwG|e_hs}@2qh|q4$woj@HNs3CH2_qvz31GzI1mYCG~MPbs$nBv;pXR57rc_u?N5s zrzO2%6c)k%L_5Lz0b>!omT)*RtWH4pF|1&9^l$8xNS;0fqsv_(|Cvp|=um?4W-LVm zA&N#VhFq?9IS%A7_?%m@Go|)H>s>ZahDsy1;tWc-VbJ@N;!HC+VxHbciMQ*I|D z_x(0w{Ao@FmUV6%jxA!A-}2k?k3Eg1D0cRO4n@So-DWcVHH}OViZmg-A3j&Z>Ip36 zmEdlKsvdieOk>Nc1BE9*!6~Ol3Hd`lh;4#D-}Z8q;+>W)t)=Vg+Q5LuNvGXC-;~0F6Y{_&}v}8X&G}r&aB~sb_OZ54Mxrtq|l7jDjLm)9WlzNL5F2s5b_%YYn6Zatg|mU z5zVk;TlP}j>r!FSD%t@puMe8Dz~!a05!^LdA^+okOaP(&m`5N}(uI4iMH>IC)&3fQ zNLijp3Het5M7?@ubdt!qY#C1IYP5{5|H`n4gC9w^Ob6_4p<=8rW) z3SjA8P62voC7v2d$OMHF%t3*dqLnt!v-B2rL=QjNHQ9r<%pgc6YIcZv%fAqbi-QHp zM45ITIU&WxpMk^*@!gbPV(Pj|F)KGz^pJ^hn6ROhcua&0>2On^JJp9v(ogP#WZXms zGVPosbW(-~PgwIoIs}Y$)MS%CK@;w(eww&pNWU6+dZ-dILcs}cK`@ahn4KuNL<*jR zEEJpw#5GA0E3ZlJWdl+i1K`De z$NDL2i-X`m^7#dR^>bbD+*Hj=|{A_H8cgP~cd z;|kJs+pu$PS$1#`L3bej{!?j)l_>VaRGkXTg1x&Y^sZ8RJFg~k$jxJw#iXQUK`L$b zR7>l`LDi?@udN12UV^rG3>`lt@o(vD9LUZDZg>GN3eMdk5QN{p$Xw_^G^I!`%CyJ^fu% zU8n3yuZ_aHw2>r*oQeKLn>&=j^aRl;{r_8)m983e{eyGn=jqE%Z50w7MYDiXPpXPD zCAHc%;@X@`28t=^rFV|DY$bT=e2x!Hz^ZQl1awyj+FqMO&|I4~zoZc*qh39{zbkOJ z6*Xtsxp~+hx;FC@>jQUR2Km)!!}czR)IP1;k50SILi}lo+s(_tQJe5;RVNkqgL5Y4 zEn0^<96FqZVHtVukaUToe`(OGH}+5g21F&A&_mndZ{0!lIzSmo-7r+_$^J1e18ohS zvsib9`Q{CE16A>i7>zH+7vyfo)o-e_2TE^@V1>CRdNc?sGW^ofw7=~6XJg_=a4u$Lde2b2(d z9cz9S-`u6PomH3zWia<;bb;4-i1VE9akoN3DfBv(fg@FxjArUVP$5iBgi+7z0)v=R zAlBH8$)lpI*(Q|iTlBNX0u7n;&2PvVyVdizd zQVI~ehmqR`Ox0VR=vpEcOm^=!@FMr5p|tP8jJj43mw5$%KpXj&cLmB%v)Z0YTVAF``r|<3-mle#Ss|d@U%k>S%%S z5)^Z$;~r022lJIscr#6y{!M!m+qIX}UZvaaP!inJ+(uBjTe6lQ1j`r$!)%j8_oER^ zbmSYTFJYhOvolp7PSb?TNy^t{bqPkDJeHRbN9ux(!!q3+ma!12x<)nwVY9iV)j5q~ zfbU9FRt$3~0eMbGg+xZO(+M6&x8w01cnRk}he3(~O09%93By&(d-gpx{r%1&a(1~- z0wh93--m58L+~Q!PPkTe|5ME=E>VOa?U&~gmU&}4cMAC#J~(vjO7u@SIO;(*$yC4< zTLLz)K7S7CXW2F{SoT4ZK%0AZZf}98Hh9o9qc)c^*AWt~V~{OT?aEkD^OfU4x6Ed* z9(o$OSX62@d-RYzwbq=XhwsNu$8W^C$+e~L!GkjoC1h&h?I^R^r-%LpI%acfjUHb7 z%Ta3QaVZatJp4%Xo3! za1g{D2kXOw&)Rj&&Ut~wE55YevtRVG;-UHH*#+sDO*3okj4DiYaywRwjuc*^BcVA> z%Q0EBVh`@^vCD2;;#~`mtft2Fh{+`h1!~K^ADTh#yWmFd=WGVUa-YL4%&-eG?5vvg zpQl)n=fDqpEe->N?|bw*8YY({hM@8uwIt?&rf6U{If<;1)k`C*#xvaeGlAXU!ffth z1$Hkw-)+d7t$cw<6@9T>hcO9E!C|~ z!fDkeszdIfsv9`$qPNDJpPQnGj_rbukIl1R_Cb7h?wDj}>z)Sx)-b;nt_Ki1BupiC z0D^FoD4F^$Cp-gN03y*g=4!8}aIot0Fxtia(sucsyE|vQiZ5P8S>}C3npytMfq!bT zspus3BAe_8xyfwG+~Eq@?J&g671bY5)mr&DiX`IoIwJOB>6Ap6om)Nhj%xM3u2^ES zsn@}V{%W1*PCJx0*5lK|w;?kU*c&NY8x8Qb0#wYU7%olfvWXXj{|ww^ynmyZkKF>s zAFIT8T8H%yyD-A8*qhU=hwcKaYgZ#fju>WcjrXE_GC#>Vtk-Qco3gOX4e@j)R{HBb zc&zTLOx`>0WjdZ)*JruC<^o_XiQ7fHai2Pp!BvT!Voe-u&Ck*sD%M%QRQ{5W=afLF z@|SC0!>U@H$kp>c+ZCb>UC#%@|+Mmwuig~pY^F5Y5nOoI(ge;W=optEl%$m))z7!`8kVd^Z zY1GnIdp=*8K)c2hm~tbOe!^SrG=i|bvX%yy^dRjk*>5LvlAVL|=-9b&Ai~CfN;dAJ zG?KehBc)~B5}`KQ$+@##pYfKm>k`!&?+M&Ua!1n%V)I5*W|P^-@s#jb3xjq=TKozb z2Ct%i$6*kh07qnd?`2D;C~94~0H(>u7E$uz+;YO=Fao<5_WR%XTeyw<1s^~qRYMQI zE_!Y2E(F}0<})6wC{k@k;12)HCUERD(-;H0@x0&$^k#FIzYy!a)pbh%k>6~WUv8TS zzwba7D9`*zgym7cTB;*;8}Q9x{*sEN?k|yqBG{Ema-ZQ>`#q_#6KO{xYs@09@Roi-87Q-p$;!qy(@`B> z*Ef}6{2djjX}j(@wxkT%n~vE4LF3QY!I?f>0jj4A2 zy}!CO%n^P>Hy(Xi22^4GjRd_2cp_t3t%%ITD9VZYeT2w4GMDR1wgPsm+vEIJBt+T~ zR<%#D1;bGxYx29pq|m*;nic698FINmmC5hs>Y+PmUv*+9lfjQ_?gUf~Z?zPseeTo4 zXR=gBew_(4y1+Kd4LexS9Ul&mS$w>!;>X^AZOZ&d6J#8uhvyywrH&@(F)EVgr^e;N zOEF0zShcV8hqoGMn;V8fU1=r9%bluJ2CTB0%kam(+YEAvK^_y6^4Puyst!zA_4P4D7|=k)W@PvM$_Q>FxvA%- z*~j)RKP>&XqDFT+uott@j?7QOh+$p=&7noR^vLrM8Rt*LOQomcg% zHTFV?8ZHOxDFcr+Qp|lRX7m(dhA=yK_PN(#A<_7D#E4EWn!KFYh`dG}6t;gJdLJ07 z+CPV(Nl~g5UX4u!yOF=(a?K`O@qpFOGjY@M;Ipsc`5dvFz4_1_?jf4_O8566od+7i zyFd)jy5pDas5MqJyv4=QllJQKZ9o62s_=FwCz z6=4lSYsOR*yL%(Y@jmojwriJ+dt&g{UuG(L&bc9>Sb<~UXp;h-du1Yu5e?l$$!4DP<$fLqI z?JFu*xPD&55QU`6x-rLI^w1x3`EerwL za_>^t!>&3jLPH5RO%}Xu>LBN$bn@Ko7y$uWi5%!%; zsP%xSW z=WiHoCl_k2M#}imnVgqXJ9J48rNGH5+pGjL&OxjK)c#TCq4AM$3>JvP8OqS3;l1bx zO3^m}il5`I^VK0QO8FUD9p=-4=qw@;a}RhEn~jiCr?AhxvHl}E3Ge@sDrv1@BW+{3 z7P)qWX*ieCYb`jz7$rl>n}`>+g)z%y-^6BRl4j2ri>3{n{A8836Tx;JDU#!{><&2Y zuY8Pz6vp<5EtYo3itJ>$@IL&aAV%?S7gGBv>|3W^$%tfc*?uqSn;v?i3(85GIdbSt zk34`BJX~I$h>Y0+wu6$&J&V;pWar&wg(}c)C2q@X@ES$Fj?v-4C%Jc{=?`tt|Janh zK@S(TLJ7jUXJ+Q1HrAayWRQPTdV@1N>=f1=ub{p{yn6T{Ad$Kr?t&OwFfxq3bQRnP zOQ*Hok8VUFol7|^g{Q6_Ql-KH7?~}cs55`-_ol%6vB&PhrzV=4%fQ8bT3(h16u@;5 zfE8_@N}b8RzLGO&u-xAp6+63OzL5ye9D>5BP?Dx8Y_yP*$gK22NL&5|qj@rKHNC=%qV zR0;lI9j|c#V<#GN?cKenp}hj)qTO}eG{SSLwM}}%0cQz&$9+5zWm1UorxN>Zu875E zT^D6XXgHL{KhR*Zjv_(7cYBJISc>tHA@ z^HBT*?B9egX4j@*AK5&TYvk$;Dk0QlWa+OqQ|JiKUI8Dmx6lFx{abF@M@(BMX~ue& zj8=nFq#&oQ-Hlv5AUj?`I|9dhRQ{uIIn(Muh#qJ|I+Y_g?HsA>z z_q5iA$hZ_efYVwyr!_O0(nE8KXrsTChzmvNB#h|5xI#7_?o)IBUd-@EWvwp0uV{zwo zC~%d}xde>52ZZE)1?C%be=5)C7Nwclt;VrpDqpCyor%IQ{=^hVl&{qY{cAJO0C zd;mw6uGema;!(r=(qnuLF6|O5;Zgqpj?)KUjg0Ul*k=2e#y_P$XyX^1uSl~8V`7s{@=Gp zrG3mNe`jv+=6q!Iw6F9cHo*CH6E7;^`#!90(b3eDhKd!b*zl4KfQ5UZ zOX2*z!Q0^vFwMJ!|6(P+y#k?CYz8KIxd!w7pjEwdfud9q2GqA*C7cDUZHQt<+)Um#7 z0q3YS)Gs#Q7iMF5-OrY+cpBD2X^ZXKvoZL)(|9&6`hKXZ>e8W3-J4duU7C*!Xp*@ z9^p$Ayo2!h3f@lm3BV(#N|xik4RwtcV|1LoG7*66D(X(P4T+(F)lAkQ>r51cIOI4R?5~gF{ZIeuD3r z3;dqoTXxZM`|@l?=C2`Gq|NE!HGm+Xf0Dny0tSC*i)`6musJ_HQj(tYjvlTS7O%#U zF2&+};HbBn^=?-*o+n(a;8lcgQ1Cwp7buvwHk=U({u|+o75p6G^A!9n;nNlT4B?&% z{wv|G3Vxb!3xWmk@D$;170mJLe6HXp2*0o3{~^3p!H*H<9YNIPyY`&t6}*!06AGsP zaUNFip9x14{3pUy3VxVyP{GRy&sOk5go^+x!F%6Nz;;DF z-YNAF_BfZGtvZ3^?A**roI9l$cnE~rM_lk>B_^goD}>=9IE_(y`&U&`iRKz+dX5Pnj@ zVZwg`48P^k!?JY`!zFe2$!tBuw>|31PJ!*2&6i+@|7=VHYChAKE;&U^qlz@RgQ{jn zMQJo}4gsMQ8FR#J@#yz#>_swNvAZk4F?#5E0nWhFQUdhF1M33(er652BVaDxFeAX# zdT22K^q`zA?nCG(^KC^Xo=!c|Q*t;@Id32brS2vCs)Fw({DOk-BK#y^#VOzU zm#yf{#4J6u)<@uCJ@gjU3zo4KEjHD89gRBvc9o5o=(b)05lHlJiZ1D1C6z#RG&lP)68$$9@T0Pa6Lu6ljPR!l9!mIK1rH(ox`J~EuTyX~ z;lC+3lkj7JF$nNr;zLYTFQM+W8hxH-6~ghnISg@+oprB08XC*cLqAhG ze8TZN0eb17uLS6(hc=3ZLaQ$ca16%tDuH_Fp{E2mQ4c*LKz}_{C%^^R69*6(jnHm5 zV@OPH={ZOA@DUgR=BQ*`h?4>|3&dg0d^9NIQATrU<#A&17zvuQ;gH+CP`~3P6?HV{ zIiy069>Vp2(NsDXyg4m;_me+y|lMzhXx6y*WJSI~qk zMc|J4P@1@7$IpR2BD*G;aH2|BmP|-d2|O67s{9~>uwGkueZo;?nJqBHpAl`FEf|~M z0YL270!R8KKp0=VuSCmf_t{sVEQ-7-Wl?E^04T{r)eJ5Ul<$N*u!V(XlRJp zJT7=KTT5FE!V9v->Y;ZrfJpgd6tf$-dccV~kjIHSyOGO?=X(Gu zsKMRjGJp7x6+}GKg8~gyq7XKY+_gb8P)*z_T<*~SM#lcbJdAl`1z@xJLOn#0h=;gO zfT3#l-fX_wC^wt0G46zg;^y4)#H{lV@gS3+e3@9U6#!G_U@=nA%Mkz`N2P!s&me=8 z9!ev?7mzUYLdD3{-AI`Y3|nkpo)sCp-aOnzzjq?tPEoa0zH*ygQG;z7tc&b3?NNS& zbPhvwk!K+vvV<#$pD5e26jgNLK>I38IKh5NO#w4Lt7>5k6Wk5eqZl;v7oa)OMLTdb zEc`tl+E(%0SAdZDI5M4&m!H2~?=g1ceax0r<6N^P zt;X3TfJRbJem*b>&o$luZE&=uEQ()6(1B z(dTA8()uI%b9FYVVrCBiKrDZAR)WWif;-?oFCqCoXe|DgV(|?W!y=SRLRgX&SRw0Q z;S(Cf(s6;ibX*ZUMQ^y%WBoYfBv~&WYnE^1gP4ziL%rc*Sq}aXJQ+beTmlz@!9qBSO@d{R_Wlb9eaDi%~)* z&d-qqE_nLxQut35E{1-=?gyUcK$BKsJz6cl&n{fy{7qitfFXX}(M)oXvW5O_N~KAf zEnW3+6Ni7oCLcv#V3Y6CCMS`itTDy`g1+mK=>*(%^rg?E!X~)6@0D4^>}-mEQ#bK7-nls&N&733#Zr1g^&AQF-kgg_=1__lFUJ0&9d@wRI!tNWH~A^hW&hyXwWwApwDNmVD4nun z09Gdc2Z3twn7CfM-mF;RvED{hb=LZ04q%hRW9_L9-vYl^9k~I&dP9VWUK^e%m|V=g z9>$N=;R2QBmM8*tc7(rbeeJ1^a66%m#EmQv<}ZWJYI$p#v}{(?rc_79vT$v9lH{*Q z0XUf{4G~^?M3`P38K;uiF=K5~-ge`bV z=^=L)$||fiH>6u?v#YRHZ@4zyYMce~Ifr4a!PC`d7Ls9<3CFZ09jMdY`iI^_!!RgktDII6yYs?K)6bjKfXWsTT+ME3behBL0H zcd8IoVL(}j!Mn|NCSr=NmPN7IJX#MGVN}51qNiAHh`+so;sdYrknvXR!0jAzkIWh3np$S90?SU+0d1*o#|8YTyiscfXdYQ`C01#=T8 z#8KJtG+>w{pr>qW4{j`tM{Ix@6UN85K-udX&LnKLKx!y^A2OV!T)NYxB5+KHMRC4q zxdKl%wVZ&Mq2)?UTj(B2W3kJ2#^d1zD&9GfbY#xY&Bip;CXT~U2b#T<)UU;|M)Ypn zI{alM7}pJd2p2s(6hSV8JdhPXat1KuZ9dkx*=$aO$*H^xR9;VFqp~3t&Dn#agmhQ3 zp$W5k8@Tg(^Z=v=tKwgvMPP=)b}H;bg>6>Y0EMju23L*sTD%wEFexD|BBYDetT+&z z73azb3G?D^L`g;u22@=UPz86m@zQ7&FTEMYB5;KsdL3&F=Tfi*9!gX@V^ynz6gCnV zXXX=8#uT4{K<;DMlP%*3!31ZOdOPn39>1)%Gfb=`({eIQW2nq7Bh!b-%o4m%?JSBr z@Yy%|!=EfX1(DiaxhZ&Y@Fg{7`36ih1IgsBa^5G2dWK@lJ~ACGA?T>*rXiJ=G~zr; zakLn=9pubOCPrt+H*;*y#3@3!ELjg`JY?RLg`n+G=pXaf(1QB%eVnhB0f(8un3Xv? zd|{|s!9>PjM$JthB0-uSzEUE#5g5~)sPym<;LtGkS2X9>n81?=A5d@};e86OBg}KL z?wp}7eN$!7%{Z?qcs$`36V_?NFiF#iA}bIj-xO56aQ)5Ql*vl&@TLNp<+J&pUn zn3VN~t)6#OtXaOvZ1qC7dgz?vWRZw%|Jo+A>2}%w0BO~s%h{nrI0MrYhYpQ1lp6KW z1xV(yKeEuH7FA~RnBb?@kvP15y$7YZ4h?@vxnTnlhC$jDwb&8pP3MW&Ko4DtzzDZU z01XfbGjk|Ka^xvSxEwtOUau}(_+jE%Av+J47sjZy>|ZL zHacT8+}C76ne3}4XICx^sz{L*UR}Q4E~<1+W3vdJxF!w%8@%$~h`VZB_J%~j)r`#b z5RU^Uw+Y&J5>t|09^PhOJKT`U*=RU+ZdRn^9}wD| z_lsVeFAO%BSh2+4;jfbcf3LbZ6t}_-V5DTg-!sKImO%4`GGDf$So{-vieJ`+&P2@j zNGCz=J36;YoqA9FiBOXs+Dqp!D*F`h>zo2^DjONTX~DZH8yTW$!P^t_=B&!bB1xH2 z*~l0Ta|OSVRM|L1AzT)zdGi3E%0`A`TJSjK51{xrkcxQ^OVQkHY|(4Ud2hPf3EDsw z9^40ZuyF`jf*KXeSR0(nP>6BDeORPgzc~BQDX?9^Iya+^8a)#MIfI~Y+{xkI-5B6% z?`|Cn=CC04^*9W}Llrx42msno!#lAd!fAL^_U~B9i%Jkw_(Mq5*~}SqRJQno_?L-t z7qQ%Kpp}h`q%r-y!hO8lY(x^S%k9p|1)g2Pt2Ib=!>i9|kE61$A%#DI0^pH^mn!%= z!e6LB2V_ezN1((Arp{3%#uA>Y;OhymX6Yj-#`h>VkqLicLJK0R{-|v_g&c+V@*Q9t zg_zW8t>AJzGIWOCa6`g>VRcvE&V{`#bJTJ@V$ludH`4`THv=n1b3NvdN$$Ezg59DO zMg!JZ5+!1d^*U}2yZd;?Gx2{`wNMdyr~rO-(alvY4+{1M{L!MEsus>6*!}xL#BdvV zzZbCCf~|Bup||LIv*jY3zb;h;ME5fRS2=%2SvsP7uUA=fSobPsweZfO9vZ{y=T)_+ zKsQsBVkF}91#CLGQu`FZ&I|CxRnF!F#jZ$;Kd1D@@$TxaAFKibzJesa_*~!@34Z|C zY(85LzoilvGI2fN;mZK&OAdiL>P=_zGe~B5sYX>F=WjGTIzw#ninqWP?NuK1%$>{e zg5oI9;SZ2fXe)k)KTI(E7>LIIYBpnYP-pq(aoD6lj@di}_iw@@6)TlUaV{Y%)vOiT zhTq|qfHX+6`BFW6y<+cX;LfqEmYa>)%C+H9;4iVJ!DjG12>(>5Blo2FR9urMu)>*( zbS`Iu)mY0qA7f4e6*z2{%6~9&t6Clhu7|Ra>GWWUQQ5_8^P#<{i^WW0p9Z0hGxj^l zpT_(Y13|-~)vfbi*!MrSrLNPRq zr$Deu*h6T&Cw@OB8hP+XHXyB)Q_(3Bp+3ta zqi`TWb#796&dOOR5lqt?Cc66}Mmiqr8d_Le{+2DZH?=}WSOmqZthm?&1HM@0K>^) z#bFFzbQYtG@MM)8WcFAE&m){CMY|X%g$HRbQ91LObH0M_Abf^`?<9PJg1Ja=x&cP` zSW(YtEs|H6%W&<7PtjW}xAwRTuK$4r+hPNF!mBA~d&LZHg3U$njQhv-1ndK4+|S)7 zg!v7)vC`e6}sxJNJp^HMq(z%oIWc3AnupEM6&7W@twIx^y4 z@h}&yM-QLP_D3KEXoMcIET@paXRBc<7E7%pxM@x z;E7P~Y3%p$=pr_ejql@Mxce)R0ymp4mo3Sx`_u+X=neXtmO?$WhQnc5ilDgL^d5+) z_(xyH<)z&FpfBcb?I?fwir*LE9cX&3#t(=U*<-UIcu0^TzHgdvbSAqb7y*O*$zYv=&n0XrxF6vPz-G&h zde{KqOjBLz%be-VQOoyr6to^FSbkE z-=s+PA<4BYDyG&C#L>`sOg#;7A|m;i0$8>dzf~<^{2H6o%@jRcuIQZ38YR-?Wq6?? zFtcT(QEIjf)5ClW$DHp^F?xfvp9L>hm3)NxsWE#DRDWOO8U#Q?pKTZh*GOW6K0uN;QH0}A5Q7YnN zsa%$dtYB6nf7hLi<_bDN^3wCSvp`;b2VtMp#&N5j7 zgbx7ML%-_+P6SRKxdcf*3Y`&wrW#onPm(wDg1?1>?3GNyt2OwVZad>1z2QpVq4i!Y zV7I2iomOAzlii!(KC6+HqTwdDYBanv3Go|vBo__8;zFjBM%eRLESqepWY@8R*_6ts z=Xvtw>?MkGCgWXqn9lJj55QfdxGBG0d>RHxb{(5du+OG=6bMOCFL(X0dDR!=7>^)F z!@nYxN%l%YcG{zdM)*>V+w9x@)*I0gbQn1PVy*E)ybG_lA{wrEjJA)upO5EN>zFb$ z%>0T=Ey3)`8?dH*^w4M!+HIHiheXT~{XN!-7nZQ08$j7w$qK4k`Bqc7@YtfNoHb-b z4|6SQi{vvu!2`vStT0v>_==z6#a=@N#*f9_+#FHK!s~4BpAO}s$^CYO#BulxC3tYy z_C7+5esLcK!$d3ry^rXj{av|36vts6?;}xfHQM9eSk5~Y-zTXqdIkp}7&yhUU=OGl zwZhb%dU_>_3~15o=X4x90VS-Bw#YgBiPc*VeTdN3UL$reip*nymdZ=S%TLORnSH91 ziYRa(xUpAKNp^%)>@GH5;uG~Q7QV10xW3{?SECEhgYiKs5!#}4z)n9j68-LB-qjx) zAT^*2{_!w+&PR*(p`hHimFO@Urn2b4@%ZpisjWx)fk}Q{>P#{_;htad{5C?lV{_ib zemj=G2=?qHbsT=U5B?3-83jnhNeFpGI4H?!Be}Zgl>hl>+$yF(sy1<)%hMHh9kqqP zFT9-`H*#meY9uqZW=-MgStQt@RkRPx;@0KR*7VjX)L}{HD}jRZ+w*dTCY{N4euIoSC@UBqp4Zt#d)ab6|q8WF*w+u+ld@niQ{i%4D^Snznq2c72hsluln0nqnvSabjr0nVtr zYc|r8Vg11;lkGYR9H;j1+iIUcey9yg+^%t%Is6+!uPeEYbnk7 z@hIn7ls_-^;sl2)Kb&rP4qnn0TT+H%UFB7_GWZ^`m0iID%3hoY(F*q$ac)PscswpE zHYA^l#<7@3aK4S~$y{eg=p~e@G3lkrX z9&5=&uV!$2%}!~TW-Sve-%4fRbTB5`e?ORLD+jgAFMqDS!AHlDQAIzC^AxD#Ou>)x zr?kL!^JwH1{*<09s)FXir5RGb8f!CF1;_i1?Q z#=DBEor&4D91_5Npi{9QT#8B9oAVyFo#8iXRnCs!cXDh7!}W$#Y#Ly*AGNbtTO7DO zKO36~_{46`hr!hNB{OSsK9rkHs4xB<^)VuHZuc&{icd@22}5<&=9~){--3O@`mn0C z5iA<1RjnIv4W57lUybx`9{rIGo;pHyD7=XYBhu^}QsaM9*_yE$r;Sz}l1_SpPnjdq zJn_fWo<)<#nIjuR>5+rPn#xZEPPmSH@ zVXUWZnFK7Eha*x>`OaX5^A@V&^p)#NLtMAoH}Fi>H$3h3Hx4!&F-r+PY*y@oljJR7 zvtlox$%I5y7=-C`7J?wB3ylQq7NnX=Ji<7}oQKC)ZJA+@$4w8Y8m5|RSM~9sLh|hG zjiChLF8b0#R6s$8GMG*i?Ns~f~>3H|1PI?#2Eiwgi5OKBTKbeotbSX<5S6;MCK}>Rl$#n6R1B1 zPf)CUSLMDxG8S;&?hdgx4aFi-?SQ_ho#tzBqmh`iH>Lwc%e7OO7Ws zycx;+yfYi-tEK=)r!TD&zpYjqE9tlCdwJ>_Z@ZC!`j7h)Ui2V0uj?53*(sG%eLqJf zykL(U-VBi~1?O3M00~KLA!o0lYo-xM*ZH<@41c~q=ei`}KasFoN8GtA3SjT;s>qvi zVrM7o%w!!AB&3Rv8~uZ9K2wIDvAm0D!A`s1Qd4A;RY#$#Xv)3r&@%r5`jd6DB#s>;l9zn!-s*FrOLlF5|$9XniOD9OMsK3X@PZ=?-h zDhYd;aGoTb%Y-6m;6;vKWmYui=U-=3sE4~D->le;AmdjAJ!Zu|KtBRXd?_z@sKPxui9O-%6d43W3*NwOFP3ScHcQ(#E z**$YUV4)8Zg@PyBq%{U%D&?Pnv#LSmdC7G{j zCBW4tFR`82H;EMP#zNvl`REVcHUyu7-0sZnRiVt|7a(@UZahU zk5L35Cs<)7YXOiU^XfkV+tMC+0gC%y?XBmdm#u`ZF>TOtuN6 z0&KQn%cLceWh3Wcn&tq19A1VsM}h)1;M|3NbJa8!(g{w2-py1`qTl5Z3JaqyWw8qT zO0l(Wg{T|dBJ4?Vra-8wSL!sQD?fHPb~gry!TdTlL2{fD82i12EQU?&+Y-Ea!ewD8cpwvVE*ly1c^!H%Q!O$Thn-v#l9Dd=@x~1oqJ@la(fPi7T43 zQHGA3CU4~8X3ffcP{C8M_@bq8exz_8oHcZwtdo$?H{|1&Gpx``)?A1+1?$=Jbq==u zlF&8Iq859IM}1mS+_z;)suMYaDiA!j^*^;u&KziTlA1V3R(T_VK(c!|SZ$r3O5r{W zVrC=ds?9&%7J$jMl1w>h^KB&%GE;$Mhx_uj^wS;SoE?!_gPz2nN~#A`1|o%%nV1G@ z!3dpe$*LqI?1FW@iIQy6Ex8OOlVw0{?*f-vyE3<;OuO^n>iga3KE5%EIDvlT8L+F~ zxP$l2us_tPypW3v2+|ji)5jlcLP>~i>}K4oLLobl4!u%xu?BF5oVQ8uRC3z56O`oD zRhAH?CsU2cw1*CT31;IfWu1yS52f*V+l6iskfg;lu|LVTZV6xVl4Nh}+++?uxQ*V( zu1xa#r}@|=iUxMEf_=Po0EAD<+`;wkm^22pO=;L7IA|5_vQNsz2e3FcaK@I~K*4>l zffzQ?@RBvw<~Kmxcl&Yu4Lw)|n_?e940ge$$XMJq#2a#FVEowL;}fDd7OYWd!%?4| zn1po)MAKz6N)_&7W5(sy2l5^VfWs(07Mmdw#48K%-UD-U8b2w#*S?tiMXvYS-pf$d zZ?#w-I$XxM=OL5%Cf*4rWpIWgxDPL$vkP`b%2Pe-zfPg8jsZ&;d3YZrzdHOJq6EDD zL}`{xLK%ndzzd5-7~;uJg)l@Ct`uEyKt7_c1kXMQqJ39OKqb*tei4^)UCtKXj;B$6 z#H)$BeJ&BcI`nzBa3NimC-!%;c*QR3!}xs?W5V7Fn_^!FY3NN*0=l(P54A#TbREcp zg2ZIqj-3vcSW+a3@>bz)=NcIj;Kwf9r8K$PDcFSNpTYP!d^) zQ$(p)>2%ew*I2bd>y6*_8t&m(fn8SN7OP-e90wH;!#)I`Yc}WVp?|{eE6Ouri6VYwFqm*sl@ZZ|pUY2N6IQ(s! zdb>8aB-*^uZIgR}YCjY+k9@Rp73i&F=c0(ga6x}I7$;MVfu`7dqy-D9W)8juJ+uJ6 z;AZFqZ@1f`hxoPv*rIE+v42a;o2)HjeQ4lmeq7}mGyo%{tp)bQs!&Feg9xlQFp@F| zfCy}9h9VHjNQKi`nB&srVEDLH7*WFZK}-}cGTI3|)=L^{g7w2QaFjqx*olhWq&i`3 zhk|I0FCf~7QYv#G@h1VJT zaY>@;`QTSETSm`W(7`gW+KSC)hwvIjP=ir%?gSUBNfDNjP7W0xGT6n~KD=GHN&16R zNf?DXx&y=`q|}4t3pSDag5A#5&=b_Lmg;B%8R}RMef}%@uDTLe;%(OzUCx&zx-&_& zt$OqrY?VgcLHb-m#%fs%okFX4`zQCG(CTKkdZpWHaH;bLG|Sh9;s()9+$jMu>`am8 zC#AOgdGw-<$Od<@ZCIHzw;H+8z;+hLOMgxIhC1FxgQ4<_TViHgiTFR=+Sj&A`%7}d zbEiA!pDko%Cr$6Xwt_PIxMiK8n6%q3hdBQhZJarCpnhhEytwZNid_S+YUBGE@LT)) z#3s#yy}Zl~x`@f&^P=lpQ*bg58)6J7pd`d;;zI-USgo}$vJ^M+j+E|kKPh*ULZd(Auy#NfbyW!Mzb%T0=TB6c<#t) zHy6+Gl@^!GD>aH|_>5V_zM1pp%$axR?76r2<^_$CpfRKe_P~1d_`jAeE-jr`%ECi> z;OVvjcW46!+)-RwHhbP&ZNTq=%qpH!f=jS$+AYP}03_b34PZit4h#olx6PX~Wyb7M zVb&fTc98_;)#ukW0hJ+%D1Dfw57 zymrL3Hz;lu<9Q<{jG*=uh5|&i2~Y;)P(0n3SGoYCeKY12m-)zY@%-6khHqY}Z??jx zO`l#|RyOXSW)y?#x#&%T)An{aW12B-kSl)Q^kf%qrcnmx&KTIE6O$dJ=bMI( zwwK^h*)AjB9l8EL?Ol76RK=MupkdnqWEfmy)aca}9gt~yW*A-y(=*LHfT4Sw84yr% z?diu%OTX;y;bpQR@t}r-LOkY0+=QbVl%M*wWs;%i4`p8nr{=6GlD< z`yks`F@Zv3EYRTbe6cuG;Amk3fWHB2n4_8d}h`Pqo%=(7F{7g!V<|m-D zGOGI1Ian4A{5k5)#!)q8mmjsB-pwPb(DNJ0>DsGVEMYQCI6gg2`M57wnd@%`AEBj(X3CQ+xWwqGI7s^ON z!(B6kK~q9^FfjIpWT&mGlS5MoO=c)p8T)nySIsQ(#r_E1bPjzyjaFBBhO^Z3?91nk zY%I5RSUctfG3UIA{{6sFpceo0Mj!AMKu+?(d5J(an@Wk+wvOcyqopspO0*b#M$}kk z7>L&y-A0?yEBe-TVG_VU6+nPwzn3(XdE-zytp8_)7o`{XQRLWSj zs=a;DJeK+b1erclRy&^&#wl|aUs5~kiVfF(_5AbhxM1dmbFci$?2DH)U-acmmkRt( z){$tW1y8Uw4Z&bgtZC%maCCK~uXorM3FN_tPFO@lRCIGL#y>%u))mtI`5cGZXNI4> z=<`qcwa@=Bu1#g!N5JoZSAc&99tCnh z5V!z119;`XkOtfe60cL6^Db_3S| zn}Lmh3G@OTz%(HLvd`ZN%m-!z=KwXpB;fcmwmoNaG#X_Wi#$w)O15#r41qz^%ZKfjfbFfO~-hz&`=^ z0ep-5FmMp~SKtxgQQ$G)ao`DnPW3~89a!cGfDeFEL`(wcZe~R}+37|GlBEAywEr@SKjPoeCyPSEsui(`QaAxdmicQo$2B0CSLEmVZ!+CM_Zm~c~WxI_JhE9=wzFn3@0sc z(gG(faMA)NEpXBT|Nkrizny%WXvZue*MxkIb6r)&+Y+v=cAY)Zf9x-x=68MFhxZ>? zTU?JdEaF3;xei;3g%sCfl;gUKfoqW?@b+;(1#_W%z9PXUL4w}In8 z)y$$=C@mYdYpVoL3U2o;kF0F#G1jhIFCAW`s?F*0 zpb;A?!?#mT$Bh^qiUZ2!mmmK99z&{8-daeO<{120t*)i#O?f)ud|acPzJIkpsR}L+ zcs`ZC%`P+@Z_*`~Dxw5#ryK-^TL5klEm-x0Gj?;nm~iHz+6>a45op77E}j`mz~5&% z`v&M>hHEon(8VjQ$0FDbKQgzDiU1vM^ivK?!cPSi(zTfT?4nq{c{)%x52oXTRzEWe zd9){8Z!AP^q;~60OM-6TLNPx8=U6GdDQV<}vi-=6e%wq(RwEb7qGdn37n{J#mjOq-^t;207+3EP;PvN} zhG9k*FiVDaQeL(gQ=TQO$VcJI$E=o3!n3^9%JcGAF$a#ttS(+bCF4V=`pkq;JF_&) z5N-19*63-dKcDYmyt8qJXP#zwzxmzEes<1|cP50+-SG~triHq1`fje~x}(ipPlT6* zog!lI_Zg0P%5W_bUc%R$m7ruk@RZ@>EZo~&5x|uy+fh#$u8lg~MYH6eZC3hz->mYe z4t@CCs{ZP=A2o9w71eBzlHs?_jWV1v!Upt#MKv3!eBbnx;Q}Bss-6#y-c{Nf#jB~>p@9#J-~Ps3dmeqJe= z@gWtT7E<-93dt_C<5)A-vaOm8l#M5cC@;hr7VjycwTm7&wCSN^&GaiSmudQ+(?iPU zXNC5&hlTyIWghVWOu@jg)DS zrLq60dI(P$uFZQ#HA0PLgX$6W;d(#Z)9A9c(&Ih0`P-pW8w|<@k9v-u(?`)W*4XB6 zkWOtdjHQRWoI0anm$k=|`66Vxqx0~$5>(kfX;yb?M%e94B)DfCd)}E6@{&0V37y(N z9jk2g`F2K5tUh0&Of>A8;l|R#)BWMLd9Hq-Y~Vd#pDXor;%_C4rN;nkv?08zB4J;> zszcf{WF+uwl;Zzo3yfAlQd1>i;>btxLFY%?JfZgr~} z%hnfyJ<`2ynNv#XdB=0UzDUWqW?y^Ec|A*NDWgBQrBZLZ%|d!O+TgOBt;cJ9Fq?X` z=eAVy8P6XYIno<-Pl{5=yKHli-rLtX%B<);$JbGhrba!hyr01V%4lmBHIE+kY@fN5 zX<6TgNDJQe34#+gaPg~t@R6sUdT>WwJ=7L|-?LxS$eo(dSpB+@dX~5qsebUXrykC_ zotn^CdYT}!B*Km1dhoW}e6U6?c0ahsYM%v^2}fKNQuX=0r>z%KreoO&$t;3QIO5iW zTL|9I;KfLdbf1`>OCS?%U+tRXl?}YtqkN|rp2-nW2} zxMmRhdFnlW_;3z4wN#YgO6l=_W`(4Td-5Gih8iN89^69k?r+o(X>#k4BH}I2zQYH0 zq#?Ri@u|OR-g(bch7W{DqgzI`kN4WYjWXSCnX&Y6-5>3APrYi*|BmO_?to0E`>`#? z8rxh=^lJS8w-Bm5*S(zFdsk@_wv_Rn<5wVkNv}3xk0G-X>2=-iIYf$}Y$_hPPUOQk z(&VntSo1Zx!(G~fR4Vvmv)6B?2xT}iI>K1}nop64CZoi?`!(0R;pMAb^P4T>^{l^) zdNi4_<^xVrk){*tqnAUbz3pOGCskwNRcjr8MhGkj)z)v%kDEEzAj3!3a8PCYwWrKF zaJJVCv&#$BE+2dT4(6VWaF-?|C2Kr?2Va5o)}GO1?0Ss+9egEZx|X|}#~yQ1oe&`v zS6jack2Y_>jYx}IM%mzH%Levunu-j+`nA$6qO!%Ds4h+mskkCz;>P-jyM5Ge{#QJ0 zeHCQFEp8cmepC5Z^w@0>qs;1yx$GGZD(wYNJ^kQ>jH~asZD8BH(s$yJS=Hf|aqAgr zw*+KXM^+A(>*R$H|C{$$9pUg|qqZZmdX_OSFfTBl+j<*B*nm}o57Q4R@my0SMw6EZOPZkRzfVTz7KuhcLx^is5q6Tt9 zf&QU%CQ+A8hyW@0$p=Lsu{DSMR9r5q6w9~oB21(evHc(X>Y|V-i$EGXjYXiG+=9Q@ z@}5Rgp4-VqAUSBJig>YM4kjRJQzF1^^(ClN(baJzBFk7d9f!I+vcaE#%v(~YVxw|C&8RLNQz&yNTo(E!vLz-y}{9R37DET_U*NFrJ*LLt(7aXuQ-yrw~!FPep zgxKa=hU<^R_GVn$dU>DN2=M(lulbJLcZ%{AIrSn*Yy*%aW%!Q0 z4}<~Q&X09~3UM7^^YNX0*&@g{1+e*c;hJTo9_|S!qw@T{Xb18!+yK~o`@y##eDWB- zOjY@j_ZA1=LA;wjI8BJBsl*Dj9mDqkJ3sD0*maxQt0)uk$JYBpi;s3W2)={ho5TjP zf?>sXB4&Q%JA?z5hwd7^A3WgTdl!7~g6}K1X9rvFL%6~G*v35B#Eq`Bs~v2<$8p2v z^MlXt!S}R-uNr*S;Is2!p7!;B;F@hgz1-t)8u$)b_Ye)Y+hJVWdS39e)@)Ue4%JH8aXd0nk}lvzoI!~q0AVUik6Yw zGoxtMLhYqdw0f_~@u_H4A&x^u!w?7@Z;GBOX^tyJpDJmNA4OwOB5<52nwArS<3Z7E z6$JDbMiWF0Bfjfu3d2AA$};{v3;*iXoRo{#+X{zb}El&rzOw(A$xIp_RS@^kbmUwdeuR zcY?O_+YVYFe_Q`Ql7{|EEdD1!ZwE~uGxhz3=~iHSe86i3DpKb}(r z#uKJn@J|y{K=aGQS6cMxp!p4An>Il6lfyPWoAOqm{(7bl_qQ*I4yIeQK>uH>(HlYA zd*sGp=U$oV)_5~ezps(D0!Q~AjXnT+&y0zFK0j*EU+>q_f1%N@YV^CHx8LfM_NDws zTKYtE+-JXw^FL^Jrk@V_{j(;@J&2?)1Rea5&);n6uV+WM0?XS<`M>!5w@g#=YeC!d zALUKZd-meI588$FHK4a+Jnw`o>AweU2U&jyZ6M)C8vRqyoIiPcI@^%7(dgGe z2Y>1FA3z%Q{Z32&M58C;kzvo*H8}AyT|Gh|O?j+kG`dNnJ3#M&JsTzeR1pEa{f9pP z-4?x;{7C<%MW;ZAeu1-4mVL@hw*vKj3-rF*aK_4NpW8qe&>r8m`0oN8eA?$v-&95-FAaC-&4f-*MJwMU-C#y%MED_6dmPQ9if5(TU=qEM$h(><^y8Bo7odDg;e**Lz zLI2?VK>CZI_dVzH-(=CXpzXl&E(Xo#BfHIXJfFI>^bH!F2fgQcpZ@{yGykt?>3cvQ zxdnSjt@ix~E&V=?egyQhDF2`EU}b*KgRcG->~G1x4f^2Eef~L?zHxr6FRbU?^zqZg zsi5t^`kbfHjT*fS^b~7;n?8PqxQyw@kI!l9k7@ix&>_n{=fOTZwDj+2^xYc$kVZeJ z(QjzvtyU@2lULEN`8b-mKA=fj%4MX9m@n?>A`adC>cw^!dMM+51}1 zM=+kw2S4-Q1v-TBaL}UnYV!AL^uK^!=IBq)Xz8zmejn|*A4^H*|ACe+H-|$PgRC!> zs0EbAkn~hBM=e>IEYp{Q?sn9V|FNYVDBr8mag81VZMQG`|4uD^FX%n!pCRPM^8OgK z)1C)3{)3?RecR{fZxZr94f-ISS1(!gD;ocQgYL$7vB&2pTKXwil2=cg=-+|?%KZ4> zn69__f6e%*BFOaHeg1o~^ksSzXnQ?hLytqeST2=^@CA>!FwJmZv{eS3 z&NfRm?L9bEY}ypn*w+;;(nln2-2BBNrGk!znQ=yE=9^6a;1pFA%Gn(FK8 zMY*`uFY5BN#bG{%GuNL5cR7D~$7YcRa+_X=&4`K!?#>~X9<5^3v_`8QQkDbwbzawA%} zSW3#Tm!QL>OpsZjzPo^~5)LeE3M^Q(pov%Wc{QI`3wX7FR||QCj|i628L*ojUD4K- zF0@j6Z(E<)*V2k6L6@fA6CI)$+lsbIxNl&ae6cLdW$SQ$etB5XjcKZTDQq||g+d+p z)JWKD2m~RAgZsI>iB2vz4Lj3P*fCxZI1PZ0hLpGB10l}DQh__IQ_fr5wdmt1}@5 zURNVwyRv5egfgwPQ!bxKVyZ~yOj&JyK_`x)n)Y`XZ0pJ%fLSW-YNT`t21FMv4~jUx z1d}t7bGg!kS?vU}G+mV$DrJ(%g0sw=jB%hrwFtNO3v+oCX2srZ%zgO4PAcK<9`$l6 zb!LGo76#lM1%@#D@pT%WFhDgN9T_bdi*uK|Gl%R(v|H9yE)!)amh&6k<(GYkQyj~L z+Bq}9*@gxujWQH7>1^832^&zBI8Vcv{Tw(I?naR+C6gN~H-h*OP%^&J?yr;)R;NY( zRwr&u45R_E3C(~`SHf3+*nv{wn zy%FRnMpr>S21Isa!fn3>JY+KXV2m?M=lGK+f~+CN;btb1rFb!2z@u2m6DP5(sm7d% zvj~YyvdC5}b23>|7td;#g<>+5-h#2mdD6)w zo7{EBH1U4Qn*FjPW+>|n80SBMS7gW-$AHi%G`xuQm+~3ZT#bG{8XR3YD2iFFrgAhp zOT$5!NN7`&>>oS}qb6q5GB=7PrF|Vk3SHGXgUC4@BQL($HNDD5o&E^JDG^r(EtTT2 zoa@1ahn)J5m9y}LbirjvH4)0OXeQ;Gzfx{wyKzQ>S>%LNnRD6q<(%*CF6!AMAJVYC la|Q{zS_7?$agm;n