source: orange-bioinformatics/obiOntology.py @ 1326:aeca6a4e1d0f

Revision 1326:aeca6a4e1d0f, 19.2 KB checked in by ales_erjavec <ales.erjavec@…>, 3 years ago (diff)
  • initial commit of a more general OBO ontology library (To handle all .obo files from the OBO Foundry
Line 
1"""
2Python module for manipulating OBO Ontology files (http://www.obofoundry.org/)
3
4TODO:
5    - handle escape characters !!!
6
7Example::
8    >>> term = OBOObject("Term", id="foo:bar", name="Foo bar")
9    >>> print term
10    [Term]
11    id: foo:bar
12    name: Foo bar
13   
14    >>> ontology = OBOOntology()
15    >>> ontology.addObject(term)
16   
17"""
18
19from itertools import chain
20from collections import defaultdict
21import itertools
22
23builtinOBOObjects = [
24"""[Typedef]
25id: is_a
26name: is_a
27range: OBO:TERM_OR_TYPE
28domain: OBO:TERM_OR_TYPE
29definition: The basic subclassing relationship [OBO:defs]"""
30,
31"""[Typedef]
32id: disjoint_from
33name: disjoint_from
34range: OBO:TERM
35domain: OBO:TERM
36definition: Indicates that two classes are disjoint [OBO:defs]"""
37,
38"""[Typedef]
39id: instance_of
40name: instance_of
41range: OBO:TERM
42domain: OBO:INSTANCE
43definition: Indicates the type of an instance [OBO:defs]"""
44,
45"""[Typedef]
46id: inverse_of
47name: inverse_of
48range: OBO:TYPE
49domain: OBO:TYPE
50definition: Indicates that one relationship type is the inverse of another [OBO:defs]"""
51,
52"""[Typedef]
53id: union_of
54name: union_of
55range: OBO:TERM
56domain: OBO:TERM
57definition: Indicates that a term is the union of several others [OBO:defs]"""
58,
59"""[Typedef]
60id: intersection_of
61name: intersection_of
62range: OBO:TERM
63domain: OBO:TERM
64definition: Indicates that a term is the intersection of several others [OBO:defs]"""
65]
66
67def _splitAndStrip(string, sep):
68    head, tail = string.split(sep, 1)
69    return head.rstrip(" "), tail.lstrip(" ")
70
71
72class OBOObject(object):
73    """ Represents a generic OBO object (e.g. Term, Typedef, Instance, ...)
74    Example::
75        >>> term = OBOObject(stanzaType="Term", id="FOO:001", name="bar")
76    """
77    def __init__(self, stanzaType="Term", **kwargs):
78        """ Init from keyword arguments.
79        Example::
80            >>> term = OBOObject(stanzaType="Term", id="FOO:001", name="bar", def_="Example definition {modifier=frob} ! Comment")
81            >>> term = OBOObject(stanzaType="Term", id="FOO:001", name="bar", def_=("Example definition", [("modifier", "frob")], "Comment"))
82            >>> term = OBOObject(stanzaType="Term", id="FOO:001", name="bar", def_=("Example definition", [("modifier", "frob")])) # without the comment
83            >>> term = OBOObject(stanzaType="Term", id="FOO:001", name="bar", def_=("Example definition",)) # without the modifiers and comment
84        """
85        self.stanzaType = stanzaType
86       
87        self.modifiers = []
88        self.comments = []
89        self.tagValues = []
90        self.values = {}
91       
92        sortedTags = sorted(kwargs.iteritems(), key=lambda key_val: chr(1) if key_val[0] == "id" else key_val[0])
93        for tag, value in sortedTags:
94            if isinstance(value, basestring):
95                tag, value, modifiers, comment = self.parseTagValue(self.name_demangle(tag), value)
96            elif isinstance(value, tuple):
97                tag, value, modifiers, comment = ((self.name_demangle(tag),) + value + (None, None))[:4]
98            self.addTag(tag, value, modifiers, comment)
99       
100        self.related = set()
101        self.relatedTo = set()
102           
103    @property
104    def is_annonymous(self):
105        value = self.getValue("is_annonymous")
106        return bool(value)
107   
108    def name_mangle(self, tag):
109        """ Mangle tag name if it conflicts with python keyword
110        Example::
111            >>> term.name_mangle("def"), term.name_mangle("class")
112            ('def_', 'class_')
113        """
114        if tag in ["def", "class", "in", "not"]:
115            return tag + "_"
116        else:
117            return tag
118       
119    def name_demangle(self, tag):
120        """ Reverse of name_mangle
121        """
122        if tag in ["def_", "class_", "in_", "not_"]:
123            return tag[:-1]
124        else:
125            return tag
126       
127    def addTag(self, tag, value, modifiers=None, comment=None):
128        """ Add `tag`, `value` pair to the object with optional modifiers and
129        comment.
130        Example::
131            >>> term = OBOObject("Term")
132            >>> term.addTag("id", "FOO:002", comment="This is an id")
133            >>> print term
134            [Term]
135            id: FOO:002 ! This is an id
136             
137        """
138        self.tagValues.append((tag, value))
139        self.modifiers.append(modifiers)
140        self.comments.append(comment)
141        self.values.setdefault(tag, []).append(value)
142       
143        #  TODO: fix multiple tags grouping
144        if hasattr(self, tag):
145            if isinstance(getattr(self, tag), list):
146                getattr(self, tag).append(value)
147            else:
148                setattr(self, tag, [getattr(self, tag)] + [value])
149        else:
150            setattr(self, self.name_mangle(tag), value)
151           
152    def update(self, other):
153        """ Update the term with tag value pairs from `other`
154        (a OBOObject instance). The tag value pairs are appended
155        to the end except for the `id` tag.
156        """ 
157        for (tag, value), modifiers, comment in zip(other.tagValues, other.modifiers, other.comments):
158            if tag != "id":
159                self.addTag(tag, value, modifiers, comment)
160       
161    def getValue(self, tag, group=True):
162        if group:
163            pairs = [pair for pair in self.tagValues if pair[0] == tag]
164            return pairs
165        else:
166            tag = self.name_mangle(tag)
167            if tag in self.__dict__:
168                return self.__dict__[tag]
169            else:
170                raise ValueError("No value for tag: %s" % tag)
171       
172    def tagCount(self):
173        """ Retrun the number of tags in this object
174        """
175        return len(self.tagValues)
176   
177    def tags(self):
178        """ Retrun an iterator over the (tag, value) pairs.
179        """
180        for i in range(self.tagCount()):
181            yield self.tagValues[i] + (self.modifiers[i], self.comments[i])
182       
183    def formatSingleTag(self, index):
184        """Return a formated string representing index-th tag pair value
185        Example::
186            >>> term = OBOObject("Term", id="FOO:001", name="bar", def_="Example definition {modifier=frob} ! Comment")
187            >>> term.formatSingleTag(0)
188            'id: FOO:001'
189            >>> term.formatSingleTag(1)
190            'def: Example definition { modifier=frob } ! Comment'
191        """
192        tag, value = self.tagValues[index]
193        modifiers = self.modifiers[index]
194        comment = self.comments[index]
195        res = ["%s: %s" % (tag, value)]
196        if modifiers:
197            res.append("{ %s }" % modifiers)
198        if comment:
199            res.append("! " + comment)
200        return " ".join(res)
201   
202    def formatStanza(self):
203        """ Return a string stanza representation of this object
204        """
205        stanza = ["[%s]" % self.stanzaType]
206        for i in range(self.tagCount()):
207            stanza.append(self.formatSingleTag(i))
208        return "\n".join(stanza)
209           
210    @classmethod     
211    def parseStanza(cls, stanza):
212        r""" Parse and return an OBOObject instance from a single stanza.
213        Example::
214            >>> term = OBOObject.parseStanza("[Term]\nid: FOO:001\nname:bar")
215            >>> print term.id, term.name
216            FOO:001 bar
217           
218        """
219        lines = stanza.splitlines()
220        stanzaType = lines[0].strip("[]")
221        tag_values = []
222        for line in lines[1:]:
223            if ":" in line:
224                tag_values.append(cls.parseTagValue(line))
225       
226        obo = OBOObject(stanzaType)
227        for i, (tag, value, modifiers, comment) in enumerate(tag_values):
228#            print tag, value, modifiers, comment
229            obo.addTag(tag, value, modifiers, comment)
230        return obo
231   
232       
233    @classmethod
234    def parseTagValue(cls, tagValuePair, *args):
235        """ Parse and return a four-tuple containing a tag, value, a list of modifier pairs, comment.
236        If no modifiers or comments are present the corresponding entries will be None.
237       
238        Example::
239            >>> OBOObject.parseTagValue("foo: bar {modifier=frob} ! Comment")
240            ('foo', 'bar', 'modifier=frob', 'Comment')
241            >>> OBOObject.parseTagValue("foo: bar")
242            ('foo', 'bar', None, None)
243            >>> #  Can also pass tag, value pair already split   
244            >>> OBOObject.parseTagValue("foo", "bar {modifier=frob} ! Comment")
245            ('foo', 'bar', 'modifier=frob', 'Comment')
246        """
247        if args and ":" not in tagValuePair:
248            tag, rest = tagValuePair, args[0]
249        else:
250            tag, rest = _splitAndStrip(tagValuePair, ":")
251        value, modifiers, comment = None, None, None
252       
253        if "{" in rest:
254            value, rest = _splitAndStrip(rest, "{",)
255            modifiers, rest = _splitAndStrip(rest, "}")
256        if "!" in rest:
257            if value is None:
258                value, comment = _splitAndStrip(rest, "!")
259            else:
260                _, comment = _splitAndStrip(rest, "!")
261        if value is None:
262            value = rest
263           
264        if modifiers is not None:
265            modifiers = modifiers #TODO: split modifiers in a list
266           
267        return tag, value, modifiers, comment
268         
269    def GetRelatedObjects(self):
270        """ Obsolete. Use `relatedObjects()` instead
271        """
272        return self.relatedObjects()
273   
274    def relatedObjects(self):
275        """ Return a list of tuple pairs where the first element is relationship (typedef id)
276        is and the second object id whom the relationship applies to.
277        """
278        result = [(typeId, id) for typeId in ["is_a"] for id in self.values.get(typeId, [])] ##TODO add other defined Typedef ids
279        result = result + [tuple(r.split(None, 1)) for r in self.values.get("relationship", [])]
280        return result
281
282    def __repr__(self):
283        """ Return a string representation of the object in OBO format
284        """
285        return self.formatStanza()
286
287    def __iter__(self):
288        """ Iterates over sub terms
289        """
290        for typeId, id in self.relatedObjects():
291            yield (typeId, id)
292       
293class Term(OBOObject):
294    def __init__(self, *args, **kwargs):
295        OBOObject.__init__(self, "Term", *args, **kwargs)
296
297class Typedef(OBOObject):
298    def __init__(self, *args, **kwargs):
299        OBOObject.__init__(self, "Typedef", *args, **kwargs)
300
301class Instance(OBOObject):
302    def __init__(self, *args, **kwargs):
303        OBOObject.__init__(self, "Instance", *args, **kwargs)
304
305import re
306
307class OBOOntology(object):
308    _RE_TERM = re.compile(r"\[.+?\].*?\n\n", re.DOTALL)
309    _RE_HEADER = re.compile(r"^[^[].*?\n\[", re.DOTALL)
310    BUILTINS = builtinOBOObjects
311   
312    def __init__(self):
313        """ Init an empty Ontology.
314       
315        .. note:: Use parseOBOFile to load from a file
316       
317        """
318        self.objects = []
319        self.headerTags = []
320        self.id2Term = {}
321       
322    def addObject(self, object):
323        """ Add OBOObject instance to  this ontology.
324        """
325        if object.id in self.id2Term:
326            raise ValueError("OBOObject with id: %s already in the ontology" % object.id)
327        self.objects.append(object)
328        self.id2Term[object.id] = object
329       
330    def addHeaderTag(self, tag, value):
331        """ Add header tag, value pair to this ontology
332        """
333        self.headerTags.append((tag, value))
334       
335#    @classmethod
336#    def parseOBOFile(cls, file):
337#        """ Parse the .obo file and return an OBOOntology instance
338#        Example::
339#            >>> OBOOntology.parseOBOFile(open("dictyostelium_anatomy.obo", "rb"))
340#            <obiOntology.OBOOntology object at ...>
341#        """
342#        ontology = OBOOntology()
343#        data = file.read()
344#       
345#        header = data[:data.index("\n[")]
346#        for line in header.splitlines():
347#            if line.strip():
348#                ontology.addHeaderTag(*line.split(":", 1))
349#       
350#        imports = [value for  tag, value in ontology.headerTags if tag == "import"]
351#       
352#        terms = cls.BUILTINS + cls._RE_TERM.findall(data)
353#        for term in terms:
354#            term = OBOObject.parseStanza(term)
355#            ontology.addObject(term)
356#           
357#        while imports:
358#            url = imports.pop(0)
359#            imported = self.parseOBOFile(open(url, "rb"))
360#            ontology.update(imported)
361#        return ontology
362   
363    @classmethod
364    def parseOBOFile(cls, file):
365        """ Parse the .obo file and return an OBOOntology instance
366        Example::
367            >>> OBOOntology.parseOBOFile(open("dictyostelium_anatomy.obo", "rb"))
368            <obiOntology.OBOOntology object at ...>
369        """ 
370        ontology = OBOOntology()
371        data = file.read()
372        header = data[: data.index("\n[")]
373        body = data[data.index("\n[") + 1:]
374        for line in header.splitlines():
375            if line.strip():
376                ontology.addHeaderTag(*line.split(":", 1))
377               
378        current = None
379        #  For speed make these functions local
380        startswith = str.startswith
381        endswith = str.endswith
382        parseTagValue = OBOObject.parseTagValue
383       
384        builtins = "\n\n".join(cls.BUILTINS)
385        for line in itertools.chain(builtins.splitlines(), body.splitlines()):
386#            line = line.strip()
387            if startswith(line, "[") and endswith(line, "]"):
388                current = OBOObject(line.strip("[]"))
389            elif startswith(line, "!"):
390                pass #  comment
391            elif line:
392                current.addTag(*parseTagValue(line))
393            else: #  empty line is the end of a term
394                ontology.addObject(current)
395        if current.id not in ontology:
396            ontology.addObject(current)
397        imports = [value for  tag, value in ontology.headerTags if tag == "import"]
398       
399        while imports:
400            url = imports.pop(0)
401            imported = self.parseOBOFile(open(url, "rb"))
402            ontology.update(imported)
403        return ontology
404       
405   
406    def update(self, other):
407        """ Update this ontology with the terms from another.
408        """
409        for term in other:
410            if term.id in self:
411                if not term.is_annonymous:
412                    self.term(term.id).update(term)
413                else: #  Do nothing
414                    pass 
415            else:
416                self.addObject(term)
417               
418    def _postLoadProcess(self):
419        for obj in self.objects:
420            pass
421   
422    def term(self, id):
423        """ Return the OBOObject associated with this id.
424        """
425        if id in self.id2Term:
426            return self.id2Term[id]
427        else:
428            raise ValueError("Unknown term id: %s" % id)
429       
430    def terms(self):
431        """ Return all `Term` instances in the ontology.
432        """
433        return [obj for obj in self.objects if obj.stanzaType == "Term"]
434   
435    def typedefs(self):
436        """ Return all `Typedef` instances in the ontology.
437        """
438        return [obj for obj in self.objects if obj.stanzaType == "Typedef"]
439   
440    def instances(self):
441        """ Return all `Instance` instances in the ontology.
442        """
443        return [obj for obj in self.objects if obj.stanzaType == "Instance"]
444       
445    def relatedTerms(self, term):
446        """ Return a list of (rel_type, term_id) tuples where rel_type is
447        relationship type (e.g. 'is_a', 'has_part', ...) and term_id is the
448        id of the term in the relationship.
449        """
450        term = self.term(term) if not isinstance(term, OBOObject) else term
451        related = [(tag, value) for tag in ["is_a"] for value in term.values.get(tag, [])] #TODO: add other typedef ids
452        relationships = term.values.get("relationship", [])
453        for rel in relationships:
454            related.append(tuple(rel.split(None, 1)))
455        return related
456           
457    def toNetwork(self):
458        """ Return a orngNetwork instance constructed from this ontology
459        """
460        edgeTypes = self.edgeTypes()
461        terms = self.terms()
462        import orngNetwork, orange
463       
464        network = orngNetwork.Network(len(terms), True, len(edgeTypes))
465        network.objects = dict([(term.id, i) for i, term in enumerate(terms)])
466       
467        edges = defaultdict(set)
468        for term in self.terms():
469#            related = term.relatedTerms()
470            related = self.relatedTerms(term)
471            for relType, relTerm in related:
472                edges[(term.id, relTerm)].add(relType)
473               
474        edgeitems = edges.items()
475        for (src, dst), eTypes in edgeitems:
476            network[src, dst] = [1 if e in eTypes else 0 for e in edgeTypes]
477           
478        domain = orange.Domain([orange.StringVariable("id"),
479                                orange.StringVariable("name"),
480                                orange.StringVariable("def"),
481                                ], False)
482       
483        items = orange.ExampleTable(domain)
484        for term in terms:
485            ex = orange.Example(domain, [term.id, term.name, term.values.get("def", [""])[0]])
486            items.append(ex)
487       
488        relationships = set([", ".join(sorted(eTypes)) for (_, _), eTypes in edgeitems])
489        domain = orange.Domain([orange.FloatVariable("u"),
490                                orange.FloatVariable("v"),
491                                orange.EnumVariable("relationship", values=list(edgeTypes))
492                                ], False)
493       
494        id2index = dict([(term.id, i + 1) for i, term in enumerate(terms)])
495        links = orange.ExampleTable(domain)
496        for (src, dst), eTypes in edgeitems:
497            ex = orange.Example(domain, [id2index[src], id2index[dst], eTypes.pop()])
498            links.append(ex)
499           
500        network.items = items
501        network.links = links
502        network.optimization = None
503        return network
504       
505    def edgeTypes(self):
506        """ Return a list of all edge types in the ontology
507        """
508        return [obj.id for obj in self.objects if obj.stanzaType == "Typedef"]
509       
510    def extractSuperGraph(self, terms):
511        """ Return all super terms of terms up to the most general one.
512        """
513        terms = [terms] if type(terms) == str else terms
514        visited = set()
515        queue = set(terms)
516        while queue:
517            term = queue.pop()
518            visited.add(term)
519            queue.update(set(id for typeId, id in self[term].related) - visited)
520        return visited
521   
522    def __len__(self):
523        return len(self.objects)
524   
525    def __iter__(self):
526        return iter(self.objects)
527   
528    def __contains__(self, obj):
529        return obj in self.id2Term
530   
531def foundryOntologies():
532    """ List ontologies available from the OBOFoundry website
533    (`http://www.obofoundry.org/`_)
534    Example::
535        >>> foundryOntologies()
536        [('Biological process', 'http://obo.cvs.sourceforge.net/*checkout*/obo/obo/ontology/genomic-proteomic/gene_ontology_edit.obo'), ...
537   
538    """
539    import urllib2, re
540    stream = urllib2.urlopen("http://www.obofoundry.org/")
541    text = stream.read()
542    pattern = r'<td class=".+?">\s*<a href=".+?">(.+?)</a>\s*</td>\s*<td class=".+?">.*?</td>\s*<td class=".+?">.*?</td>\s*?<td class=".+?">\s*<a href="(.+?obo)">.+?</a>'
543    return re.findall(pattern, text)
544   
545   
546if __name__ == "__main__":
547    import doctest
548    stanza = '''[Term]
549id: FOO:001
550name: bar
551    '''
552    term = OBOObject.parseStanza(stanza)
553    doctest.testmod(extraglobs={"stanza": stanza, "term": term}, optionflags=doctest.ELLIPSIS)
554       
Note: See TracBrowser for help on using the repository browser.