# YASARA BioTools # Visit www.yasara.org for more... # Copyright by Elmar Krieger # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os,string,pickle,disk # ====================================================================== # P D B _ F I N D E R C L A S S # ====================================================================== class interface: """ This class provides an interface to the PDBFINDER/CHECKDB data bases. Create a class instance giving a path+filename,"r"/"w"/"a", a type and an error handling function in parentheses. Current types are 0=PDBFINDER, 1=PDBFINDER2, 2=CHECKDB. - Instance.error contains an error description if something went wrong - Instance.file contains the PDBFINDER file object - Instance.read() reads the next record. - Instance.fields then contains the number of fields within this record - Instance.field[] contains the field names of the record just read (a list, the leading spaces are preserved, the trailing spaces are truncated) - Instance.value[] contains the corresponding field values (a list of same length) - Instance.id contains the four letter PDB ID - Instance.chain[] contains a list of chain names found - Instance.chaintype[] contains a list of corresponding chain types: PEPTIDE,DNA,UNKNOWN - Instance.sequence contains a list of sequence strings, one for every chain. - Instance.secstr contains a list of secondary structure strings, one for every chain. - Instance.amacs{} is a dictionary containing the number of amino acids per structure. - Instance.date{} is a dictionary containing the release date per structure. - Instance.backboneok{} is a dictionary containing lists of tuples (Chain,OKflag) - Instance.recordpos{} is a dictionary used by Instance.seek() to find a certain record - Instance.resolution{} is a dictionary containing the resolution of the structures (4.0A if not specified) - Instance.rfactor{} is a dictionary containing the R-factor of the structures (0.3 if not specified) - Instance.reldate{} is a dictionary containing the release data of the structures - Instance.newfield is a list of fieldnames that are new in PDBFINDER II. """ # OPEN PDBFINDER # ============== # - path IS THE PDBFINDER LOCATION. # - access IS EITHER "r" OR "w". # - type IS 0 FOR PDBFINDER, 1 FOR PDBFINDER II AND 2 FOR CHECKDB FILES. # - errorfunc IS THE NAME OF AN ERROR HANDLING FUNCTION. def __init__(self,path=None,access=None,type=None,errorfunc=None): self.error=None self.errorfunc=errorfunc self.eof=0 self.type=type self.recordpos={} self.date={} self.resolution={} self.rfactor={} self.amacs={} self.backboneok={} self.supplement={} self.newfield=[" DSSP", " Nalign", " Nindel", " Entropy", " Cons-Weight", " Cryst-Cont", " Access", " Quality", " Present", " B-Factors", " Bonds", " Angles", " Torsions", " Phi/psi", " Planarity", " Chirality", " Backbone", " Peptide-Pl", " Rotamer", " Chi-1/chi-2", " Bumps", " Packing-1", " Packing-2", " In/out", " H-Bonds", " Flips"] # WHATIF CHECKS THAT ARE ADDED TO PDBFINDER II self.checklist=["", "", "", "", "", "SCOLST", "ACCLST", "", "MISCHK", "BVALST", "BNDCHK", "ANGCHK", "CHICHK", "RAMCHK", "PLNCHK", "HNDCHK", "BBCCHK", "FLPCHK", "ROTCHK", "C12CHK", "BMPCHK", "QUACHK", "NQACHK", "INOCHK", "BH2CHK", "HNQCHK"] # EMPTY OBJECT? if (path==None): return self.path=path self.indexfilename=disk.rmext(path)+".ind" # OPEN FILE try: self.file=open(path,access) except: self.eof=1 self.file=None self.raiseerror("__init__: PDBFINDER file %s not found" % path) return # IF FILE IS READ, SKIP INITIAL COMMENTS if (access[0]=="r"): while (1): # CURRENT POSITION IN FILE IS STORED BEFORE SNOOPING AHEAD pos=self.file.tell() line=self.file.readline() if (line[0:2]!='//'): break # BACK TO LAST VALID POSITION self.file.seek(pos) # IF FILE IS WRITTEN FOR THE FIRST TIME, ADD INITIAL COMMENTS elif ((access[0]=="w" or access[0]=="a") and self.file.tell()==0): if (type<2): if (type==1): self.file.write("// ##\\ ##\\ ##\\ ### O #\\ # ##\\ ### ##\\ ### ###\n") self.file.write("// # # # # # # # # #\\\\ # # # # # # # #\n") self.file.write("// ##/ # # ##< ## # # \\\\# # # ## ##< # #\n") self.file.write("// # # # # # # # # \\# # # # # # # #\n") self.file.write("// # ##/ ##/ # # # # ##/ ### # # ### ###\n//\n") self.file.write("// On my watch it's %s.\n//\n" % time.ctime(time.time())) self.file.write("// When using this database, please cite:\n") self.file.write("// The PDBFINDER database: a summary of PDB, DSSP and HSSP information with added value.\n") self.file.write("// Hooft RW, Sander C, Scharf M, Vriend G., Comput Appl Biosci. (1996) 12:525-529.\n") #self.file.write("// PDBFinderII - a database for protein structure analysis and prediction\n") #self.file.write("// Krieger,E., Hooft,R.W.W., Nabuurs,S., Vriend.G. (2004) Submitted\n//\n") self.file.write("// Fields also present in the original PDBFinder are explained at www.cmbi.ru.nl/gv/pdbfinder\n//\n") # =============================================================================================== self.file.write("// The following extensions have been added in PDBFinderII:\n") self.file.write("// Chain breaks '-' in the Sequence fields allow an easy alignment with Swissprot or SEQRES sequences (breaks are based on DSSP judgement, may differ slightly from the number in the 'Break' field.\n") self.file.write("// DSSP: The secondary structure assigned by DSSP, 'H' for helix, 'E' for beta sheet and 'C' for coil (instead of spaces).\n") self.file.write("// Nalign: Number of alignments in the HSSP file on a logarithmic scale: calculate 10^((N-1)*0.25) to get an estimate (N is in [0..9]). The number on the right side is the average number of HSSP alignments per residue.\n") self.file.write("// Nindel: Sum of insertions and deletions, on the same logarithmic scale as Nalign. Again the number on the right is the non-logarithmic average over all residues.\n") self.file.write("// Entropy: The HSSP entropy (page 60 in the original paper), multiplied with 9/ln(20).\n") self.file.write("// Cons-Weight: The HSSP conservation weights (page 59 in the original paper), multiplied with 9.\n") self.file.write("// Cryst-Cont: '+' marks residues involved in crystal contacts.\n") self.file.write("// Access: Relative side chain accessibility, 0=buried, 9=exposed. To help identify completely buried residues, the mapping is as follows: 0% is mapped to 0, and ]0-100]% is mapped to [1..9]\n") self.file.write("// Quality: The number given here is the average over the fields 'Phi/psi','Backbone' and 'Packing-1' below. High resolution X-ray structures reach values around 0.75, NMR structures calculated from only a few restraints can be found around 0.3.\n") self.file.write("// Several quality estimators from the PDBREPORTs follow, most of the time, 0=requires attention, 9=perfect.\n") self.file.write("// Present: 9 minus the number of missing atoms per residue.\n") self.file.write("// B-Factors: Crystallographic B-factors, the range [10..60] is mapped to [9..0].\n") self.file.write("// Bonds: Absolute Z-score of the largest bond deviation per residue (using Engh&Huber parameters), absolute Z-Scores in the range [5..2] are mapped to [0..9].\n") self.file.write("// Angles: Absolute Z-score of the largest angle deviation per residue (using Engh&Huber parameters), absolute Z-Scores in the range [5..2] are mapped to [0..9].\n") self.file.write("// Torsions: Average Z-score of the torsion angles per residue, Z-Scores in the range [-3..+3] are mapped to [0..9].\n") self.file.write("// Phi/Psi: Ramachandran Z-score per residue, Z-Scores in the range [-4..+4] are mapped to [0..9]. The number on the right side is not a plain average, but again remapped using the WHAT IF database distribution of averages. Multiply with 8 and subtract 4 to get the Z-score in the PDBReport. N- and C-terminal residues are undefined ('?').\n") self.file.write("// Planarity: Z-score for the planarity of the residue sidechain, Z-Scores in the range [6..2] are mapped to [0..9]. Residues without planar side-chains score '9'.\n") self.file.write("// Chirality: Average absolute Z-score of the chirality deviations per residue, average absolute Z-Scores in the range [4..2] are mapped to [0..9]. Glycine always scores '9'.\n") self.file.write("// Backbone: Number of similar backbone conformations found in the database, numbers in the range [0..10] are mapped to [0..9]. No scores can be obtained for the N- and C-terminal two residues, as stretches of five are used.\n") self.file.write("// Peptide-Pl: RMS distance of the backbone oxygen from the oxygen in similar backbone conformations found in the database, distances in the range [3..1] are mapped to [0..9]. If less than 10 hits are found, there are not sufficient data to perform the following two checks.\n") self.file.write("// Rotamer: Probability that the sidechain rotamer (chi-1 only) is correct, probabilities in the range [0.1 .. 0.9] are mapped to [0..9]. Gly, Ala and Pro always score '9'.\n") self.file.write("// Chi-1/chi-2:Z-score for the sidechain chi-1/chi-2 combination, Z-scores in the range [-4..+4] are mapped to [0..9]. Residues with only <=1 side-chain torsion angle score '9'. The number on the right side is not a plain average, but again remapped using the WHAT IF database distribution of averages. Multiply with 8 and subtract 4 to get the Z-score in the PDBReport.\n") self.file.write("// Bumps: Sum of bumps per residue, distances in the range [0.1 .. 0] are mapped to [0..9].\n") self.file.write("// Packing-1: First packing quality Z-score, Z-scores in the range [-5..+5] are mapped to [0..9]. The number on the right side is not a plain average, but again remapped using the WHAT IF database distribution of averages. Multiply with 10 and subtract 5 to get the Z-score in the PDBReport.\n") self.file.write("// Packing-2: Second packing quality Z-score, Z-scores in the range [-3..+3] are mapped to [0..9]. The number on the right side is not a plain average, but again remapped using the WHAT IF database distribution of averages. Multiply with 6 and subtract 3 to get the Z-score in the PDBReport.\n") self.file.write("// In/Out: Absolute inside/outside distribution Z-score per residue, Z-scores in the range [4..2] are mapped to [0..9].\n") self.file.write("// H-Bonds: 9 minus number of unsatisfied hydrogen bonds, an additional 1 is subtracted for a buried backbone nitrogen, 4 for buried sidechain.\n") self.file.write("// Flips: Indicates flipped Asn/Gln/His sidechain, 9=OK, 0=needs flipping.\n") self.file.write("// If not indicated otherwise, numbers on the right side are the average, multiplied with 1/9. This average is calculated before the values of the individual residues are clamped to [0..9].\n") self.file.write("// *NOTE*: There are some very incorrect PDB files containing multiple structure models on top of each other but forgetting the 'MODEL' keyword. These errors can propagate to some fields in PDBFINDER II (DSSP, Packing, Bumps).\n") self.file.write("// It is suggested to look for the 'T-Alternates' field which contains the number of residues with alternate locations and ignore those PDB files with more than a handful.\n") self.file.write("//\n") else: self.file.write("//\n") self.file.write("// This file is PDBFIND.TXT\n") self.file.write("//\n") self.file.write("// On my watch it's %s.\n//\n" % time.ctime(time.time())) self.file.write("//\n") self.file.write("// (C) 1996 Rob W.W. Hooft, Chris Sander, Michael Scharf and Gert Vriend\n") self.file.write("// Updated to V3.0 in May 2000 by Elmar Krieger\n") self.file.write("//\n") self.file.write("// This file is freely redistributable, but only in unmodified form.\n") self.file.write("// This copyright notice must be preserved on each copy. The latest\n") self.file.write("// version of this database should always be available by FTP from\n") self.file.write("// ftp.cmbi.kun.nl. It is distributed as part of the WHAT IF program.\n") self.file.write("// Proper acknowledgement is required.\n") # GET AVERAGE QUALITY # =================== # IN PDBFINDER II ENTRIES, QUALITY INDICATORS FOR MULTIPLE CHAINS CAN BE CONCATENATED # LIKE THAT: 5599999998999999999999| 0.94469999| 0.9912 # (TWO CHAINS, ONE WITH 22 RESIDUES AND QUALITY 0.9446 AND ONE WITH 4 RESIDUES AND # QUALITY 0.9912) def averagequal(self,checkname): checkstr=self.fieldvalue(checkname) qualsum=0 residues=0 while (checkstr): pos=checkstr.find('|') chainlen=pos if (pos==-1): break quality=float(checkstr[pos+1:pos+9]) # Why was this here? Fails for qualities that are accidentally zero #if (quality!=0): qualsum=qualsum+quality*chainlen residues=residues+chainlen checkstr=checkstr[pos+9:] if (residues): return(qualsum/residues) else: return(None) # RETURN CHAIN SEQUENCE # ===================== # THE SEQUENCE OF THE GIVEN CHAIN IS RETURNED, chainstr CAN BE '~' FOR ALL CHAINS. def chainseq(self,chainstr,chaintype=None): sequence="" for i in range(self.chains): if ((self.chain[i] in chainstr or chainstr=='~') and self.sequence[i]!="" and (chaintype==None or self.chaintype[i]==chaintype)): sequence+=self.sequence[i]+'|' sequence=sequence[:-1] if (sequence==""): sequence=None return(sequence) # RETURN CHAIN SECONDARY STRUCTURE # ================================ # THE SECONDARY STRUCTURE OF THE GIVEN CHAIN IS RETURNED, chainstr CAN BE '~' FOR ALL CHAINS. def chainsecstr(self,chainstr): secstr="" for i in range(self.chains): if (self.chain[i] in chainstr or chainstr=='~'): secstr=secstr+self.secstr[i]+'|' secstr=secstr[:-1] if (secstr==""): secstr=None return(secstr) # GET CHAIN SEQUENCE WITHOUT GAPS # =============================== # THE SEQUENCE OF THE SPECIFIED CHAIN IS RETURNED WITH GAPS '-' REMOVED. def gaplesschainseq(self,chain): return(self.chainseq(chain).replace('-','')) # GET SECONDARY STRUCTURE WITHOUT GAPS # ==================================== # THE SECONDARY STRUCTURE OF THE SPECIFIED CHAIN IS RETURNED WITH GAPS '-' REMOVED. def gaplesschainsecstr(self,chain): return(self.chainsecstr(chain).replace('-','')) # INSERT GAPS # =========== # GAP SYMBOLS '-' ARE INSERTED IN THE sequence STRING AT THE POSITIONS DEFINED # IN CHAIN chain. def gapsinserted(self,sequence,chain): chainseq=self.chainseq(chain) for i in range(len(chainseq)): if (chainseq[i]=='-' and i=len(line)): self.raiseerror("read: Wrong file format - line too short or missing // terminator") return(None) if (line[colpos]!=':'): self.raiseerror("read: Wrong file format - no colon found in column %d" % (colpos+1)) return(None) # ADD FIELD NAME self.field.append(line[:colpos].rstrip()) # ADD VALUE self.value.append(line[colpos+2:].rstrip()) # IF NO FIELDS COULD BE READ, WE HAVE REACHED THE END if (not self.fields): self.eof=1 else: # SEARCH FOR ID,RESOLUTION,CHAINS AND CHECK FOR BACKBONE COMPLETENESS self.id=None date=None resolution=4.0 rfactor=0.3 amacs=0 self.expmethod="Unknown" self.source="Unknown" for i in range(self.fields): if (self.field[i]=="ID"): self.id=self.value[i] elif (self.field[i]==" Date"): date=self.value[i] elif (self.field[i]==" Resolution"): resolution=float(self.value[i]) elif (self.field[i]==" R-Factor"): rfactor=float(self.value[i]) elif (self.field[i]=="T-Nres-Prot"): amacs=int(self.value[i]) elif (self.field[i]=="Exp-Method"): self.expmethod=self.value[i] elif (self.field[i]=="Chain"): chaintype="UNKNOWN" self.chain.append(self.value[i]) self.sequence.append("") self.secstr.append("") self.chaintype.append(chaintype) self.chainquality.append(None) bblist.append((self.value[i],1)) self.chains+=1 elif (self.field[i]==" Ch-Auth-ID"): # THE AUTHOR PROVIDED CHAIN NAME IN MMCIF FILES IS THE ONE WE NEED self.chain[-1]=self.value[i] elif (self.field[i]=="Source" and self.source==""): self.source=self.value[i] if (source.startswith('(') and source.endswith(')')): source=source[1:-1].strip() if (len(source)): source=source[0].upper()+source[1:] elif (self.field[i]==" Amino-Acids"): self.peptidechains=self.peptidechains+1 self.chaintype[-1]="PEPTIDE" elif (self.field[i]==" Nucl-Acids"): self.nucleotidechains=self.nucleotidechains+1 self.chaintype[-1]="DNA" elif (self.field[i]==" Break" or self.field[i]==" Miss-BB" or self.field[i]==" only-Ca"): bblist=bblist[:-1] bblist.append((self.chain[-1],0)) elif (self.field[i]==" Sequence"): # UPDATE NUMBER OF DIFFERENT CHAINS if (self.value[i] not in self.sequence): self.diffchains=self.diffchains+1 if (self.chaintype[-1]=="PEPTIDE"): self.diffpeptidechains=self.diffpeptidechains+1 elif (self.chaintype[-1]=="DNA"): self.diffnucleotidechains=self.diffnucleotidechains+1 self.sequence[-1]=self.value[i] elif (self.field[i]==" DSSP"): # ADD POSSIBLY MISSING TERMINAL SPACES self.value[i]=self.value[i]+' '*(len(self.sequence[-1])-len(self.value[i])) self.secstr[-1]=self.value[i] elif (self.field[i]==" Quality"): qualitylist=self.value[i].split('|') self.chainquality[-1]=0 for quality in qualitylist: self.chainquality[-1]+=float(quality) self.chainquality[-1]/=len(qualitylist) if (self.id!=None): # ADD VALUES TO FAST ACCESS DICTIONARY if (startpos!=None): self.recordpos[self.id]=startpos self.resolution[self.id]=resolution self.date[self.id]=date self.rfactor[self.id]=rfactor self.amacs[self.id]=amacs # LIST OF DUPLES (CHAIN,OKFLAG) self.backboneok[self.id]=bblist return(not self.eof) # CHECK IF A GIVEN PDB ID IS KNOWN # ================================ def hasid(self,id): id=id[:4].upper() if (id not in self.supplement and id not in self.recordpos): return(0) else: return(1) # ADD A GIVEN RECORD AS A SUPPLEMENT # ================================== def addsupplement(self,record): id=record.fieldvalue("ID") print("Adding PDBFINDER supplement for %s"%id) self.supplement[id]=[record.fields,record.field,record.value] # WRITE A PDBFINDER RECORD # ======================== # THE CURRENT RECORD IS WRITTEN TO DISC. def write(self): for i in range(self.fields): line=self.field[i] while (len(line)<13): line=line+" " line=line+": "+self.value[i]+"\n" self.file.write(line) self.file.write("//\n") # SEEK RECORD # =========== # THE RECORD CORRESPONDING TO PDB id IS READ, ASSUMING THAT # Instance.buildindex() WAS CALLED SOMETIME BEFORE. def seek(self,id): id=id[:4].upper() if (id not in self.supplement): if (id not in self.recordpos): self.raiseerror("seek: PDBFINDER does not contain a record called '%s'. Update database." % id) else: self.file.seek(self.recordpos[id]) self.eof=0 self.error=None # GET SEQUENTIAL RESIDUE NUMBERS # ============================== # THE CURRENT CHECKDB FILE IS READ TILL THE PDBLST FIELD, RESIDUES MATCHING # chain ARE EXTRACTED, THEN THE LIST OF RESIDUE NAMES PASSED AS reslist IS # CONVERTED INTO A LIST OF SEQUENTIAL RESIDUE NUMBERS WHICH IS RETURNED. def sequentialnumbers(self,chain,reslist): if (self.type!=2): self.raiseerror("sequentialnumbers: This method can only be called with CHECKDB files") return if (chain==' '): chain='_' while (1): if (self.eof): self.raiseerror("sequentialnumbers: No PDBLST found in CHECKDB file") return if (self.fieldvalue("CheckID")=="PDBLST"): pdblist=[] resnolist=[] # READ ALL RESIDUE NAMES for i in range(self.fields): if (self.field[i]==" Name"): resname=self.resname(i) if (resname[0]==chain or chain=='~'): pdblist.append(resname) # FIND THE SEQUENTIAL RESIDUE NUMBERS for resname in reslist: if (resname not in pdblist): self.raiseerror("sequentialnumbers: Residue %s not found in PDBLST" % resname) return resnolist.append(pdblist.index(resname)) return(resnolist) self.read() # REWIND TO START # =============== def rewind(self): self.file.seek(0) self.eof=0 self.error=None # BUILD INDEX # =========== # READS THE COMPLETE DATA BASE AND MAKES IT ACCESSIBLE WITH Instance.seek() def buildindex(self): if (not self.error): if (os.path.exists(self.indexfilename) and disk.modtime(self.indexfilename)>disk.modtime(self.path)): # LOAD INDEX FILE print("Found index file.") indexfile=open(self.indexfilename,"rb") self.date=pickle.load(indexfile) self.recordpos=pickle.load(indexfile) self.resolution=pickle.load(indexfile) self.rfactor=pickle.load(indexfile) self.amacs=pickle.load(indexfile) self.backboneok=pickle.load(indexfile) indexfile.close() else: # THIS BUILDS THE COMPLETE DICTIONARY OF RECORD POSITIONS FOR RANDOM ACCESS self.rewind() while (not self.eof): self.read() # WRITE INDEX TO DISC try: print("Writing PDBFINDER index for fast access next time") indexfile=open(self.indexfilename,"wb") pickle.dump(self.date,indexfile) pickle.dump(self.recordpos,indexfile) pickle.dump(self.resolution,indexfile) pickle.dump(self.rfactor,indexfile) pickle.dump(self.amacs,indexfile) pickle.dump(self.backboneok,indexfile) except: print("PDBFINDER index could not be written.") # CLOSE FILE # ========== # THE PDBFINDER FILE IS CLOSED, NO FURTHER ACCESS IS POSSIBLE. def close(self): if (self.file): self.file.close() # COPY RECORD # =========== # THE CURRENT RECORD IS REPLACED WITH A COPY FROM ANOTHER pdb_finder INSTANCE. def copy(self,source): self.id=source.id self.error=source.error self.fields=source.fields self.field=source.field self.value=source.value self.chain=source.chain # SEARCH FOR FIELD # ================ # THE SPECIFIED FIELD IS SEARCHED, THE VALUE OF ITS FIRST OCCURANCE IS RETURNED. def fieldvalue(self,name): for i in range(self.fields): if (self.field[i]==name): return(self.value[i]) return(None) # SEARCH FOR NEXT FIELD WITH GIVEN NAME # ===================================== # THE INDEX IS RETURNED, THE SEARCH IS STOPPED IF A FIELD WITH LOWER INDENTATION IS FOUND def nextfield(self,index,name): if (index==None): return(None) # GET CURRENT INDENTATION LEVEL indlevel=self.field[index].count(" ") # SEARCH while (1): index=index+1 if (index>=self.fields or self.field[index].count(" "). def insert(self,pos,field,value): self.field.insert(pos,field) self.value.insert(pos,value) self.fields=self.fields+1 # DELETE ALL FIELDS # ================= # ALL FIELDS WITH ARE DELETED. def delfields(self,name): i=0 while (i IS DELETED. def delete(self,number): if (number IS RETURNED IN STANDARD FORMAT: C-NUMB-RES-ATOM # KNOWN CHECKDB FORMATS SO FAR: # - Name: A- 1-OCY- OP1 # - Name: 21-CYS # - Name: HOH -HOH # - Name: 22-PRO - CA # - Name: 22-PRO-CA # - Name: A- 22-PRO-CA def resname(self,field): res=self.value[field] if (res[1]=='-' and res[6]=='-'): if (len(res)>10): res=res[0:10] return(res) if (res[4]=='-'): res="_-"+res if (len(res)>10): if (res[10]==' '): res=res[:10]+res[11:] if (res[10]=='-'): res=res[:10] return(res) return(None) # PRINT PDBFINDER RECORD # ====================== def __repr__(self): pdbfinder=self # WHEN USING THIS MODULE, YOU WOULD DO SOMETHING LIKE # import pdbfinder_file # pdbfinder = pdbfinder_file.interface("MyPDBFinderFilename") # IN THE BEGINNING. THEN FOLLOW THE EXAMPLES BELOW: print("PDBFinder Record:") for i in range(pdbfinder.fields): line=pdbfinder.field[i] while (len(line)<13): line+=" " line+=": "+pdbfinder.value[i] print(line) return("")