source: orange-bioinformatics/_bioinformatics/widgets/OWGEODatasets.py @ 1636:10d234fdadb9

Revision 1636:10d234fdadb9, 23.5 KB checked in by mitar, 2 years ago (diff)

Restructuring because we will not be using namespaces.

Line 
1"""<name>GEO Data Sets</name>
2<description>Access to Gene Expression Omnibus data sets.</description>
3<priority>20</priority>
4<contact>Ales Erjavec (ales.erjavec(@at@)fri.uni-lj.si)</contact>
5<icon>icons/GEODataSets.png</icon>
6"""
7
8from __future__ import absolute_import, with_statement
9
10from collections import defaultdict
11from functools import partial
12import sys, os, glob
13
14from Orange.orng import orngServerFiles
15from Orange.orng.orngDataCaching import data_hints
16from Orange.OrangeWidgets import OWGUI, OWGUIEx
17from Orange.OrangeWidgets.OWWidget import *
18
19from .. import obiGEO
20
21LOCAL_GDS_COLOR = Qt.darkGreen
22
23TextFilterRole = OWGUI.OrangeUserRole.next()
24
25class TreeModel(QAbstractItemModel):
26    def __init__(self, data, header, parent):
27        QAbstractItemModel.__init__(self, parent)
28        self._data = [[QVariant(s) for s in row] for row in data]
29        self._dataDict = {}
30        self._header = {Qt.Horizontal: dict([(i, {Qt.DisplayRole: h}) for i, h in enumerate(header)])}
31        self._roleData = {Qt.DisplayRole:self._data}
32        dataStore = partial(defaultdict, partial(defaultdict, partial(defaultdict, QVariant)))
33        self._roleData = dataStore(self._roleData)
34        self._header = dataStore(self._header)
35   
36    def setColumnLinks(self, column, links):
37        font =QFont()
38        font.setUnderline(True)
39        font = QVariant(font)
40        for i, link in enumerate(links):
41            self._roleData[LinkRole][i][column] = QVariant(link)
42            self._roleData[Qt.FontRole][i][column] = font
43            self._roleData[Qt.ForegroundRole][i][column] = QVariant(QColor(Qt.blue))
44   
45    def setRoleData(self, role, row, col, data):
46        self._roleData[role][row][col] = data
47       
48    def setData(self, index, value, role=Qt.EditRole):
49        self._roleData[role][index.row()][index.column()] = value
50        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
51       
52    def data(self, index, role):
53        row, col = index.row(), index.column()
54        return self._roleData[role][row][col]
55       
56    def index(self, row, col, parent=QModelIndex()):
57        return self.createIndex(row, col, 0)
58   
59    def parent(self, index):
60        return QModelIndex()
61   
62    def rowCount(self, index=QModelIndex()):
63        if index.isValid():
64            return 0
65        else:
66            return len(self._data)
67       
68    def columnCount(self, index):
69        return len(self._header[Qt.Horizontal])
70
71    def headerData(self, section, orientation, role):
72        try:
73            return QVariant(self._header[orientation][section][role])
74        except KeyError, er:
75#            print >> sys.stderr, er
76            return QVariant()
77       
78    def setHeaderData(self, section, orientation, value, role=Qt.EditRole):
79        self._header[orientation][section][role] = value
80       
81from Orange.utils import lru_cache
82
83class MySortFilterProxyModel(QSortFilterProxyModel):   
84    def __init__(self, parent=None):
85        QSortFilterProxyModel.__init__(self, parent)
86        self._filter_strings = []
87        self._cache = {}
88        self._cache_fixed = {}
89        self._cache_prefix = {}
90        self._row_text = {}
91       
92        # Create a cached version of _filteredRows
93        self._filteredRows = lru_cache(100)(self._filteredRows) 
94
95    def setSourceModel(self, model):
96        """ Set the source model for the filter
97        """ 
98        self._filter_strings = []
99        self._cache = {}
100        self._cache_fixed = {}
101        self._cache_prefix = {}
102        self._row_text = {}
103        QSortFilterProxyModel.setSourceModel(self, model)
104       
105    def addFilterFixedString(self, string, invalidate=True):
106        """ Add `string` filter to the list of filters. If invalidate is
107        True the filter cache will be recomputed.
108        """
109        self._filter_strings.append(string)
110        all_rows = range(self.sourceModel().rowCount())
111        row_text = [self.rowFilterText(row) for row in all_rows]
112        self._cache[string] = [string in text for text in row_text]
113        if invalidate:
114            self.updateCached()
115            self.invalidateFilter()
116       
117    def removeFilterFixedString(self, index=-1, invalidate=True):
118        """ Remove the `index`-th filter string. If invalidate is True the
119        filter cache will be recomputed.
120        """
121        string = self._filter_strings.pop(index) 
122        del self._cache[string] 
123        if invalidate:
124            self.updateCached()
125            self.invalidateFilter()
126           
127    def setFilterFixedStrings(self, strings):
128        """ Set a list of string to be the new filters.
129        """
130        s_time = time.time()
131        to_remove = set(self._filter_strings) - set(strings)
132        to_add = set(strings) - set(self._filter_strings)
133        for str in to_remove:
134            self.removeFilterFixedString(self._filter_strings.index(str), invalidate=False)
135       
136        for str in to_add:
137            self.addFilterFixedString(str, invalidate=False)
138        self.updateCached()
139        self.invalidateFilter()
140           
141    def _filteredRows(self, filter_strings):
142        """ Return a dictionary mapping row indexes to True False values.
143        .. note:: This helper function is wrapped in the __init__ method.
144        """
145        all_rows = range(self.sourceModel().rowCount())
146        cache = self._cache
147        return dict([(row, all([cache[str][row] for str in filter_strings])) for row in all_rows])
148   
149    def updateCached(self):
150        """ Update the combined filter cache.
151        """
152        self._cache_fixed = self._filteredRows(tuple(sorted(self._filter_strings))) 
153       
154    def setFilterFixedString(self, string):
155        """Should this raise an error? It is not being used.
156        """
157        QSortFilterProxyModel.setFilterFixedString(self, string)
158       
159    def rowFilterText(self, row):
160        """ Return text for `row` to filter on.
161        """
162        f_role = self.filterRole()
163        f_column = self.filterKeyColumn()
164        s_model = self.sourceModel()
165        data = s_model.data(s_model.index(row, f_column), f_role)
166        if isinstance(data, QVariant):
167            data = unicode(data.toString(), errors="ignore")
168        else:
169            data = unicode(data, errors="ignore")
170        return data
171       
172    def filterAcceptsRow(self, row, parent): 
173        return self._cache_fixed.get(row, True)
174   
175    def lessThan(self, left, right):
176        if left.column() == 1 and right.column(): # TODO: Remove fixed column handling
177            left_gds = str(left.data(Qt.DisplayRole).toString())
178            right_gds = str(right.data(Qt.DisplayRole).toString())
179            left_gds = left_gds.lstrip("GDS")
180            right_gds = right_gds.lstrip("GDS")
181            try:
182                return int(left_gds) < int(right_gds)
183            except Exception, ex:
184                pass
185        return QSortFilterProxyModel.lessThan(self, left, right)
186   
187from Orange.OrangeWidgets.OWGUI import LinkStyledItemDelegate, LinkRole
188
189def childiter(item):
190    """ Iterate over the children of an QTreeWidgetItem instance.
191    """
192    for i in range(item.childCount()):
193        yield item.child(i)
194               
195class OWGEODatasets(OWWidget):
196    settingsList = ["outputRows", "mergeSpots", "gdsSelectionStates", "splitterSettings", "currentGds", "autoCommit"]
197
198    def __init__(self, parent=None ,signalManager=None, name=" GEO Data Sets"):
199        OWWidget.__init__(self, parent ,signalManager, name)
200
201        self.outputs = [("Expression Data", ExampleTable)]
202
203        ## Settings
204#        self.selectedSubsets = []
205#        self.sampleSubsets = []
206        self.selectedAnnotation = 0
207        self.includeIf = False
208        self.minSamples = 3
209        self.autoCommit = False
210        self.outputRows = 0
211        self.mergeSpots = True
212        self.filterString = ""
213        self.currentGds = None
214        self.selectionChanged = False
215        self.autoCommit = False
216        self.gdsSelectionStates = {}
217        self.splitterSettings = ['\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\xea\x00\x00\x00\xd7\x01\x00\x00\x00\x07\x01\x00\x00\x00\x02',
218                                 '\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\xb5\x00\x00\x02\x10\x01\x00\x00\x00\x07\x01\x00\x00\x00\x01']
219
220        self.loadSettings()
221
222        ## GUI
223        self.infoBox = OWGUI.widgetLabel(OWGUI.widgetBox(self.controlArea, "Info", addSpace=True), "\n\n")
224
225        box = OWGUI.widgetBox(self.controlArea, "Output", addSpace=True)
226        OWGUI.radioButtonsInBox(box, self, "outputRows", ["Genes or spots", "Samples"], "Rows", callback=self.commitIf)
227        OWGUI.checkBox(box, self, "mergeSpots", "Merge spots of same gene", callback=self.commitIf)
228
229        box = OWGUI.widgetBox(self.controlArea, "Output", addSpace=True)
230        self.commitButton = OWGUI.button(box, self, "Commit", callback=self.commit)
231        cb = OWGUI.checkBox(box, self, "autoCommit", "Commit on any change")
232        OWGUI.setStopper(self, self.commitButton, cb, "selectionChanged", self.commit)
233        OWGUI.rubber(self.controlArea)
234
235        self.filterLineEdit = OWGUIEx.lineEditHint(self.mainArea, self, "filterString", "Filter",
236                                   caseSensitive=False, matchAnywhere=True, 
237                                   #listUpdateCallback=self.filter, callbackOnType=False,
238                                   callback=self.filter,  delimiters=" ")
239       
240        splitter = QSplitter(Qt.Vertical, self.mainArea)
241        self.mainArea.layout().addWidget(splitter)
242        self.treeWidget = QTreeView(splitter)
243       
244        self.treeWidget.setSelectionMode(QAbstractItemView.SingleSelection)
245        self.treeWidget.setRootIsDecorated(False)
246        self.treeWidget.setSortingEnabled(True)
247        self.treeWidget.setAlternatingRowColors(True)
248        self.treeWidget.setUniformRowHeights(True)
249        self.treeWidget.setItemDelegate(LinkStyledItemDelegate(self.treeWidget))
250        self.treeWidget.setItemDelegateForColumn(0, OWGUI.IndicatorItemDelegate(self.treeWidget, role=Qt.DisplayRole))
251       
252        self.connect(self.treeWidget, SIGNAL("itemSelectionChanged ()"), self.updateSelection)
253        self.treeWidget.viewport().setMouseTracking(True)
254       
255        splitterH = QSplitter(Qt.Horizontal, splitter) 
256       
257        box = OWGUI.widgetBox(splitterH, "Description")
258        self.infoGDS = OWGUI.widgetLabel(box, "")
259        self.infoGDS.setWordWrap(True)
260        OWGUI.rubber(box)
261       
262        box = OWGUI.widgetBox(splitterH, "Sample Annotations")
263        self.annotationsTree = QTreeWidget(box)
264        self.annotationsTree.setHeaderLabels(["Type (Sample annotations)", "Sample count"])
265        self.annotationsTree.setRootIsDecorated(True)
266        box.layout().addWidget(self.annotationsTree)
267        self.connect(self.annotationsTree, SIGNAL("itemChanged(QTreeWidgetItem * , int)"), self.annotationSelectionChanged)
268        self._annotationsUpdating = False
269        self.splitters = splitter, splitterH
270        self.connect(splitter, SIGNAL("splitterMoved(int, int)"), self.splitterMoved)
271        self.connect(splitterH, SIGNAL("splitterMoved(int, int)"), self.splitterMoved)
272       
273        for sp, setting in zip(self.splitters, self.splitterSettings):
274            sp.restoreState(setting)
275           
276        self.searchKeys = ["dataset_id", "title", "platform_organism", "description"]
277        self.cells = []
278       
279        QTimer.singleShot(50, self.updateTable)
280        self.resize(1000, 600)
281
282    def updateInfo(self):
283        gds_info = self.gds_info #obiGEO.GDSInfo()
284        text = "%i datasets\n%i datasets cached\n" % (len(gds_info), len(glob.glob(orngServerFiles.localpath("GEO") + "/GDS*")))
285        filtered = self.treeWidget.model().rowCount()
286        if len(self.cells) != filtered:
287            text += ("%i after filtering") % filtered
288        self.infoBox.setText(text)
289       
290    def updateTable(self):
291        self.treeItems = []
292        self.progressBarInit()
293        with orngServerFiles.DownloadProgress.setredirect(self.progressBarSet):
294            self.gds_info = info = obiGEO.GDSInfo()
295        milestones = set(range(0, len(info), max(len(info)/100, 1)))
296        self.cells = cells = []
297        gdsLinks = []
298        pmLinks = []
299        localGDS = []
300        full_text_search_data = []
301       
302        self.gds = []
303        for i, (name, gds) in enumerate(info.items()):
304            local = os.path.exists(orngServerFiles.localpath(obiGEO.DOMAIN, gds["dataset_id"] + ".soft.gz"))
305            cells.append([" " if local else "", gds["dataset_id"], gds["title"], gds["platform_organism"], len(gds["samples"]), gds["feature_count"],
306                          gds["gene_count"], len(gds["subsets"]), gds.get("pubmed_id", "")])
307
308            gdsLinks.append("http://www.ncbi.nlm.nih.gov/sites/GDSbrowser?acc=%s" % gds["dataset_id"])
309            pmLinks.append("http://www.ncbi.nlm.nih.gov/pubmed/%s" % gds.get("pubmed_id") if gds.get("pubmed_id") else QVariant())
310
311            if local:
312                localGDS.append(i)
313            self.gds.append(gds)
314           
315            full_text_search_data.append(unicode(" | ".join([gds.get(key, "").lower() for key in self.searchKeys]), errors="ignore"))
316           
317            if i in milestones:
318                self.progressBarSet(100.0*i/len(info))
319
320        model = TreeModel(cells, ["", "ID", "Title", "Organism", "Samples", "Features", "Genes", "Subsets", "PubMedID"], self.treeWidget)
321        model.setColumnLinks(1, gdsLinks)
322        model.setColumnLinks(8, pmLinks)
323       
324        for i, text in enumerate(full_text_search_data):
325            model.setData(model.index(i, 0), QVariant(text), TextFilterRole)
326       
327        proxyModel = MySortFilterProxyModel(self.treeWidget)
328#        proxyModel = QSortFilterProxyModel(self.treeWidget)
329        proxyModel.setSourceModel(model)
330        proxyModel.setFilterKeyColumn(0)
331        proxyModel.setFilterRole(TextFilterRole)
332        proxyModel.setFilterCaseSensitivity(False)
333        proxyModel.setFilterFixedString(self.filterString)
334        self.treeWidget.setModel(proxyModel)
335        self.connect(self.treeWidget.selectionModel(), SIGNAL("selectionChanged(QItemSelection , QItemSelection )"), self.updateSelection)
336        filterItems = " ".join([self.gds[i][key] for i in range(len(self.gds)) for key in self.searchKeys])
337        filterItems = reduce(lambda s, d: s.replace(d, " "),
338                             [",", ".", ":", ";", "!", "?", "(", ")", "{", "}"
339                              "[", "]", "_", "-", "+", "\\", "|", "/", "%", "#",
340                              "@", "$", "^", "&", "*", "<", ">", "~", "`"],
341                             filterItems.lower())
342        filterItems = sorted(set(filterItems.split(" ")))
343        filterItems = [item for item in filterItems if len(filterItems) > 3]
344        self.filterLineEdit.setItems(filterItems)
345       
346        for i in range(8):
347            self.treeWidget.resizeColumnToContents(i)
348        self.treeWidget.setColumnWidth(1, min(self.treeWidget.columnWidth(1), 300))
349        self.treeWidget.setColumnWidth(2, min(self.treeWidget.columnWidth(2), 200))
350        self.progressBarFinished()
351
352        if self.currentGds:
353            gdss = [(i, model.data(model.index(i,1), Qt.DisplayRole)) for i in range(model.rowCount())]
354            current = [i for i, variant in gdss if variant.isValid() and str(variant.toString()) == self.currentGds["dataset_id"]]
355            if current:
356                mapFromSource = self.treeWidget.model().mapFromSource
357                self.treeWidget.selectionModel().select(mapFromSource(model.index(current[0], 0)), QItemSelectionModel.Select | QItemSelectionModel.Rows)
358           
359        self.updateInfo()
360
361    def updateSelection(self, *args):
362        current = self.treeWidget.selectedIndexes()
363        mapToSource = self.treeWidget.model().mapToSource
364        current = [mapToSource(index).row() for index in current]
365        if current:
366            self.currentGds = self.gds[current[0]]
367            self.setAnnotations(self.currentGds)
368            self.infoGDS.setText(self.currentGds.get("description", ""))
369        else:
370            self.currentGds = None
371#        self.commitButton.setDisabled(not bool(self.currentGds))
372        self.commitIf()
373       
374   
375    def setAnnotations(self, gds):
376        self._annotationsUpdating = True
377        self.annotationsTree.clear()
378        annotations = reduce(lambda d, info: d[info["type"]].add(info["description"]) or d, gds["subsets"], defaultdict(set))
379        subsetscount = dict([(s["description"], str(len(s["sample_id"]))) for s in gds["subsets"]])
380        for type, subsets in annotations.items():
381            key = (gds["dataset_id"], type)
382            subsetItem = QTreeWidgetItem(self.annotationsTree, [type])
383            subsetItem.setFlags(subsetItem.flags() | Qt.ItemIsUserCheckable | Qt.ItemIsTristate)
384            subsetItem.setCheckState(0, self.gdsSelectionStates.get(key, Qt.Checked))
385            subsetItem.key = key
386            for subset in subsets:
387                key = (gds["dataset_id"], type, subset)
388                item = QTreeWidgetItem(subsetItem, [subset, subsetscount.get(subset, "")])
389                item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
390                item.setCheckState(0, self.gdsSelectionStates.get(key, Qt.Checked))
391                item.key = key
392        self._annotationsUpdating = False
393        self.annotationsTree.expandAll()
394        for i in range(self.annotationsTree.columnCount()):
395            self.annotationsTree.resizeColumnToContents(i)
396               
397    def annotationSelectionChanged(self, item, column):
398        if self._annotationsUpdating:
399            return 
400        for i in range(self.annotationsTree.topLevelItemCount()):
401            item = self.annotationsTree.topLevelItem(i)
402            self.gdsSelectionStates[item.key] = item.checkState(0)
403            for j in range(item.childCount()):
404                child = item.child(j)
405                self.gdsSelectionStates[child.key] = child.checkState(0)
406       
407    def rowFiltered(self, row):
408        filterStrings = self.filterString.lower().split()
409        try:
410            string = " ".join([self.gds[row].get(key, "").lower() for key in self.searchKeys])
411            return not all([s in string for s in filterStrings])
412        except UnicodeDecodeError:
413            string = " ".join([unicode(self.gds[row].get(key, "").lower(), errors="ignore") for key in self.searchKeys])
414            return not all([s in string for s in filterStrings])
415   
416    def filter(self):
417        filter_string = unicode(self.filterLineEdit.text(), errors="ignore")
418        proxyModel = self.treeWidget.model()
419        if proxyModel:
420            strings = filter_string.lower().strip().split()
421            proxyModel.setFilterFixedStrings(strings)
422            self.updateInfo()
423
424    def selectedSamples(self):
425        """ Return the currently selected sample annotations (list of
426        sample type, sample value tuples).
427       
428        .. note:: if some Sample annotation type has no selected values.
429                  this method will return all values for it.
430       
431        """
432        samples = []
433        unused_types = []
434        for stype in childiter(self.annotationsTree.invisibleRootItem()): 
435            selected_values = []
436            all_values = []
437            for sval in childiter(stype):
438                value = (str(stype.text(0)), str(sval.text(0)))
439                if self.gdsSelectionStates.get(sval.key, True):
440                    selected_values.append(value)
441                all_values.append(value)
442            if selected_values:
443                samples.extend(selected_values)
444            else:
445                # If no sample of sample type is selected we don't filter on it.
446                samples.extend(all_values)
447                unused_types.append(str(stype.text(0)))
448               
449        return samples
450   
451    def commitIf(self):
452        if self.autoCommit:
453            self.commit()
454        else:
455            self.selectionChanged = True
456   
457    def commit(self):
458        if self.currentGds:
459            self.error(0) 
460            sample_type = None
461            self.progressBarInit()
462            self.progressBarSet(10)
463           
464            def getdata(gds_id, **kwargs):
465                gds = obiGEO.GDS(gds_id)
466                data = gds.getdata(**kwargs)
467                return data
468           
469            self.setEnabled(False)
470            call = self.asyncCall(getdata, (self.currentGds["dataset_id"],), dict(report_genes=self.mergeSpots,
471                                           transpose=self.outputRows,
472                                           sample_type=sample_type if sample_type!="Include all" else None),
473                                  onResult=self.onData,
474                                  onFinished=lambda: self.setEnabled(True),
475                                  onError=self.onAsyncError,
476                                  threadPool=QThreadPool.globalInstance()
477                                 )
478            call.__call__() #invoke
479
480    def onAsyncError(self, (exctype, value, tb)):
481        import ftplib
482        if issubclass(exctype, ftplib.error_temp):
483            self.error(0, "Can not download dataset from NCBI ftp server! Try again later.")
484        elif issubclass(exctype, ftplib.all_errors):
485            self.error(0, "Error while connecting to the NCBI ftp server! %s" % str(value))
486        else:
487            sys.excepthook(exctype, value, tb)
488           
489        self.progressBarFinished()
490
491    def onData(self, data):
492        self.progressBarSet(50)
493       
494        samples = self.selectedSamples()
495       
496        self.warning(0)
497        message = None
498        if self.outputRows:
499            samples = set(s[1] for s in samples) # dont have info on sample types in the data class variable
500            select = [1 if samples.issuperset(str(ex.getclass()).split("|")) else 0 for ex in data]
501            data = data.select(select)
502            # TODO: add sample types as separate features 
503            data.domain.classVar.values = ["|".join([cl for cl in val.split("|") if cl in samples]) for val in data.domain.classVar.values]
504            if len(data) == 0:
505                message = "No samples with selected sample annotations."
506        else:
507            samples = set(samples)
508            domain = orange.Domain([attr for attr in data.domain.attributes if samples.issuperset(attr.attributes.items())], data.domain.classVar)
509            domain.addmetas(data.domain.getmetas())
510            if len(domain.attributes) == 0:
511                message = "No samples with selected sample annotations."
512            stypes = set(s[0] for s in samples)
513            for attr in domain.attributes:
514                attr.attributes = dict([(key, value) for key, value in attr.attributes.items() if key in stypes])
515            data = orange.ExampleTable(domain, data)
516       
517        if message is not None:
518            self.warning(0, message)
519           
520        data_hints.set_hint(data, "taxid", self.currentGds.get("taxid", ""), 10.0)
521        data_hints.set_hint(data, "genesinrows", self.outputRows, 10.0)
522       
523        self.progressBarFinished()
524        self.send("Expression Data", data)
525
526        model = self.treeWidget.model().sourceModel()
527        row = self.gds.index(self.currentGds)
528#            model._roleData[Qt.ForegroundRole][row].update(zip(range(1, 7), [QVariant(QColor(LOCAL_GDS_COLOR))] * 6))
529        model.setData(model.index(row, 0),  QVariant(" "), Qt.DisplayRole) 
530#            model.emit(SIGNAL("dataChanged(const QModelIndex &, const QModelIndex &)"), model.index(row, 0), model.index(row, 0))
531        self.updateInfo()
532        self.selectionChanged = False
533       
534    def splitterMoved(self, *args):
535        self.splitterSettings = [str(sp.saveState()) for sp in self.splitters]
536
537if __name__ == "__main__":
538    app = QApplication(sys.argv)
539    w = OWGEODatasets()
540    w.show()
541    app.exec_()
542    w.saveSettings()
Note: See TracBrowser for help on using the repository browser.