source: orange-bioinformatics/_bioinformatics/obiOntology.py @ 1743:9f18a1e0657c

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

Extended term name mangling to all python reserved keywords.

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