source: orange-bioinformatics/_bioinformatics/obiOntology.py @ 1742:fa3a9e4af7e6

Revision 1742:fa3a9e4af7e6, 30.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Code style and docstring fixes.

RevLine 
[1742]1"""
2===========
[1349]3obiOntology
[1742]4===========
[1349]5
[1742]6This module provides an interface for parsing, creating and manipulating of
7OBO ontologies (http://www.obofoundry.org/)
[1326]8
[1742]9Construct an ontology from scratch with custom terms ::
[1326]10
11    >>> term = OBOObject("Term", id="foo:bar", name="Foo bar")
12    >>> print term
13    [Term]
14    id: foo:bar
15    name: Foo bar
[1742]16
[1326]17    >>> ontology = OBOOntology()
[1349]18    >>> ontology.add_object(term)
[1355]19    >>> ontology.add_header_tag("created-by", "ales") # add a header tag
20    >>> from StringIO import StringIO
21    >>> buffer = StringIO()
22    >>> ontology.dump(buffer) # Save the ontology to a file like object
23    >>> print buffer.getvalue() # Print the contents of the buffer
24    created-by: ales
25    <BLANKLINE>
26    [Term]
27    id: foo:bar
28    name: Foo bar
29    <BLANKLINE>
[1742]30
31To load an ontology from a file pass the file or filename to the
32:class:`OBOOntology` constructor or call its load method ::
[1355]33
34    >>> buffer.seek(0) # rewind
35    >>> ontology = OBOOntology(buffer)
[1742]36    >>> # Or equivalently
[1355]37    >>> buffer.seek(0) # rewind
38    >>> ontology = OBOOntology()
39    >>> ontology.load(buffer)
[1742]40
41
42See http://www.geneontology.org/GO.format.obo-1_2.shtml for the definition
43of the .obo file format.
[1355]44
[1326]45"""
46
[1742]47import re
48import urllib2
49import warnings
[1326]50from collections import defaultdict
[1349]51from StringIO import StringIO
[1326]52
[1742]53
54#: These are builtin OBO objects present in any ontology by default.
[1349]55BUILTIN_OBO_OBJECTS = [
[1326]56"""[Typedef]
57id: is_a
58name: is_a
59range: OBO:TERM_OR_TYPE
60domain: OBO:TERM_OR_TYPE
[1742]61definition: The basic subclassing relationship [OBO:defs]""",
62
[1326]63"""[Typedef]
64id: disjoint_from
65name: disjoint_from
66range: OBO:TERM
67domain: OBO:TERM
[1742]68definition: Indicates that two classes are disjoint [OBO:defs]""",
69
[1326]70"""[Typedef]
71id: instance_of
72name: instance_of
73range: OBO:TERM
74domain: OBO:INSTANCE
[1742]75definition: Indicates the type of an instance [OBO:defs]""",
76
[1326]77"""[Typedef]
78id: inverse_of
79name: inverse_of
80range: OBO:TYPE
81domain: OBO:TYPE
[1742]82definition: Indicates that one relationship type is the inverse of another [OBO:defs]""",
83
[1326]84"""[Typedef]
85id: union_of
86name: union_of
87range: OBO:TERM
88domain: OBO:TERM
[1742]89definition: Indicates that a term is the union of several others [OBO:defs]""",
90
[1326]91"""[Typedef]
92id: intersection_of
93name: intersection_of
94range: OBO:TERM
95domain: OBO:TERM
96definition: Indicates that a term is the intersection of several others [OBO:defs]"""
97]
[1742]98
99
[1349]100def _split_and_strip(string, sep):
[1742]101    """
102    Split the `string` by separator `sep` in to two parts and strip
103    any whitespace between the inner parts.
104
105    """
[1326]106    head, tail = string.split(sep, 1)
107    return head.rstrip(" "), tail.lstrip(" ")
108
109
110class OBOObject(object):
[1742]111    """
112    Represents a generic OBO object (e.g. Term, Typedef, Instance, ...)
[1326]113    Example::
[1742]114
[1349]115        >>> term = OBOObject(stanza_type="Term", id="FOO:001", name="bar")
[1742]116
117        >>> term = OBOObject(
118        ...     stanza_type="Term",
119        ...     id="FOO:001",
120        ...     name="bar",
121        ...     def_="Example definition { modifier=frob } ! Comment"
122        ... )
123        ...
124
125    An alternative way to specify tags in the constructor::
126
127        >>> term = OBOObject(stanza_type="Term", id="FOO:001", name="bar",
128        ...                  def_=("Example definition",
129        ...                        [("modifier", "frob")],
130        ...                        "Comment"))
131        ...
132
133    .. note::
134        Note the use of ``def_`` to define the 'def' tag. This is to
135        avoid the name clash with the python's ``def`` keyword.
136
[1326]137    """
[1349]138    def __init__(self, stanza_type="Term", **kwargs):
[1742]139        """
140        Initialize from keyword arguments.
[1326]141        """
[1349]142        self.stanza_type = stanza_type
[1742]143
[1326]144        self.modifiers = []
145        self.comments = []
[1349]146        self.tag_values = []
[1326]147        self.values = {}
[1742]148
149        sorted_tags = sorted(
150            kwargs.iteritems(),
151            key=lambda key_val: chr(1) if key_val[0] == "id" else key_val[0]
152        )
153
[1349]154        for tag, value in sorted_tags:
[1326]155            if isinstance(value, basestring):
[1742]156                tag, value, modifiers, comment = \
157                    self.parse_tag_value(self.name_demangle(tag), value)
[1326]158            elif isinstance(value, tuple):
[1742]159                tag, value, modifiers, comment = \
160                    ((self.name_demangle(tag),) + value + (None, None))[:4]
[1349]161            self.add_tag(tag, value, modifiers, comment)
[1742]162
[1326]163        self.related = set()
[1742]164
[1326]165    @property
166    def is_annonymous(self):
[1742]167        """
168        Is this object anonymous.
169        """
[1349]170        value = self.get_value("is_annonymous")
[1326]171        return bool(value)
[1742]172
[1326]173    def name_mangle(self, tag):
[1742]174        """
175        Mangle tag name if it conflicts with python keyword.
176
[1326]177        Example::
[1742]178
[1326]179            >>> term.name_mangle("def"), term.name_mangle("class")
180            ('def_', 'class_')
[1742]181
[1326]182        """
183        if tag in ["def", "class", "in", "not"]:
184            return tag + "_"
185        else:
186            return tag
[1742]187
[1326]188    def name_demangle(self, tag):
[1742]189        """
190        Reverse of `name_mangle`.
[1326]191        """
192        if tag in ["def_", "class_", "in_", "not_"]:
193            return tag[:-1]
194        else:
195            return tag
[1742]196
[1349]197    def add_tag(self, tag, value, modifiers=None, comment=None):
[1742]198        """
199        Add `tag`, `value` pair to the object with optional modifiers and
[1326]200        comment.
[1742]201
[1326]202        Example::
[1742]203
[1326]204            >>> term = OBOObject("Term")
[1349]205            >>> term.add_tag("id", "FOO:002", comment="This is an id")
[1326]206            >>> print term
207            [Term]
208            id: FOO:002 ! This is an id
[1742]209
[1326]210        """
[1742]211        tag = intern(tag)  # a small speed and memory benefit
[1349]212        self.tag_values.append((tag, value))
[1326]213        self.modifiers.append(modifiers)
214        self.comments.append(comment)
215        self.values.setdefault(tag, []).append(value)
[1742]216
[1326]217        #  TODO: fix multiple tags grouping
218        if hasattr(self, tag):
219            if isinstance(getattr(self, tag), list):
220                getattr(self, tag).append(value)
221            else:
222                setattr(self, tag, [getattr(self, tag)] + [value])
223        else:
224            setattr(self, self.name_mangle(tag), value)
[1742]225
[1326]226    def update(self, other):
[1742]227        """
228        Update the term with tag value pairs from `other`
229        (:class:`OBOObject`). The tag value pairs are appended to the
230        end except for the `id` tag.
231
232        """
233        for (tag, value), modifiers, comment in \
234                zip(other.tag_values, other.modifiers, other.comments):
[1326]235            if tag != "id":
[1349]236                self.add_tag(tag, value, modifiers, comment)
[1742]237
[1349]238    def get_value(self, tag, group=True):
[1326]239        if group:
[1349]240            pairs = [pair for pair in self.tag_values if pair[0] == tag]
[1326]241            return pairs
242        else:
243            tag = self.name_mangle(tag)
244            if tag in self.__dict__:
245                return self.__dict__[tag]
246            else:
247                raise ValueError("No value for tag: %s" % tag)
[1742]248
[1349]249    def tag_count(self):
[1742]250        """
251        Return the number of tags in this object.
[1326]252        """
[1349]253        return len(self.tag_values)
[1742]254
[1326]255    def tags(self):
[1742]256        """
257        Return an iterator over the (tag, value) pairs.
[1326]258        """
[1349]259        for i in range(self.tag_count()):
260            yield self.tag_values[i] + (self.modifiers[i], self.comments[i])
[1742]261
[1349]262    def format_single_tag(self, index):
[1742]263        """
264        Return a formated string representing index-th tag pair value
265
[1326]266        Example::
[1742]267
268            >>> term = OBOObject(
269            ...     "Term", id="FOO:001", name="bar",
270            ...      def_="Example definition {modifier=frob} ! Comment")
271            ...
[1349]272            >>> term.format_single_tag(0)
[1326]273            'id: FOO:001'
[1349]274            >>> term.format_single_tag(1)
[1326]275            'def: Example definition { modifier=frob } ! Comment'
[1742]276
277        ..
278            Why by index, and not by tag?
279
[1326]280        """
[1349]281        tag, value = self.tag_values[index]
[1326]282        modifiers = self.modifiers[index]
283        comment = self.comments[index]
284        res = ["%s: %s" % (tag, value)]
285        if modifiers:
286            res.append("{ %s }" % modifiers)
287        if comment:
288            res.append("! " + comment)
289        return " ".join(res)
[1742]290
[1349]291    def format_stanza(self):
[1742]292        """
293        Return a string stanza representation of this object.
[1326]294        """
[1349]295        stanza = ["[%s]" % self.stanza_type]
296        for i in range(self.tag_count()):
297            stanza.append(self.format_single_tag(i))
[1326]298        return "\n".join(stanza)
[1742]299
300    @classmethod
[1349]301    def parse_stanza(cls, stanza):
[1742]302        r"""
303        Parse and return an OBOObject instance from a stanza string.
304
[1326]305        Example::
[1742]306
[1349]307            >>> term = OBOObject.parse_stanza("[Term]\nid: FOO:001\nname:bar")
[1326]308            >>> print term.id, term.name
309            FOO:001 bar
[1742]310
[1326]311        """
312        lines = stanza.splitlines()
[1349]313        stanza_type = lines[0].strip("[]")
[1326]314        tag_values = []
315        for line in lines[1:]:
316            if ":" in line:
[1349]317                tag_values.append(cls.parse_tag_value(line))
[1742]318
[1349]319        obo = OBOObject(stanza_type)
[1742]320        for tag, value, modifiers, comment in tag_values:
[1349]321            obo.add_tag(tag, value, modifiers, comment)
[1326]322        return obo
[1742]323
[1326]324    @classmethod
[1349]325    def parse_tag_value_1(cls, tag_value_pair, *args):
[1742]326        """
327        Parse and return a four-tuple containing a tag, value, a
328        list of modifier pairs, comment. If no modifiers or comments
329        are present the corresponding entries will be ``None``.
330
[1326]331        Example::
[1742]332
[1349]333            >>> OBOObject.parse_tag_value("foo: bar {modifier=frob} ! Comment")
[1326]334            ('foo', 'bar', 'modifier=frob', 'Comment')
[1349]335            >>> OBOObject.parse_tag_value("foo: bar")
[1326]336            ('foo', 'bar', None, None)
[1742]337            >>> # Can also pass tag, value pair already split
[1349]338            >>> OBOObject.parse_tag_value("foo", "bar {modifier=frob} ! Comment")
[1326]339            ('foo', 'bar', 'modifier=frob', 'Comment')
[1742]340
[1326]341        """
[1349]342        if args and ":" not in tag_value_pair:
343            tag, rest = tag_value_pair, args[0]
[1326]344        else:
[1349]345            tag, rest = _split_and_strip(tag_value_pair, ":")
[1326]346        value, modifiers, comment = None, None, None
[1742]347
[1326]348        if "{" in rest:
[1349]349            value, rest = _split_and_strip(rest, "{",)
350            modifiers, rest = _split_and_strip(rest, "}")
[1326]351        if "!" in rest:
352            if value is None:
[1349]353                value, comment = _split_and_strip(rest, "!")
[1326]354            else:
[1349]355                _, comment = _split_and_strip(rest, "!")
[1326]356        if value is None:
357            value = rest
[1742]358
[1326]359        if modifiers is not None:
[1742]360            modifiers = modifiers  # TODO: split modifiers in a list
361
[1326]362        return tag, value, modifiers, comment
[1742]363
[1349]364    _RE_TAG_VALUE = re.compile(r"^(?P<tag>.+?[^\\])\s*:\s*(?P<value>.+?)\s*(?P<modifiers>[^\\]{.+?[^\\]})?\s*(?P<comment>[^\\]!.*)?$")
365    _RE_VALUE = re.compile(r"^\s*(?P<value>.+?)\s*(?P<modifiers>[^\\]{.+?[^\\]})?\s*(?P<comment>[^\\]!.*)?$")
[1742]366
[1349]367    @classmethod
368    def parse_tag_value(cls, tag_value_pair, arg=None):
[1742]369        """
370        Parse and return a four-tuple containing a tag, value, a list
371        of modifier pairs, comment. If no modifiers or comments are
372        present the corresponding entries will be None.
373
[1349]374        Example::
375            >>> OBOObject.parse_tag_value("foo: bar {modifier=frob} ! Comment")
376            ('foo', 'bar', 'modifier=frob', 'Comment')
377            >>> OBOObject.parse_tag_value("foo: bar")
378            ('foo', 'bar', None, None)
[1742]379            >>> #  Can also pass tag, value pair already split
[1349]380            >>> OBOObject.parse_tag_value("foo", "bar {modifier=frob} ! Comment")
381            ('foo', 'bar', 'modifier=frob', 'Comment')
[1742]382
[1349]383        .. warning: This function assumes comment an modifiers are prefixed
[1742]384            with a whitespace i.e. 'tag: bla! comment' will be parsed
385            incorrectly!
386
[1349]387        """
[1742]388        if arg is not None:  # tag_value_pair is actually a tag only
[1349]389            tag = tag_value_pair
[1742]390            value, modifiers, comment = cls._RE_VALUE.findall(arg)[0]
[1349]391        else:
[1742]392            tag, value, modifiers, comment = \
393                cls._RE_TAG_VALUE.findall(tag_value_pair)[0]
[1349]394        none_if_empyt = lambda val: None if not val.strip() else val.strip()
395        modifiers = modifiers.strip(" {}")
396        comment = comment.lstrip(" !")
397        return (none_if_empyt(tag), none_if_empyt(value),
398                none_if_empyt(modifiers), none_if_empyt(comment))
[1742]399
[1349]400    def related_objects(self):
[1326]401        """
[1742]402        Return a list of tuple pairs where the first element is
403        relationship (typedef id) and the second object id whom the
404        relationship applies to.
405
406        """
407        result = [(type_id, id)
408                  for type_id in ["is_a"]  # TODO add other defined Typedef ids
409                  for id in self.values.get(type_id, [])]
410
411        result = result + [tuple(r.split(None, 1))
412                           for r in self.values.get("relationship", [])]
[1326]413        return result
414
415    def __repr__(self):
[1742]416        """
417        Return a string representation of the object in OBO format
[1326]418        """
[1349]419        return self.format_stanza()
[1326]420
421    def __iter__(self):
422        """
[1742]423        Iterates over sub terms
424        """
425        return iter(self.related_objects())
426
427
[1326]428class Term(OBOObject):
[1742]429    """
430    A 'Term' object in the ontology.
431    """
[1326]432    def __init__(self, *args, **kwargs):
433        OBOObject.__init__(self, "Term", *args, **kwargs)
434
[1742]435
[1326]436class Typedef(OBOObject):
[1742]437    """
438    A 'Typedef' object in the ontology.
439    """
[1326]440    def __init__(self, *args, **kwargs):
441        OBOObject.__init__(self, "Typedef", *args, **kwargs)
442
[1742]443
[1326]444class Instance(OBOObject):
[1742]445    """
446    An 'Instance' object in the ontology
447    """
[1326]448    def __init__(self, *args, **kwargs):
449        OBOObject.__init__(self, "Instance", *args, **kwargs)
450
[1349]451
452class OBOParser(object):
453    r""" A simple parser for .obo files (inspired by xml.dom.pulldom)
[1742]454
[1349]455    Example::
[1742]456
[1349]457        >>> from StringIO import StringIO
[1742]458        >>> file = StringIO("header_tag: header_value\n[Term]\nid: "
459        ...                 "FOO { modifier=bar } ! comment\n\n")
460        ...
[1349]461        >>> parser = OBOParser(file)
462        >>> for event, value in parser:
463        ...     print event, value
[1742]464        ...
[1355]465        HEADER_TAG ['header_tag', 'header_value']
[1349]466        START_STANZA Term
467        TAG_VALUE ('id', 'FOO', 'modifier=bar', 'comment')
468        CLOSE_STANZA None
[1742]469
[1349]470    """
471    def __init__(self, file):
472        self.file = file
[1742]473
[1349]474    def parse(self, progress_callback=None):
[1742]475        """
476        Parse the file and yield parse events.
477
478        .. todo List events and values
479
[1326]480        """
[1349]481        data = self.file.read()
[1326]482        header = data[: data.index("\n[")]
483        body = data[data.index("\n[") + 1:]
484        for line in header.splitlines():
485            if line.strip():
[1355]486                yield "HEADER_TAG", line.split(": ", 1)
[1742]487
[1326]488        current = None
489        #  For speed make these functions local
490        startswith = str.startswith
491        endswith = str.endswith
[1349]492        parse_tag_value = OBOObject.parse_tag_value
[1742]493
[1349]494        for line in body.splitlines():
[1326]495            if startswith(line, "[") and endswith(line, "]"):
[1349]496                yield "START_STANZA", line.strip("[]")
497                current = line
[1326]498            elif startswith(line, "!"):
[1349]499                yield "COMMENT", line[1:]
[1326]500            elif line:
[1349]501                yield "TAG_VALUE", parse_tag_value(line)
[1742]502            else:  # empty line is the end of a term
[1349]503                yield "CLOSE_STANZA", None
504                current = None
505        if current is not None:
506            yield "CLOSE_STANZA", None
[1742]507
[1349]508    def __iter__(self):
509        """
[1742]510        Iterate over parse events (same as parse())
511        """
512        return self.parse()
513
514
[1349]515class OBOOntology(object):
[1355]516    """
[1742]517    An class for representing OBO ontologies.
518
519    :param file-like file:
520        A optional file like object describing the ontology in obo format.
521
522    """
523
[1349]524    BUILTINS = BUILTIN_OBO_OBJECTS
[1742]525
[1349]526    def __init__(self, file=None):
[1742]527        """
528        Initialize an ontology instance from a file like object (.obo format)
[1349]529        """
530        self.objects = []
531        self.header_tags = []
532        self.id2term = {}
533        self.alt2id = {}
534        self._resolved_imports = []
535        self._invalid_cache_flag = False
536        self._related_to = {}
[1742]537
[1349]538        # First load the built in OBO objects
539        builtins = StringIO("\n" + "\n\n".join(self.BUILTINS) + "\n")
540        self.load(builtins)
541        if file:
542            self.load(file)
[1742]543
[1349]544    def add_object(self, object):
[1742]545        """
546        Add an :class:`OBOObject` instance to this ontology.
[1349]547        """
548        if object.id in self.id2term:
[1742]549            raise ValueError("OBOObject with id: %s already in "
550                             "the ontology" % object.id)
[1349]551        self.objects.append(object)
552        self.id2term[object.id] = object
553        self._invalid_cache_flag = True
[1742]554
[1349]555    def add_header_tag(self, tag, value):
[1742]556        """
557        Add header tag, value pair to this ontology.
[1349]558        """
559        self.header_tags.append((tag, value))
[1742]560
[1349]561    def load(self, file, progress_callback=None):
[1742]562        """
563        Load terms from a file.
564
565        :param file-like file:
566            A file-like like object describing the ontology in obo format.
567        :param function progress_callback:
568            An optional function callback to report on the progress.
569
[1349]570        """
571        if isinstance(file, basestring):
572            file = open(file, "rb")
[1742]573
[1349]574        parser = OBOParser(file)
575        current = None
576        for event, value in parser.parse(progress_callback=progress_callback):
577            if event == "TAG_VALUE":
578                current.add_tag(*value)
579            elif event == "START_STANZA":
580                current = OBOObject(value)
581            elif event == "CLOSE_STANZA":
582                self.add_object(current)
583                current = None
584            elif event == "HEADER_TAG":
585                self.add_header_tag(*value)
586            elif event != "COMMENT":
[1742]587                raise Exception("Parse Error! Unknown parse "
588                                "event {0}".format(event))
589
590        imports = [value for tag, value in self.header_tags
591                   if tag == "import"]
592
593        if imports:
594            warnings.warn("Import header tags are not supported")
595
596#        while imports:
597#            url = imports.pop(0)
598#            if uri not in self._resolved_imports:
599#                imported = self.parse_file(open(url, "rb"))
600#                ontology.update(imported)
601#                self._resolved_imports.append(uri)
602
[1349]603    def dump(self, file):
[1742]604        """
605        Dump the contents of the ontology to a `file` in .obo format.
606
607        :param file-like file:
608            A file like object.
609
[1349]610        """
611        if isinstance(file, basestring):
612            file = open(file, "wb")
[1742]613
[1349]614        for key, value in self.header_tags:
[1355]615            file.write(key + ": " + value + "\n")
[1742]616
[1355]617        # Skip the builtins
618        for object in self.objects[len(self.BUILTINS):]:
[1349]619            file.write("\n")
620            file.write(object.format_stanza())
621            file.write("\n")
[1742]622
[1326]623    def update(self, other):
[1742]624        """
625        Update this ontology with the terms from `other`.
[1326]626        """
627        for term in other:
628            if term.id in self:
629                if not term.is_annonymous:
630                    self.term(term.id).update(term)
[1742]631                else:  # Do nothing
632                    pass
[1326]633            else:
[1349]634                self.add_object(term)
635        self._invalid_cache_flag = True
[1742]636
[1349]637    def _cache_validate(self, force=False):
[1742]638        """
639        Update the relations cache if `self._invalid_cache` flag is set.
[1349]640        """
641        if self._invalid_cache_flag or force:
642            self._cache_relations()
[1742]643
[1349]644    def _cache_relations(self):
[1742]645        """
646        Collect all relations from parent to a child and store it in
[1349]647        `self._related_to` member.
[1742]648
[1349]649        """
650        related_to = defaultdict(list)
651        for obj in self.objects:
652            for rel_type, id in self.related_terms(obj):
653                term = self.term(id)
654                related_to[term].append((rel_type, obj))
[1742]655
[1349]656        self._related_to = related_to
657        self._invalid_cache_flag = False
[1742]658
[1326]659    def term(self, id):
[1742]660        """
661        Return the :class:`OBOObject` associated with this id.
662
663        :param str id:
664            Term id string.
665
[1326]666        """
[1349]667        if isinstance(id, basestring):
668            if id in self.id2term:
669                return self.id2term[id]
670            elif id in self.alt2id:
671                return self.id2term[self.alt2id[id]]
672            else:
[1531]673                raise ValueError("Unknown term id: %r" % id)
[1349]674        elif isinstance(id, OBOObject):
675            return id
[1742]676
[1326]677    def terms(self):
[1742]678        """
679        Return all :class:`Term` instances in the ontology.
[1326]680        """
[1349]681        return [obj for obj in self.objects if obj.stanza_type == "Term"]
[1742]682
[1531]683    def term_by_name(self, name):
[1742]684        """
685        Return the term with name `name`.
[1531]686        """
687        terms = [t for t in self.terms() if t.name == name]
688        if len(terms) != 1:
689            raise ValueError("Unknown term name: %r" % name)
690        return terms[0]
[1742]691
[1326]692    def typedefs(self):
[1742]693        """
694        Return all :class:`Typedef` instances in the ontology.
[1326]695        """
[1349]696        return [obj for obj in self.objects if obj.stanza_type == "Typedef"]
[1742]697
[1326]698    def instances(self):
[1742]699        """
700        Return all :class:`Instance` instances in the ontology.
[1326]701        """
[1349]702        return [obj for obj in self.objects if obj.stanza_type == "Instance"]
[1742]703
[1349]704    def related_terms(self, term):
[1742]705        """
706        Return a list of (`rel_type`, `term_id`) tuples where `rel_type` is
707        relationship type (e.g. 'is_a', 'has_part', ...) and `term_id` is
708        the id of the term in the relationship.
709
[1326]710        """
711        term = self.term(term) if not isinstance(term, OBOObject) else term
[1742]712        related = [(tag, value)
713                   for tag in ["is_a"]  # TODO: add other typedef ids
714                   for value in term.values.get(tag, [])]
[1326]715        relationships = term.values.get("relationship", [])
716        for rel in relationships:
717            related.append(tuple(rel.split(None, 1)))
718        return related
[1742]719
[1355]720    def edge_types(self):
[1742]721        """
722        Return a list of all edge types in the ontology.
[1355]723        """
724        return [obj.id for obj in self.objects if obj.stanza_type == "Typedef"]
[1742]725
[1355]726    def parent_edges(self, term):
[1742]727        """
728        Return a list of (rel_type, parent_term) tuples.
[1355]729        """
730        term = self.term(term)
731        parents = []
732        for rel_type, parent in self.related_terms(term):
733            parents.append((rel_type, self.term(parent)))
734        return parents
[1742]735
[1355]736    def child_edges(self, term):
[1742]737        """
738        Return a list of (rel_type, source_term) tuples.
[1355]739        """
740        self._cache_validate()
741        term = self.term(term)
742        return self._related_to.get(term, [])
[1742]743
[1355]744    def super_terms(self, term):
[1742]745        """
746        Return a set of all super terms of `term` up to the most general one.
[1355]747        """
748        terms = self.parent_terms(term)
749        visited = set()
750        queue = set(terms)
751        while queue:
752            term = queue.pop()
753            visited.add(term)
754            queue.update(self.parent_terms(term) - visited)
755        return visited
[1742]756
[1355]757    def sub_terms(self, term):
[1742]758        """
759        Return a set of all sub terms for `term`.
[1355]760        """
761        terms = self.child_terms(term)
762        visited = set()
763        queue = set(terms)
764        while queue:
765            term = queue.pop()
766            visited.add(term)
767            queue.update(self.child_terms(term) - visited)
768        return visited
[1742]769
[1355]770    def child_terms(self, term):
[1742]771        """
772        Return a set of all child terms for this `term`.
[1355]773        """
774        self._cache_validate()
775        term = self.term(term)
776        children = []
777        for rel_type, term in self.child_edges(term):
778            children.append(term)
779        return set(children)
[1742]780
[1355]781    def parent_terms(self, term):
[1742]782        """
783        Return a set of all parent terms for this `term`
[1355]784        """
785        term = self.term(term)
786        parents = []
[1742]787        for rel_type, id in self.parent_edges(term):
[1355]788            parents.append(self.term(id))
789        return set(parents)
[1742]790
[1355]791    def relations(self):
[1742]792        """
793        Return a list of all relations in the ontology.
[1355]794        """
795        relations = []
796        for obj in self.objects:
797            for type_id, id in  obj.related:
798                target_term = self.term(id)
799            relations.append((obj, type_id, target_term))
800        return relations
[1742]801
[1355]802    def __len__(self):
803        return len(self.objects)
[1742]804
[1355]805    def __iter__(self):
806        return iter(self.objects)
[1742]807
[1355]808    def __contains__(self, obj):
809        if isinstance(obj, basestring):
810            return obj in self.id2term
811        else:
812            return obj in self.objects
[1742]813
[1355]814    def __getitem__(self, key):
815        return self.id2term[key]
[1742]816
[1355]817    def has_key(self, key):
[1742]818        return key in self.id2term
819
[1531]820    def traverse_bf(self, term):
[1742]821        """
822        BF traverse of the ontology down from term.
[1531]823        """
824        queue = list(self.child_terms(term))
825        while queue:
826            term = queue.pop(0)
827            queue.extend(self.child_terms(term))
828            yield term
[1742]829
[1531]830    def traverse_df(self, term, depth=1e30):
[1742]831        """
832        DF traverse of the ontology down from term.
[1531]833        """
834        if depth >= 1:
835            for child in self.child_terms(term):
836                yield child
[1742]837                for t in self.traverse_df(child, depth - 1):
[1531]838                    yield t
[1742]839
[1355]840    def to_network(self, terms=None):
[1742]841        """
842        Return an Orange.network.Network instance constructed from
[1349]843        this ontology.
[1742]844
[1326]845        """
[1349]846        edge_types = self.edge_types()
[1326]847        terms = self.terms()
[1632]848        from Orange.orng import orngNetwork
849        import orange
[1742]850
[1349]851        network = orngNetwork.Network(len(terms), True, len(edge_types))
[1326]852        network.objects = dict([(term.id, i) for i, term in enumerate(terms)])
[1742]853
[1326]854        edges = defaultdict(set)
855        for term in self.terms():
[1349]856            related = self.related_terms(term)
[1326]857            for relType, relTerm in related:
858                edges[(term.id, relTerm)].add(relType)
[1742]859
[1326]860        edgeitems = edges.items()
861        for (src, dst), eTypes in edgeitems:
[1349]862            network[src, dst] = [1 if e in eTypes else 0 for e in edge_types]
[1742]863
[1326]864        domain = orange.Domain([orange.StringVariable("id"),
865                                orange.StringVariable("name"),
866                                orange.StringVariable("def"),
867                                ], False)
[1742]868
[1326]869        items = orange.ExampleTable(domain)
870        for term in terms:
871            ex = orange.Example(domain, [term.id, term.name, term.values.get("def", [""])[0]])
872            items.append(ex)
[1742]873
[1326]874        relationships = set([", ".join(sorted(eTypes)) for (_, _), eTypes in edgeitems])
875        domain = orange.Domain([orange.FloatVariable("u"),
876                                orange.FloatVariable("v"),
[1349]877                                orange.EnumVariable("relationship", values=list(edge_types))
[1326]878                                ], False)
[1742]879
[1326]880        id2index = dict([(term.id, i + 1) for i, term in enumerate(terms)])
881        links = orange.ExampleTable(domain)
882        for (src, dst), eTypes in edgeitems:
883            ex = orange.Example(domain, [id2index[src], id2index[dst], eTypes.pop()])
884            links.append(ex)
[1742]885
[1326]886        network.items = items
887        network.links = links
888        network.optimization = None
889        return network
[1742]890
[1355]891    def to_networkx(self, terms=None):
[1742]892        """
893        Return a NetworkX graph of this ontology.
[1349]894        """
[1355]895        import networkx
896        graph = networkx.Graph()
[1742]897
[1355]898        edge_types = self.edge_types()
[1742]899
[1355]900        edge_colors = {"is_a": "red"}
[1742]901
[1355]902        if terms is None:
903            terms = self.terms()
904        else:
905            terms = [self.term(term) for term in terms]
906            super_terms = [self.super_terms(term) for term in terms]
907            terms = reduce(set.union, super_terms, set(terms))
[1742]908
[1355]909        for term in terms:
910            graph.add_node(term.id, name=term.name)
[1742]911
[1355]912        for term in terms:
913            for rel_type, rel_term in self.related_terms(term):
914                rel_term = self.term(rel_term)
915                if rel_term in terms:
[1742]916                    graph.add_edge(term.id, rel_term.id, label=rel_type,
917                                   color=edge_colors.get(rel_type, "blue"))
918
[1355]919        return graph
[1742]920
[1355]921    def to_graphviz(self, terms=None):
[1742]922        """
923        Return an pygraphviz.AGraph representation of the ontology.
[1355]924        If `terms` is not `None` it must be a list of terms in the ontology.
925        The graph will in this case contain only the super graph of those
[1742]926        terms.
927
[1326]928        """
[1355]929        import pygraphviz as pgv
930        graph = pgv.AGraph(directed=True, name="ontology")
[1742]931
[1355]932        edge_types = self.edge_types()
[1742]933
[1355]934        edge_colors = {"is_a": "red"}
[1742]935
[1355]936        if terms is None:
937            terms = self.terms()
[1349]938        else:
[1355]939            terms = [self.term(term) for term in terms]
940            super_terms = [self.super_terms(term) for term in terms]
941            terms = reduce(set.union, super_terms, set(terms))
[1742]942
[1355]943        for term in terms:
944            graph.add_node(term.id, name=term.name)
[1742]945
[1355]946        for term in terms:
947            for rel_type, rel_term in self.related_terms(term):
948                rel_term = self.term(rel_term)
949                if rel_term in terms:
[1742]950                    graph.add_edge(term.id, rel_term.id, label=rel_type,
951                                   color=edge_colors.get(rel_type, "blue"))
952
[1355]953        return graph
[1742]954
955
[1349]956def load(file):
[1742]957    """
958    Load an ontology from a .obo file
[1349]959    """
960    return OBOOntology(file)
[1742]961
962
[1349]963def foundry_ontologies():
[1326]964    """
[1742]965    Return a list of ontologies available from the OBOFoundry `website
966    <http://www.obofoundry.org/>`_. The list contains a tuples of the form
967    `(title, url)` for instance
968    ``('Biological process', 'http://purl.obolibrary.org/obo/go.obo')``
969
970    """
[1326]971    stream = urllib2.urlopen("http://www.obofoundry.org/")
972    text = stream.read()
973    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>'
974    return re.findall(pattern, text)
[1742]975
976
[1326]977if __name__ == "__main__":
978    import doctest
979    stanza = '''[Term]
980id: FOO:001
981name: bar
[1349]982'''
983    seinfeld = StringIO("""
984[Typedef]
985id: parent
986
987[Typedef]
988id: child
989inverse_of: parent ! not actually used yet
990
991[Term]
992id: 001
993name: George
994
995[Term]
996id: 002
997name: Estelle
998relationship: parent 001 ! George
999
1000[Term]
1001id: 003
1002name: Frank
1003relationship: parent 001 ! George
1004
[1742]1005""")  # TODO: fill the ontology with all characters
[1349]1006    term = OBOObject.parse_stanza(stanza)
[1742]1007
[1349]1008    seinfeld = OBOOntology(seinfeld)
1009    print seinfeld.child_edges("001")
[1742]1010
1011    doctest.testmod(extraglobs={"stanza": stanza, "term": term},
1012                    optionflags=doctest.ELLIPSIS)
Note: See TracBrowser for help on using the repository browser.