comparison corebio/resource/scop.py @ 0:c55bdc2fb9fa

Uploaded
author davidmurphy
date Thu, 27 Oct 2011 12:09:09 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:c55bdc2fb9fa
1
2 # Copyright 2000 by Jeffrey Chang. All rights reserved.
3 # Copyright 2001 by Gavin E. Crooks. All rights reserved.
4 # Modifications Copyright 2004/2005 James Casbon.
5 # Copyright 2005 by Regents of the University of California. All rights Reserved.
6 # (Major rewrite for conformance to corebio. Gavin Crooks)
7 #
8 # This code is derived from the Biopython distribution and is governed by it's
9 # license. Please see the LICENSE file that should have been included
10 # as part of this package.
11
12
13 """ SCOP: Structural Classification of Proteins.
14
15 The SCOP database aims to provide a manually constructed classification of
16 all know protein structures into a hierarchy, the main levels of which
17 are family, superfamily and fold.
18
19 * SCOP: http://scop.mrc-lmb.cam.ac.uk/scop/
20 * Introduction: http://scop.mrc-lmb.cam.ac.uk/scop/intro.html
21 * SCOP parsable files: http://scop.mrc-lmb.cam.ac.uk/scop/parse/
22
23 The Scop object in this module represents the entire SCOP classification. It
24 can be built from the three SCOP parsable files (see DesRecord, HieRecord and
25 ClaRecord), modified is so desired, and converted back to the same file formats.
26 A single SCOP domain (represented by the Domain class) can be obtained from
27 Scop using the domain's SCOP identifier (sid).
28
29 Classes:
30 - Scop -- The entire SCOP hierarchy.
31 - Node -- A node in the SCOP hierarchy.
32 - Domain -- A SCOP domain.
33 - Residues -- A collection of residues from a PDB structure.
34 - HieRecord -- Handle the SCOP HIErarchy files.
35 - DesRecord -- Handle the SCOP DEScription file.
36 - ClaRecord -- Handle the SCOP CLAssification file.
37
38
39 nodeCodeDict -- A mapping between known 2 letter node codes and a longer
40 description. The known node types are 'cl' (class), 'cf'
41 (fold), 'sf' (superfamily), 'fa' (family), 'dm' (domain),
42 'sp' (species), 'px' (domain). Additional node types may
43 be added in the future.
44 """
45
46 import os, re
47
48
49 nodeCodeDict = { 'cl':'class', 'cf':'fold', 'sf':'superfamily',
50 'fa':'family', 'dm':'protein', 'sp':'species', 'px':'domain'}
51
52
53 _nodetype_to_code= dict([[v,k] for k,v in nodeCodeDict.items()])
54
55
56 nodeCodeOrder = [ 'ro', 'cl', 'cf', 'sf', 'fa', 'dm', 'sp', 'px' ]
57
58
59 def cmp_sccs(sccs1, sccs2) :
60 """Order SCOP concise classification strings (sccs).
61
62 a.4.5.1 < a.4.5.11 < b.1.1.1
63
64 A sccs (e.g. a.4.5.11) compactly represents a domain's classification.
65 The letter represents the class, and the numbers are the fold,
66 superfamily, and family, respectively.
67
68 """
69
70 s1 = sccs1.split(".")
71 s2 = sccs2.split(".")
72
73 if s1[0] != s2[0]: return cmp(s1[0], s2[0])
74
75 s1 = map(int, s1[1:])
76 s2 = map(int, s2[1:])
77
78 return cmp(s1,s2)
79
80
81
82 def _open_scop_file(scop_dir_path, version, filetype) :
83 filename = "dir.%s.scop.txt_%s" % (filetype,version)
84 afile = open(os.path.join( scop_dir_path, filename))
85 return afile
86
87
88 class Scop(object):
89 """The entire SCOP hierarchy.
90
91 root -- The root node of the hierarchy
92 domains -- A list of all domains
93 nodes_by_sid -- A dictionary of nodes indexed by SCOP identifier
94 (e.g. 'd1hbia_')
95 domains_by_sunid -- A dictionary of domains indexed by SCOP uniquie
96 identifiers (e.g. 14996)
97 """
98 def __init__(self):
99 """ An empty Scop object.
100
101 See also Scop.parse() and Scop.parse_files()
102 """
103 self.root = None
104 self.domains = []
105 self.nodes_by_sunid = dict()
106 self.domains_by_sid = dict()
107
108 #@classmethod
109 def parse(cls, dir_path, version) :
110 """Build the SCOP hierarchy from the SCOP parsable files.
111
112 - dir_path -- A directory that contains the SCOP files
113 - version -- The SCOP version (as a string)
114
115 The SCOP files are named dir.XXX.scop.txt_VERSION, where XXX
116 is 'cla', 'des' or 'hie'.
117 """
118 cla_file = None
119 des_file = None
120 hie_file = None
121 try :
122 cla_file = _open_scop_file( dir_path, version, 'cla')
123 des_file = _open_scop_file( dir_path, version, 'des')
124 hie_file = _open_scop_file( dir_path, version, 'hie')
125 scop = cls.parse_files(cla_file, des_file, hie_file)
126 finally :
127 # If we opened the files, we close the files
128 if cla_file : cla_file.close()
129 if des_file : des_file.close()
130 if hie_file : hie_file.close()
131
132 return scop
133 parse = classmethod(parse)
134
135 #@classmethod
136 def parse_files(cls, cla_file, des_file, hie_file):
137 """Build the SCOP hierarchy from the SCOP parsable files.
138
139 - cla_file -- the CLA clasification file
140 - des_file -- the DES description file
141 - hie_file -- the HIE hierarchy file
142 """
143
144 self = cls()
145
146 sunidDict = {}
147
148 root = Node()
149 domains = []
150 root.sunid=0
151 root.type='ro'
152 sunidDict[root.sunid] = root
153
154 root.description = 'SCOP Root'
155
156 # Build the rest of the nodes using the DES file
157 for rec in DesRecord.records(des_file):
158 if rec.nodetype =='px' :
159 n = Domain()
160 n.sid = rec.name
161 domains.append(n)
162 else :
163 n = Node()
164 n.sunid = rec.sunid
165 n.type = rec.nodetype
166 n.sccs = rec.sccs
167 n.description = rec.description
168
169 sunidDict[n.sunid] = n
170
171 # Glue all of the Nodes together using the HIE file
172 for rec in HieRecord.records(hie_file):
173 if not rec.sunid in sunidDict :
174 print rec.sunid #FIXME: HUH?
175
176 n = sunidDict[rec.sunid]
177 if rec.parent !='': # Not root node
178 if not rec.parent in sunidDict :
179 raise ValueError("Incomplete data?")
180 n.parent = sunidDict[rec.parent]
181
182 for c in rec.children:
183 if not c in sunidDict :
184 raise ValueError("Incomplete data?")
185 n.children.append(sunidDict[c])
186
187
188 # Fill in the gaps with information from the CLA file
189 sidDict = {}
190 for rec in ClaRecord.records(cla_file):
191 n = sunidDict[rec.sunid]
192 assert n.sccs == rec.sccs
193 assert n.sid == rec.sid
194 n.residues = rec.residues
195 sidDict[n.sid] = n
196
197 # Clean up
198 self.root = root
199 self.nodes_by_sunid = sunidDict
200 self.domains_by_sid = sidDict
201 self.domains = tuple(domains)
202
203 return self
204 parse_files = classmethod(parse_files)
205
206
207 def write_hie(self, stream) :
208 """Build an HIE SCOP parsable file from this object"""
209 nodes = self.nodes_by_sunid.values()
210 # We order nodes to ease comparison with original file
211 nodes.sort(lambda n1,n2: cmp(n1.sunid, n2.sunid))
212
213 for n in nodes :
214 stream.write(str(n.to_hie_record()))
215
216
217 def write_des(self, stream) :
218 """Build a DES SCOP parsable file from this object"""
219 nodes = self.nodes_by_sunid.values()
220 # Origional SCOP file is not ordered?
221 nodes.sort(lambda n1,n2: cmp(n1.sunid, n2.sunid))
222
223 for n in nodes :
224 if n != self.root :
225 stream.write(str(n.to_des_record()))
226
227
228 def write_cla(self, stream) :
229 """Build a CLA SCOP parsable file from this object"""
230 nodes = self.domains_by_sid.values()
231 # We order nodes to ease comparison with original file
232 nodes.sort(lambda n1,n2: cmp(n1.sunid, n2.sunid))
233
234 for n in nodes :
235 stream.write(str(n.to_cla_record()))
236 # End Scop
237
238
239
240 class Node(object) :
241 """ A node in the Scop hierarchy
242
243 sunid -- SCOP unique identifiers. e.g. '14986'
244 parent -- The parent node
245 children -- A list of child nodes
246 sccs -- SCOP concise classification string. e.g. 'a.1.1.2'
247 type -- A 2 letter node type code. e.g. 'px' for domains
248 description --
249
250 """
251 def __init__(self) :
252 """A new, uninitilized SCOP node."""
253 self.sunid=''
254 self.parent = None
255 self.children=[]
256 self.sccs = ''
257 self.type =''
258 self.description =''
259
260 def __str__(self) :
261 s = []
262 s.append(str(self.sunid))
263 s.append(self.sccs)
264 s.append(self.type)
265 s.append(self.description)
266
267 return " ".join(s)
268
269 def to_hie_record(self):
270 """Return an Hie.Record"""
271 rec = HieRecord()
272 rec.sunid = str(self.sunid)
273 if self.parent : # Not root node
274 rec.parent = str(self.parent.sunid)
275 else:
276 rec.parent = '-'
277 for c in self.children :
278 rec.children.append(str(c.sunid))
279 return rec
280
281 def to_des_record(self):
282 """Return a Des.Record"""
283 rec = DesRecord()
284 rec.sunid = str(self.sunid)
285 rec.nodetype = self.type
286 rec.sccs = self.sccs
287 rec.description = self.description
288 return rec
289
290 def descendents( self, node_type) :
291 """ Return a list of all decendent nodes of the given type. Node type
292 can be a two letter code or longer description. e.g. 'fa' or 'family'
293 """
294 if node_type in _nodetype_to_code:
295 node_type = _nodetype_to_code[node_type]
296
297 nodes = [self]
298
299 while nodes[0].type != node_type:
300 if nodes[0].type == 'px' :
301 return [] # Fell of the bottom of the hierarchy
302 child_list = []
303 for n in nodes:
304 for child in n.children:
305 child_list.append( child )
306 nodes = child_list
307
308 return nodes
309
310
311 def ascendent( self, node_type) :
312 """ Return the ancestor node of the given type, or None. Node type can
313 be a two letter code or longer description. e.g. 'fa' or 'family'
314 """
315 if node_type in _nodetype_to_code :
316 node_type = _nodetype_to_code[node_type]
317
318 n = self
319 if n.type == node_type: return None
320 while n.type != node_type:
321 if n.type == 'ro':
322 return None # Fell of the top of the hierarchy
323 n = n.parent
324
325 return n
326 # End Node
327
328
329 class Domain(Node) :
330 """ A SCOP domain. A leaf node in the Scop hierarchy.
331
332 - sid -- The SCOP domain identifier. e.g. 'd5hbib_'
333 - residues -- A Residue object. It defines the collection
334 of PDB atoms that make up this domain.
335 """
336 def __init__(self) :
337 Node.__init__(self)
338 self.sid = ''
339 self.residues = None
340
341 def __str__(self) :
342 s = []
343 s.append(self.sid)
344 s.append(self.sccs)
345 s.append("("+str(self.residues)+")")
346
347 if not self.parent :
348 s.append(self.description)
349 else :
350 sp = self.parent
351 dm = sp.parent
352 s.append(dm.description)
353 s.append("{"+sp.description+"}")
354
355 return " ".join(s)
356
357 def to_des_record(self):
358 """Return a des.Record"""
359 rec = Node.to_des_record(self)
360 rec.name = self.sid
361 return rec
362
363 def to_cla_record(self) :
364 """Return a cla.Record"""
365 rec = ClaRecord()
366 rec.sid = self.sid
367 rec.residues = self.residues
368 rec.sccs = self.sccs
369 rec.sunid = self.sunid
370
371 n = self
372 while n.sunid != 0: # Not root node
373 rec.hierarchy.append( (n.type, str(n.sunid)) )
374 n = n.parent
375
376 rec.hierarchy.reverse()
377
378 return rec
379 # End Domain
380
381
382
383 class DesRecord(object):
384 """ Handle the SCOP DEScription file.
385
386 The file format is described in the scop
387 "release notes.":http://scop.berkeley.edu/release-notes-1.55.html
388 The latest DES file can be found
389 "elsewhere at SCOP.":http://scop.mrc-lmb.cam.ac.uk/scop/parse/
390
391 The DES file consisnt of one DES record per line. Each record
392 holds information for one node in the SCOP hierarchy, and consist
393 of 5 tab deliminated fields,
394 sunid, node type, sccs, node name, node description.
395
396 For example ::
397
398 21953 px b.1.2.1 d1dan.1 1dan T:,U:91-106
399 48724 cl b - All beta proteins
400 48725 cf b.1 - Immunoglobulin-like beta-sandwich
401 49265 sf b.1.2 - Fibronectin type III
402 49266 fa b.1.2.1 - Fibronectin type III
403
404
405 - sunid -- SCOP unique identifiers
406 - nodetype -- One of 'cl' (class), 'cf' (fold), 'sf' (superfamily),
407 'fa' (family), 'dm' (protein), 'sp' (species),
408 'px' (domain). Additional node types may be added.
409 - sccs -- SCOP concise classification strings. e.g. b.1.2.1
410 - name -- The SCOP ID (sid) for domains (e.g. d1anu1),
411 currently empty for other node types
412 - description -- e.g. "All beta proteins","Fibronectin type III",
413 """
414 def __init__(self, record=None):
415
416 if not record :
417 self.sunid = ''
418 self.nodetype = ''
419 self.sccs = ''
420 self.name = ''
421 self.description =''
422 else :
423 entry = record.rstrip() # no trailing whitespace
424 columns = entry.split("\t") # separate the tab-delineated cols
425 if len(columns) != 5:
426 raise ValueError("I don't understand the format of %s" % entry)
427
428 self.sunid, self.nodetype, self.sccs, self.name, self.description \
429 = columns
430 if self.name == '-' : self.name =''
431 self.sunid = int(self.sunid)
432
433 def __str__(self):
434 s = []
435 s.append(self.sunid)
436 s.append(self.nodetype)
437 s.append(self.sccs)
438 if self.name :
439 s.append(self.name)
440 else :
441 s.append("-")
442 s.append(self.description)
443 return "\t".join(map(str,s)) + "\n"
444
445 #@staticmethod
446 def records(des_file):
447 """Iterates over a DES file, generating DesRecords """
448 for line in des_file:
449 if line[0] =='#': continue # A comment
450 if line.isspace() : continue
451 yield DesRecord(line)
452 records = staticmethod(records)
453 # End DesRecord
454
455 class HieRecord(object):
456 """Handle the SCOP HIErarchy files, which describe the SCOP hierarchy in
457 terms of SCOP unique identifiers (sunid).
458
459 The file format is described in the scop
460 "release notes.":http://scop.berkeley.edu/release-notes-1.55.html
461 The latest HIE file can be found
462 "elsewhere at SCOP.":http://scop.mrc-lmb.cam.ac.uk/scop/parse/
463
464 "Release 1.55":http://scop.berkeley.edu/parse/dir.hie.scop.txt_1.55
465 Records consist of 3 tab deliminated fields; node's sunid,
466 parent's sunid, and a list of children's sunids. For example ::
467
468 0 - 46456,48724,51349,53931,56572,56835,56992,57942
469 21953 49268 -
470 49267 49266 49268,49269
471
472 Each record holds information for one node in the SCOP hierarchy.
473
474 sunid -- SCOP unique identifiers of this node
475 parent -- Parents sunid
476 children -- Sequence of childrens sunids
477 """
478 def __init__(self, record = None):
479 self.sunid = None
480 self.parent = None
481 self.children = []
482
483 if not record : return
484
485 # Parses HIE records.
486 entry = record.rstrip() # no trailing whitespace
487 columns = entry.split('\t') # separate the tab-delineated cols
488 if len(columns) != 3:
489 raise ValueError("I don't understand the format of %s" % entry)
490
491 self.sunid, self.parent, children = columns
492
493 if self.sunid =='-' : self.sunid = ''
494 if self.parent =='-' : self.parent = ''
495 else : self.parent = int( self.parent )
496
497 if children =='-' :
498 self.children = ()
499 else :
500 self.children = children.split(',')
501 self.children = map ( int, self.children )
502
503 self.sunid = int(self.sunid)
504
505 def __str__(self):
506 s = []
507 s.append(str(self.sunid))
508
509 if self.parent:
510 s.append(str(self.parent))
511 else:
512 if self.sunid != 0:
513 s.append('0')
514 else:
515 s.append('-')
516
517 if self.children :
518 child_str = map(str, self.children)
519 s.append(",".join(child_str))
520 else:
521 s.append('-')
522
523 return "\t".join(s) + "\n"
524
525
526 #@staticmethod
527 def records(hie_file):
528 """Iterates over a DOM file, generating DomRecords """
529 for line in hie_file:
530 if line[0] =='#': continue # A comment
531 if line.isspace() : continue
532 yield HieRecord(line)
533 records = staticmethod(records)
534 # End HieRecord
535
536
537
538 class ClaRecord(object):
539 """Handle the SCOP CLAssification file, which describes SCOP domains.
540
541 The file format is described in the scop
542 "release notes.":http://scop.berkeley.edu/release-notes-1.55.html
543 The latest CLA file can be found
544 "elsewhere at SCOP.":http://scop.mrc-lmb.cam.ac.uk/scop/parse/
545
546 sid -- SCOP identifier. e.g. d1danl2
547 residues -- The domain definition as a Residues object
548 sccs -- SCOP concise classification strings. e.g. b.1.2.1
549 sunid -- SCOP unique identifier for this domain
550 hierarchy -- A sequence of tuples (nodetype, sunid) describing the
551 location of this domain in the SCOP hierarchy.
552 See the Scop module for a description of nodetypes.
553 """
554 def __init__(self, record=None):
555 self.sid = ''
556 self.residues = None
557 self.sccs = ''
558 self.sunid =''
559 self.hierarchy = []
560
561 if not record: return
562
563 # Parse a tab-deliminated CLA record.
564 entry = record.rstrip() # no trailing whitespace
565 columns = entry.split('\t') # separate the tab-delineated cols
566 if len(columns) != 6:
567 raise ValueError("I don't understand the format of %s" % entry)
568
569 self.sid, pdbid, residues, self.sccs, self.sunid, hierarchy = columns
570 self.residues = Residues(residues)
571 self.residues.pdbid = pdbid
572 self.sunid = int(self.sunid)
573
574 h = []
575 for ht in hierarchy.split(",") :
576 h.append( ht.split('='))
577 for ht in h:
578 ht[1] = int(ht[1])
579 self.hierarchy = h
580
581 def __str__(self):
582 s = []
583 s.append(self.sid)
584 s += str(self.residues).split(" ")
585 s.append(self.sccs)
586 s.append(self.sunid)
587
588 h=[]
589 for ht in self.hierarchy:
590 h.append("=".join(map(str,ht)))
591 s.append(",".join(h))
592
593 return "\t".join(map(str,s)) + "\n"
594
595 #@staticmethod
596 def records(cla_file):
597 """Iterates over a DOM file, generating DomRecords """
598 for line in cla_file:
599 if line[0] =='#': continue # A comment
600 if line.isspace() : continue
601 yield ClaRecord(line)
602 records = staticmethod(records)
603
604 # End ClaRecord
605
606
607
608
609 class DomRecord(object):
610 """Handle the SCOP DOMain file.
611
612 The DOM file has been officially deprecated. For more information see
613 the SCOP"release notes.":http://scop.berkeley.edu/release-notes-1.55.html
614 The DOM files for older releases can be found
615 "elsewhere at SCOP.":http://scop.mrc-lmb.cam.ac.uk/scop/parse/
616
617 DOM records consist of 4 tab deliminated fields;
618 sid, pdbid, residues, hierarchy
619 For example ::
620
621 d1sctg_ 1sct g: 1.001.001.001.001.001
622 d1scth_ 1sct h: 1.001.001.001.001.001
623 d1flp__ 1flp - 1.001.001.001.001.002
624 d1moh__ 1moh - 1.001.001.001.001.002
625
626 sid -- The SCOP ID of the entry, e.g. d1anu1
627 residues -- The domain definition as a Residues object
628 hierarchy -- A string specifying where this domain is in the hierarchy.
629 """
630 def __init__(self, record= None):
631 self.sid = ''
632 self.residues = []
633 self.hierarchy = ''
634
635 if record:
636 entry = record.rstrip() # no trailing whitespace
637 columns = entry.split("\t") # separate the tab-delineated cols
638 if len(columns) != 4:
639 raise ValueError("I don't understand the format of %s" % entry)
640 self.sid, pdbid, res, self.hierarchy = columns
641 self.residues = Residues(res)
642 self.residues.pdbid = pdbid
643
644 def __str__(self):
645 s = []
646 s.append(self.sid)
647 s.append(str(self.residues).replace(" ","\t") )
648 s.append(self.hierarchy)
649 return "\t".join(s) + "\n"
650
651 #@staticmethod
652 def records(dom_file):
653 """Iterates over a DOM file, generating DomRecords """
654 for line in dom_file:
655 if line[0] =='#': continue # A comment
656 if line.isspace() : continue
657 yield DomRecord(line)
658 records = staticmethod(records)
659 # End DomRecord
660
661
662
663
664 _pdbid_re = re.compile(r"^(\w\w\w\w)(?:$|\s+|_)(.*)")
665 _fragment_re = re.compile(r"\(?(\w:)?(-?\w*)-?(-?\w*)\)?(.*)")
666
667 class Residues(object) :
668 """A collection of residues from a PDB structure.
669
670 This class provides code to work with SCOP domain definitions. These
671 are concisely expressed as a one or more chain fragments. For example,
672 "(1bba A:10-20,B:)" indicates residue 10 through 20 (inclusive) of
673 chain A, and every residue of chain B in the pdb structure 1bba. The pdb
674 id and brackets are optional. In addition "-" indicates every residue of
675 a pbd structure with one unnamed chain.
676
677 Start and end residue ids consist of the residue sequence number and an
678 optional single letter insertion code. e.g. "12", "-1", "1a", "1000"
679
680
681 pdbid -- An optional PDB id, e.g. "1bba"
682 fragments -- A sequence of tuples (chainID, startResID, endResID)
683 """
684
685
686 def __init__(self, str=None) :
687 self.pdbid = ''
688 self.fragments = ()
689 if str is not None : self._parse(str)
690
691
692 def _parse(self, string):
693 string = string.strip()
694
695 #Is there a pdbid at the front? e.g. 1bba A:1-100
696 m = _pdbid_re.match(string)
697 if m is not None :
698 self.pdbid = m.group(1)
699 string = m.group(2) # Everything else
700
701 if string=='' or string == '-' or string=='(-)': # no fragments, whole sequence
702 return
703
704 fragments = []
705 for l in string.split(",") :
706 m = _fragment_re.match(l)
707 if m is None:
708 raise ValueError("I don't understand the format of %s" % l)
709 chain, start, end, postfix = m.groups()
710
711 if postfix != "" :
712 raise ValueError("I don't understand the format of %s" % l )
713
714 if chain:
715 if chain[-1] != ':':
716 raise ValueError("I don't understand the chain in %s" % l)
717 chain = chain[:-1] # chop off the ':'
718 else :
719 chain =""
720
721 fragments.append((chain, start, end))
722 self.fragments = tuple(fragments)
723
724 def __str__(self):
725 prefix =""
726 if self.pdbid :
727 prefix =self.pdbid +' '
728
729 if not self.fragments: return prefix+'-'
730 strs = []
731 for chain, start, end in self.fragments:
732 s = []
733 if chain: s.append("%s:" % chain)
734 if start: s.append("%s-%s" % (start, end))
735 strs.append("".join(s))
736 return prefix+ ",".join(strs)
737 # End Residues
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757