source: orange-bioinformatics/widgets/OWGeneInfo.py @ 1614:b8ee06613d54

Revision 1614:b8ee06613d54, 22.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 years ago (diff)

Added columns from the gene info view to the output.

Line 
1"""
2<name>Gene Info</name>
3<description>Displays gene information from NCBI and other sources.</description>
4<priority>2010</priority>
5<contact>Ales Erjavec (ales.erjavec(@at@)fri.uni-lj.si)</contact>
6<icon>icons/GeneInfo.png</icon>
7"""
8from __future__ import with_statement
9
10import Orange
11
12import obiGene, obiTaxonomy
13import orange
14import orngServerFiles
15
16from OWWidget import *
17import OWGUI
18
19from orngDataCaching import data_hints
20
21from collections import defaultdict
22from functools import partial
23
24class TreeModel(QAbstractItemModel):
25    def __init__(self, data, header, parent):
26        QAbstractItemModel.__init__(self, parent)
27        self._data = [[QVariant(s) for s in row] for row in data]
28        self._dataDict = {}
29        self._header = header
30        self._roleData = {Qt.DisplayRole:self._data}
31        self._roleData = partial(defaultdict, partial(defaultdict, partial(defaultdict, QVariant)))(self._roleData)
32   
33    def setColumnLinks(self, column, links):
34        font =QFont()
35        font.setUnderline(True)
36        font = QVariant(font)
37        for i, link in enumerate(links):
38            self._roleData[LinkRole][i][column] = QVariant(link)
39            self._roleData[Qt.FontRole][i][column] = font
40            self._roleData[Qt.ForegroundRole][i][column] = QVariant(QColor(Qt.blue))
41   
42    def setRoleData(self, role, row, col, data):
43        self._roleData[role][row][col] = data
44       
45    def data(self, index, role=Qt.DisplayRole):
46        row, col = index.row(), index.column()
47        return self._roleData[role][row][col]
48       
49    def index(self, row, col, parent=QModelIndex()):
50        return self.createIndex(row, col, 0)
51   
52    def parent(self, index):
53        return QModelIndex()
54   
55    def rowCount(self, index=QModelIndex()):
56        if index.isValid():
57            return 0
58        else:
59            return len(self._data)
60       
61    def columnCount(self, index=QModelIndex()):
62        return len(self._header)
63
64    def headerData(self, section, orientation, role=Qt.DisplayRole):
65        if role==Qt.DisplayRole:
66            return QVariant(self._header[section])
67        return QVariant()
68
69from OWGUI import LinkStyledItemDelegate, LinkRole
70
71def lru_cache(maxsize=100):
72    """ A least recently used cache function decorator.
73    """
74   
75    def decorating_function(func):
76        import functools
77        cache = {}
78       
79        functools.wraps(func)
80        def wrapped(*args, **kwargs):
81            key = args + tuple(sorted(kwargs.items()))
82            if key not in cache:
83                res = func(*args, **kwargs)
84                cache[key] = (time.time(), res)
85                if len(cache) > maxsize:
86                    key, (_, _) = min(cache.iteritems(), key=lambda item: item[1][0])
87                    del cache[key]
88            else:
89                _, res = cache[key]
90                cache[key] = (time.time(), res) # update the time
91               
92            return res
93       
94        def clear():
95            cache.clear()
96       
97        wrapped.clear = clear
98       
99        return wrapped
100    return decorating_function
101               
102class LinkFmt(object):
103    def __init__(self, link_fmt, name):
104        self.link_fmt = link_fmt
105        self.name = name
106       
107    def format(self, *args, **kwargs):
108        return Link(self.link_fmt.format(*args, **kwargs), **kwargs)
109   
110    def __repr__(self):
111        return "<LinkFmt " + repr(self.name) + " >"
112   
113    def __str__(self):
114        return self.name
115   
116class Link(object):
117    def __init__(self, link, text=None, **kwargs):
118        self.link = link
119        self.text = text if text is not None else "link"
120        self.__dict__.update(kwargs)
121       
122    def str(self):
123        return link
124   
125   
126@lru_cache(maxsize=2)
127def get_ncbi_info(taxid):
128    return obiGene.NCBIGeneInfo(taxid)
129
130def ncbi_info(taxid, genes):
131    info = get_ncbi_info(taxid)
132    schema_link = LinkFmt("http://www.ncbi.nlm.nih.gov/sites/entrez?Db=gene&Cmd=ShowDetailView&TermToSearch={gene_id}", name="NCBI ID")
133    schema = [schema_link, "Symbol", "Locus Tag", "Chromosome",
134              "Description", "Synonyms", "Nomenclature"]
135    ret = []
136    for gene in genes:
137        gi = info.get_info(gene)
138        if gi:
139            ret.append([schema_link.format(gene_id=gi.gene_id, text=gi.gene_id),
140                        gi.symbol + " (%s)" % gene if gene != gi.symbol else gi.symbol,
141                        gi.locus_tag or "",
142                        gi.chromosome or "",
143                        gi.description or "",
144                        ", ".join(gi.synonyms),
145                        gi.symbol_from_nomenclature_authority or ""
146                        ])
147        else:
148            ret.append(None)
149    return schema, ret
150   
151def dicty_info(taxid, genes):
152    import obiDicty 
153    info = obiDicty.DictyBase()
154    name_matcher = obiGene.GMDicty()
155    name_matcher.set_targets(info.info.keys())
156    schema_link = LinkFmt("http://dictybase.org/db/cgi-bin/gene_page.pl?dictybaseid={gene_id}", name="Dicty Base Id")
157    schema = [schema_link, "Name", "Synonyms", "Gene Products"]
158   
159    ret = []
160    for gene in genes:
161        gene = name_matcher.umatch(gene)
162        gi = info.info.get(gene, None)
163        if gi:
164            ret.append([schema_link.format(gene_id=gene, text=gene),
165                        gi[0] + " (%s)" % gene if gene != gi[0] else gi[0], # Gene Name
166                        ", ".join(gi[1]), # Synonyms
167                        gi[2] or "", # Gene Products
168                        ])
169           
170        else:
171            ret.append(None)
172   
173    return schema, ret
174   
175   
176INFO_SOURCES = {"default": [("NCBI Info", ncbi_info)],
177                "352472": [("NCBI Info", ncbi_info),
178                           ("Dicty Base", dicty_info)
179                           ]
180                }
181
182class OWGeneInfo(OWWidget):
183    settingsList = ["organismIndex", "geneAttr", "useAttr", "autoCommit"]
184    contextHandlers = {"":DomainContextHandler("", ["organismIndex",
185                                "geneAttr", "useAttr", "useAltSource"])}
186    def __init__(self, parent=None, signalManager=None, name="Gene Info"):
187        OWWidget.__init__(self, parent, signalManager, name)
188
189        self.inputs = [("Examples", ExampleTable, self.setData)]
190        self.outputs = [("Selected Examples", ExampleTable)]
191
192        self.organismIndex = 0
193        self.geneAttr = 0
194        self.useAttr = False
195        self.autoCommit = False
196        self.searchString = ""
197        self.selectionChangedFlag = False
198        self.useAltSource = 0
199        self.loadSettings()
200       
201        self.infoLabel = OWGUI.widgetLabel(OWGUI.widgetBox(self.controlArea,
202                                                    "Info", addSpace=True),
203                                           "No data on input\n")
204        self.organisms = sorted(set([name.split(".")[-2] for name in \
205                            orngServerFiles.listfiles("NCBI_geneinfo")] + \
206                            obiGene.NCBIGeneInfo.essential_taxids()))
207   
208        self.organismBox = OWGUI.widgetBox(self.controlArea, "Organism",
209                                           addSpace=True)
210        self.organismComboBox = OWGUI.comboBox(self.organismBox, self,
211                                "organismIndex", "Organism",
212                                items=[obiTaxonomy.name(id) for id in self.organisms],
213                                callback=self.setItems,
214                                debuggingEnabled=0)
215       
216        # For now only support one alt source, with a checkbox
217        # In the future this can be extended to multiple selections
218        self.altSourceCheck = OWGUI.checkBox(self.organismBox, self,
219                            "useAltSource", "Show information from dictyBase",
220                            callback=self.onAltSourceChange,
221#                            debuggingEnabled=0,
222                            )
223        self.altSourceCheck.hide()
224       
225        box = OWGUI.widgetBox(self.controlArea, "Gene names", addSpace=True)
226        self.geneAttrComboBox = OWGUI.comboBox(box, self, "geneAttr",
227                                "Gene atttibute", callback=self.setItems)
228       
229        c = OWGUI.checkBox(box, self, "useAttr", "Use attribute names",
230                           callback=self.setItems,
231                           disables=[(-1, self.geneAttrComboBox)])
232       
233        self.geneAttrComboBox.setDisabled(bool(self.useAttr))
234
235        box = OWGUI.widgetBox(self.controlArea, "Commit", addSpace=True)
236        b = OWGUI.button(box, self, "Commit", callback=self.commit)
237        c = OWGUI.checkBox(box, self, "autoCommit", "Commit on change")
238        OWGUI.setStopper(self, b, c, "selectionChangedFlag",
239                         callback=self.commit)
240       
241        ## A label for dictyExpress link
242        self.dictyExpressBox = OWGUI.widgetBox(self.controlArea, "Dicty Express")
243        self.linkLabel = OWGUI.widgetLabel(self.dictyExpressBox, "")
244        self.linkLabel.setOpenExternalLinks(True)
245        self.dictyExpressBox.hide()
246       
247        OWGUI.rubber(self.controlArea)
248
249        OWGUI.lineEdit(self.mainArea, self, "searchString", "Filter",
250                       callbackOnType=True, callback=self.searchUpdate)
251       
252        self.treeWidget = QTreeView(self.mainArea)
253        self.treeWidget.setRootIsDecorated(False)
254        self.treeWidget.setSelectionMode(QAbstractItemView.ExtendedSelection)
255        self.treeWidget.setItemDelegate(LinkStyledItemDelegate(self.treeWidget))
256        #self.connect(self.treeWidget, SIGNAL("itemSelectionChanged()"), self.commitIf)
257        self.treeWidget.viewport().setMouseTracking(True)
258        self.treeWidget.setSortingEnabled(True)
259        self.mainArea.layout().addWidget(self.treeWidget)
260       
261        box = OWGUI.widgetBox(self.mainArea, "",
262                              orientation="horizontal")
263        OWGUI.button(box, self, "Select Filtered",
264                     callback=self.selectFiltered)
265        OWGUI.button(box, self, "Clear Selection",
266                     callback=self.treeWidget.clearSelection)
267       
268        self.resize(1000, 700)       
269
270        self.geneinfo = []
271        self.cells = []
272        self.row2geneinfo = {}
273        self.data = None
274        self.currentLoaded = None, None
275        self.selectionUpdateInProgress = False
276       
277    def setData(self, data=None):
278        self.closeContext()
279        self.data = data
280        if data:
281            self.geneAttrComboBox.clear()
282            self.attributes = [attr for attr in self.data.domain.variables + \
283                               self.data.domain.getmetas().values() \
284                               if attr.varType in [orange.VarTypes.String,
285                                                   orange.VarTypes.Discrete]]
286            self.geneAttrComboBox.addItems([attr.name for attr in self.attributes])
287            self.openContext("", data)
288            self.geneAttr = min(self.geneAttr, len(self.attributes) - 1)
289           
290            taxid = data_hints.get_hint(self.data, "taxid", "")
291            if taxid in self.organisms:
292                self.organismIndex = self.organisms.index(taxid)
293               
294            self.useAttr = data_hints.get_hint(self.data, "genesinrows",  self.useAttr)
295           
296            self.setItems()
297        else:
298            self.clear()
299
300    def infoSource(self):
301        """ Return the current selected info source getter function from
302        INFO_SOURCES
303        """
304        org = self.organisms[min(self.organismIndex, len(self.organisms) - 1)]
305        if org not in INFO_SOURCES:
306            org = "default"
307        sources = INFO_SOURCES[org]
308        name, func =  sources[min(self.useAltSource, len(sources) - 1)]
309        return name, func
310       
311    def setItems(self):
312        self.warning(0)
313        if not self.data:
314            return
315        if self.useAttr:
316            genes = [attr.name for attr in self.data.domain.attributes]
317        elif self.attributes:
318            attr = self.attributes[self.geneAttr]
319            genes = [str(ex[attr]) for ex in self.data if not ex[attr].isSpecial()]
320        else:
321            genes = []
322        if not genes:
323            self.warning(0, "Could not extract genes from input dataset.")
324        self.warning(1)
325        org = self.organisms[min(self.organismIndex, len(self.organisms) - 1)]
326        source_name, info_getter = self.infoSource()
327        info , currorg = self.currentLoaded
328        self.error(0)
329       
330        self.updateDictyExpressLink(genes, show=org == "352472")
331        self.altSourceCheck.setVisible(org == "352472")
332       
333        # get the info for the genes in a separate thread
334        self.progressBarInit()
335#        call = self.asyncCall(info_getter, (org, genes),
336#                              name="Load NCBI Gene Info",
337#                              blocking=False)
338#        call.connect(call, SIGNAL("progressChanged(float)"), self.progressBarSet, Qt.QueuedConnection)
339#        with orngServerFiles.DownloadProgress.setredirect(call.emitProgressChanged):
340#            call.__call__()
341#            schema, geneinfo = call.get_result()
342#        call.__call__()
343#        schema, geneinfo = call.get_result()
344        with orngServerFiles.DownloadProgress.setredirect(self.progressBarSet):
345            schema, geneinfo = info_getter(org, genes)
346        self.progressBarFinished()
347        # schema, geneinfo = info_getter(org, genes)
348
349        self.geneinfo = geneinfo = list(zip(genes, geneinfo))
350
351        self.progressBarInit()
352        milestones = set([i for i in range(0, len(geneinfo), max(len(geneinfo)/100, 1))])
353        self.cells = cells = []
354        self.row2geneinfo = {}
355        links = []
356        for i, (gene, gi) in enumerate(geneinfo):
357            if gi:
358                row = []
359                for sch, item in zip(schema, gi):
360                    if isinstance(item, Link): # TODO: This should be handled by delegates
361                        row.append(item.text)
362                        links.append(item.link)
363                    else:
364                        row.append(item)
365                cells.append(row)
366                self.row2geneinfo[len(cells) - 1] = i
367#                cells.append([gi.gene_id, gi.symbol + " (%s)" % gene if gene != gi.symbol else gi.symbol,
368#                            gi.locus_tag or "", gi.chromosome or "", gi.description or "",
369#                            ", ".join(gi.synonyms), gi.symbol_from_nomenclature_authority or ""])
370#                links.append("http://www.ncbi.nlm.nih.gov/sites/entrez?Db=gene&Cmd=ShowDetailView&TermToSearch=%s" % gi.gene_id)
371               
372
373            if i in milestones:
374                self.progressBarSet(100.0*i/len(geneinfo))
375        model = TreeModel(cells, [str(col) for col in schema], self.treeWidget)
376       
377        model.setColumnLinks(0, links)
378        proxyModel = QSortFilterProxyModel(self)
379        proxyModel.setSourceModel(model)
380        self.treeWidget.setModel(proxyModel)
381        self.connect(self.treeWidget.selectionModel(), SIGNAL("selectionChanged(QItemSelection , QItemSelection )"), self.commitIf)
382        for i in range(7):
383            self.treeWidget.resizeColumnToContents(i)
384            self.treeWidget.setColumnWidth(i, min(self.treeWidget.columnWidth(i), 200))
385        self.treeWidget.update()
386        self.progressBarFinished()
387
388        self.infoLabel.setText("%i genes\n%i matched NCBI's IDs" % (len(genes), len(cells)))
389        self.matchedInfo = len(genes), len(cells)
390
391    def clear(self):
392        self.infoLabel.setText("No data on input\n")
393        self.treeWidget.setModel(TreeModel([], ["NCBI ID", "Symbol", "Locus Tag",
394                                            "Chromosome", "Description", "Synonyms",
395                                            "Nomenclature"], self.treeWidget))
396        self.geneAttrComboBox.clear()
397        self.send("Selected Examples", None)
398
399    def commitIf(self, *args):
400        if self.autoCommit and not self.selectionUpdateInProgress:
401            self.commit()
402        else:
403            self.selectionChangedFlag = True
404
405    def commit(self):
406        if not self.data:
407            return
408        model = self.treeWidget.model()
409        mapToSource = model.mapToSource
410        selectedIds = [self.cells[mapToSource(index).row()][0] for index in self.treeWidget.selectedIndexes()]
411        selectedRows = self.treeWidget.selectedIndexes()
412        selectedRows = [mapToSource(index).row() for index in selectedRows]
413        model = model.sourceModel()
414       
415        selectedGeneids = [self.row2geneinfo[row] for row in selectedRows]
416        selectedIds = [self.geneinfo[i][0] for i in selectedGeneids]
417        selectedIds = set(selectedIds)
418        gene2row = dict((self.geneinfo[self.row2geneinfo[row]][0], row) \
419                        for row in selectedRows)
420       
421        if self.useAttr:
422            def is_selected(attr):
423                return attr.name in selectedIds
424            attrs = [attr for attr in self.data.domain.attributes if is_selected(attr)]
425            domain = orange.Domain(attrs, self.data.domain.classVar)
426            domain.addmetas(self.data.domain.getmetas())
427            newdata = orange.ExampleTable(domain, self.data)
428            self.send("Selected Examples", newdata)
429        elif self.attributes:
430            attr = self.attributes[self.geneAttr]
431            geneinfo = dict(self.geneinfo)
432            examples = [ex for ex in self.data if str(ex[attr]) in selectedIds]
433            if True:  # Add gene info
434                domain = orange.Domain(self.data.domain, self.data.domain.classVar)
435                domain.addmetas(self.data.domain.getmetas())
436                n_columns = model.columnCount()
437
438                headers = [str(model.headerData(i, Qt.Horizontal, Qt.DisplayRole).toString()) \
439                           for i in range(n_columns)]
440                new_meta_attrs = [(orange.newmetaid(), orange.StringVariable(name)) \
441                                  for name in headers]
442                domain.addmetas(dict(new_meta_attrs))
443                examples = [orange.Example(domain, ex) for ex in examples]
444                for ex in examples:
445                    for i, (_, meta) in enumerate(new_meta_attrs):
446                        row = gene2row[str(ex[attr])]
447                        ex[meta] = str(model.data(model.index(row, i), Qt.DisplayRole).toString())
448
449            if examples:
450                newdata = orange.ExampleTable(examples)
451            else:
452                newdata = None
453            self.send("Selected Examples", newdata)
454        else:
455            self.send("Selected Examples", None)
456           
457    def rowFiltered(self, row):
458        searchStrings = self.searchString.lower().split()
459        row = unicode(" ".join(self.cells[row]).lower(), errors="ignore")
460        return not all([s in row for s in searchStrings])
461   
462    def searchUpdate(self):
463        if not self.data:
464            return
465        searchStrings = self.searchString.lower().split()
466        index = self.treeWidget.model().sourceModel().index
467        mapFromSource = self.treeWidget.model().mapFromSource
468        for i, row in enumerate(self.cells):
469            row = unicode(" ".join(row).lower(), errors="ignore")
470            self.treeWidget.setRowHidden(mapFromSource(index(i, 0)).row(), QModelIndex(), not all([s in row for s in searchStrings]))
471        #self.treeWidget.model().setFilterRegExp(QRegExp(self.searchString, Qt.CaseInsensitive, QRegExp.FixedString))
472           
473    def selectFiltered(self):
474        if not self.data:
475            return
476        itemSelection = QItemSelection()
477       
478        index = self.treeWidget.model().sourceModel().index
479        mapFromSource = self.treeWidget.model().mapFromSource
480        for i, row in enumerate(self.cells):
481            if not self.rowFiltered(i):
482                itemSelection.select(mapFromSource(index(i, 0)), mapFromSource(index(i, 0)))
483        self.treeWidget.selectionModel().select(itemSelection, QItemSelectionModel.Select | QItemSelectionModel.Rows)
484       
485    def sendReport(self):
486        import OWReport
487        genes, matched = self.matchedInfo
488        info, org = self.currentLoaded
489        self.reportRaw("<p>Input: %i genes of which %i (%.1f%%) matched NCBI synonyms<br>Organism: %s<br>Filter: %s</p>" % (genes, matched, 100.0 * matched / genes, obiTaxonomy.name(org), self.searchString))
490        self.reportSubsection("Gene list")
491        self.reportRaw(reportItemView(self.treeWidget))
492       
493    def updateDictyExpressLink(self, genes, show=False):
494        def fix(ddb):
495            if ddb.startswith("DDB"): 
496                if not ddb.startswith("DDB_G"):
497                    ddb = ddb.replace("DDB", "DDB_G")
498                return ddb
499            return None 
500        if show:
501            genes = [fix(gene) for gene in genes if fix(gene)]
502            link1 = '<a href="http://www.ailab.si/dictyexpress/run/index.php?gene=%s">Microarray profile</a>' % (" ".join(genes))
503            link2 = '<a href="http://www.ailab.si/dictyexpress/run/index.php?gene=%s&db=rnaseq">RNA-Seq profile</a>' % (" ".join(genes))
504            self.linkLabel.setText(link1 + "<br/>" + link2)
505           
506            show = any(genes)
507               
508        if show:
509            self.dictyExpressBox.show()
510        else:
511            self.dictyExpressBox.hide()
512           
513    def onAltSourceChange(self):
514        self.setItems()
515       
516def reportItemView(view):
517    model = view.model()
518    return reportItemModel(view, model)
519   
520def reportItemModel(view, model, index=QModelIndex()):
521    if not index.isValid() or model.hasChildren(index):
522        columnCount, rowCount = model.columnCount(index), model.rowCount(index)
523        if not index.isValid():
524            text = '<table>\n<tr>' + ''.join('<th>%s</th>' % model.headerData(i, Qt.Horizontal, Qt.DisplayRole).toString() for i in range(columnCount)) +'</tr>\n'
525        else:
526#            variant = model.data(index, Qt.DisplayRole)
527#            text = '<table' + (' caption="%s"' % variant.toString() if variant.isValid() else '') + '>\n'
528            pass
529        text += ''.join('<tr>' + ''.join('<td>' + reportItemModel(view, model, model.index(row, column, index)) + '</td>' for column in range(columnCount)) + '</tr>\n' for row in range(rowCount) if not view.isRowHidden(row, index))
530        text += '</table>'
531        return text
532    else:
533        variant = model.data(index, Qt.DisplayRole)
534        return str(variant.toString()) if variant.isValid() else ""
535       
536if __name__ == "__main__":
537    app = QApplication(sys.argv)
538    data = orange.ExampleTable("brown-selected.tab")
539    w = OWGeneInfo()
540    w.show()
541    w.setData(data)
542    app.exec_()
543    w.saveSettings()
544       
545       
546       
547       
Note: See TracBrowser for help on using the repository browser.