source: orange-bioinformatics/orangecontrib/bio/widgets/OWGEODatasets.py @ 2021:711da9aa19e2

Revision 2021:711da9aa19e2, 22.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 5 weeks ago (diff)

Invalidate sorting when removing filter strings.

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.svg</icon>
6"""
7
8from __future__ import absolute_import, with_statement
9
10import sys
11import os
12import glob
13import string
14import urllib2
15from collections import defaultdict
16from functools import partial
17
18from Orange.utils import lru_cache
19from Orange.utils import serverfiles
20from Orange.orng.orngDataCaching import data_hints
21from Orange.OrangeWidgets import OWGUI, OWGUIEx
22from Orange.OrangeWidgets.OWWidget import *
23
24from Orange.OrangeWidgets.OWConcurrent import (
25    ThreadExecutor, Task, methodinvoke
26)
27
28from .. import geo
29
30NAME = "GEO Data Sets"
31DESCRIPTION = "Access to Gene Expression Omnibus data sets."
32ICON = "icons/GEODataSets.svg"
33PRIORITY = 20
34
35INPUTS = []
36OUTPUTS = [("Expression Data", Orange.data.Table)]
37
38REPLACES = ["_bioinformatics.widgets.OWGEODatasets.OWGEODatasets"]
39
40
41TextFilterRole = OWGUI.OrangeUserRole.next()
42
43
44class MySortFilterProxyModel(QSortFilterProxyModel):
45    def __init__(self, parent=None):
46        QSortFilterProxyModel.__init__(self, parent)
47        self._filter_strings = []
48        self._cache = {}
49        self._cache_fixed = {}
50        self._cache_prefix = {}
51        self._row_text = {}
52
53        # Create a cached version of _filteredRows
54        self._filteredRows = lru_cache(100)(self._filteredRows)
55
56    def setSourceModel(self, model):
57        """Set the source model for the filter.
58        """
59        self._filter_strings = []
60        self._cache = {}
61        self._cache_fixed = {}
62        self._cache_prefix = {}
63        self._row_text = {}
64        QSortFilterProxyModel.setSourceModel(self, model)
65
66    def addFilterFixedString(self, string, invalidate=True):
67        """ Add `string` filter to the list of filters. If invalidate is
68        True the filter cache will be recomputed.
69        """
70        self._filter_strings.append(string)
71        all_rows = range(self.sourceModel().rowCount())
72        row_text = [self.rowFilterText(row) for row in all_rows]
73        self._cache[string] = [string in text for text in row_text]
74        if invalidate:
75            self.updateCached()
76            self.invalidateFilter()
77
78    def removeFilterFixedString(self, index=-1, invalidate=True):
79        """ Remove the `index`-th filter string. If invalidate is True the
80        filter cache will be recomputed.
81        """
82        string = self._filter_strings.pop(index)
83        del self._cache[string]
84        if invalidate:
85            self.updateCached()
86            self.invalidate()
87
88    def setFilterFixedStrings(self, strings):
89        """Set a list of string to be the new filters.
90        """
91        to_remove = set(self._filter_strings) - set(strings)
92        to_add = set(strings) - set(self._filter_strings)
93        for str in to_remove:
94            self.removeFilterFixedString(
95                self._filter_strings.index(str),
96                invalidate=False)
97
98        for str in to_add:
99            self.addFilterFixedString(str, invalidate=False)
100        self.updateCached()
101        self.invalidate()
102
103    def _filteredRows(self, filter_strings):
104        """Return a dictionary mapping row indexes to True False values.
105
106        .. note:: This helper function is wrapped in the __init__ method.
107
108        """
109        all_rows = range(self.sourceModel().rowCount())
110        cache = self._cache
111        return dict([(row, all([cache[str][row] for str in filter_strings]))
112                     for row in all_rows])
113
114    def updateCached(self):
115        """Update the combined filter cache.
116        """
117        self._cache_fixed = self._filteredRows(
118            tuple(sorted(self._filter_strings)))
119
120    def setFilterFixedString(self, string):
121        """Should this raise an error? It is not being used.
122        """
123        QSortFilterProxyModel.setFilterFixedString(self, string)
124
125    def rowFilterText(self, row):
126        """Return text for `row` to filter on.
127        """
128        f_role = self.filterRole()
129        f_column = self.filterKeyColumn()
130        s_model = self.sourceModel()
131        data = s_model.data(s_model.index(row, f_column), f_role)
132        if isinstance(data, QVariant):
133            data = unicode(data.toString(), errors="ignore")
134        else:
135            data = unicode(data, errors="ignore")
136        return data
137
138    def filterAcceptsRow(self, row, parent):
139        return self._cache_fixed.get(row, True)
140
141    def lessThan(self, left, right):
142        # TODO: Remove fixed column handling
143        if left.column() == 1 and right.column():
144            left_gds = str(left.data(Qt.DisplayRole).toString())
145            right_gds = str(right.data(Qt.DisplayRole).toString())
146            left_gds = left_gds.lstrip("GDS")
147            right_gds = right_gds.lstrip("GDS")
148            try:
149                return int(left_gds) < int(right_gds)
150            except ValueError:
151                pass
152        return QSortFilterProxyModel.lessThan(self, left, right)
153
154from Orange.OrangeWidgets.OWGUI import LinkStyledItemDelegate, LinkRole
155
156
157def childiter(item):
158    """ Iterate over the children of an QTreeWidgetItem instance.
159    """
160    for i in range(item.childCount()):
161        yield item.child(i)
162
163
164class OWGEODatasets(OWWidget):
165    settingsList = ["outputRows", "mergeSpots", "gdsSelectionStates",
166                    "splitterSettings", "currentGds", "autoCommit"]
167
168    def __init__(self, parent=None, signalManager=None, name=" GEO Data Sets"):
169        OWWidget.__init__(self, parent, signalManager, name)
170
171        self.outputs = [("Expression Data", ExampleTable)]
172
173        ## Settings
174        self.selectedAnnotation = 0
175        self.includeIf = False
176        self.minSamples = 3
177        self.autoCommit = False
178        self.outputRows = 0
179        self.mergeSpots = True
180        self.filterString = ""
181        self.currentGds = None
182        self.selectionChanged = False
183        self.autoCommit = False
184        self.gdsSelectionStates = {}
185        self.splitterSettings = [
186            '\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',
187            '\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'
188        ]
189
190        self.loadSettings()
191
192        ## GUI
193        self.infoBox = OWGUI.widgetLabel(
194            OWGUI.widgetBox(self.controlArea, "Info", addSpace=True),
195            "Initializing\n\n"
196        )
197
198        box = OWGUI.widgetBox(self.controlArea, "Output", addSpace=True)
199        OWGUI.radioButtonsInBox(box, self, "outputRows",
200                                ["Genes or spots", "Samples"], "Rows",
201                                callback=self.commitIf)
202        OWGUI.checkBox(box, self, "mergeSpots", "Merge spots of same gene",
203                       callback=self.commitIf)
204
205        box = OWGUI.widgetBox(self.controlArea, "Output", addSpace=True)
206        self.commitButton = OWGUI.button(box, self, "Commit",
207                                         callback=self.commit)
208        cb = OWGUI.checkBox(box, self, "autoCommit", "Commit on any change")
209        OWGUI.setStopper(self, self.commitButton, cb, "selectionChanged",
210                         self.commit)
211        OWGUI.rubber(self.controlArea)
212
213        self.filterLineEdit = OWGUIEx.lineEditHint(
214            self.mainArea, self, "filterString", "Filter",
215            caseSensitive=False, matchAnywhere=True,
216            callback=self.filter,  delimiters=" ")
217
218        splitter = QSplitter(Qt.Vertical, self.mainArea)
219        self.mainArea.layout().addWidget(splitter)
220        self.treeWidget = QTreeView(splitter)
221
222        self.treeWidget.setSelectionMode(QAbstractItemView.SingleSelection)
223        self.treeWidget.setRootIsDecorated(False)
224        self.treeWidget.setSortingEnabled(True)
225        self.treeWidget.setAlternatingRowColors(True)
226        self.treeWidget.setUniformRowHeights(True)
227
228        linkdelegate = LinkStyledItemDelegate(self.treeWidget)
229        self.treeWidget.setItemDelegateForColumn(1, linkdelegate)
230        self.treeWidget.setItemDelegateForColumn(8, linkdelegate)
231        self.treeWidget.setItemDelegateForColumn(
232            0, OWGUI.IndicatorItemDelegate(self.treeWidget,
233                                           role=Qt.DisplayRole))
234
235        proxyModel = MySortFilterProxyModel(self.treeWidget)
236        self.treeWidget.setModel(proxyModel)
237        self.treeWidget.selectionModel().selectionChanged.connect(
238            self.updateSelection
239        )
240        self.treeWidget.viewport().setMouseTracking(True)
241
242        splitterH = QSplitter(Qt.Horizontal, splitter)
243
244        box = OWGUI.widgetBox(splitterH, "Description")
245        self.infoGDS = OWGUI.widgetLabel(box, "")
246        self.infoGDS.setWordWrap(True)
247        OWGUI.rubber(box)
248
249        box = OWGUI.widgetBox(splitterH, "Sample Annotations")
250        self.annotationsTree = QTreeWidget(box)
251        self.annotationsTree.setHeaderLabels(
252            ["Type (Sample annotations)", "Sample count"]
253        )
254        self.annotationsTree.setRootIsDecorated(True)
255        box.layout().addWidget(self.annotationsTree)
256        self.annotationsTree.itemChanged.connect(
257            self.annotationSelectionChanged
258        )
259        self._annotationsUpdating = False
260        self.splitters = splitter, splitterH
261
262        for sp, setting in zip(self.splitters, self.splitterSettings):
263            sp.splitterMoved.connect(self.splitterMoved)
264            sp.restoreState(setting)
265
266        self.searchKeys = ["dataset_id", "title", "platform_organism",
267                           "description"]
268
269        self.gds = []
270        self.gds_info = None
271
272        self.resize(1000, 600)
273
274        self.setBlocking(True)
275        self.setEnabled(False)
276        self.progressBarInit()
277
278        self._executor = ThreadExecutor()
279
280        func = partial(get_gds_model,
281                       methodinvoke(self, "_setProgress", (float,)))
282        self._inittask = Task(function=func)
283        self._inittask.finished.connect(self._initializemodel)
284        self._executor.submit(self._inittask)
285
286        self._datatask = None
287
288    @pyqtSlot(float)
289    def _setProgress(self, value):
290        self.progressBarValue = value
291
292    def _initializemodel(self):
293        assert self.thread() is QThread.currentThread()
294        model, self.gds_info, self.gds = self._inittask.result()
295        model.setParent(self)
296
297        proxy = self.treeWidget.model()
298        proxy.setFilterKeyColumn(0)
299        proxy.setFilterRole(TextFilterRole)
300        proxy.setFilterCaseSensitivity(False)
301        proxy.setFilterFixedString(self.filterString)
302
303        proxy.setSourceModel(model)
304        proxy.sort(0, Qt.DescendingOrder)
305
306        self.progressBarFinished()
307        self.setBlocking(False)
308        self.setEnabled(True)
309
310        filter_items = " ".join(
311            gds[key] for gds in self.gds for key in self.searchKeys
312        )
313        tr_chars = ",.:;!?(){}[]_-+\\|/%#@$^&*<>~`"
314        tr_table = string.maketrans(tr_chars, " " * len(tr_chars))
315        filter_items = filter_items.translate(tr_table)
316
317        filter_items = sorted(set(filter_items.split(" ")))
318        filter_items = [item for item in filter_items if len(item) > 3]
319        self.filterLineEdit.setItems(filter_items)
320
321        if self.currentGds:
322            gdss = [(i, proxy.data(proxy.index(i, 1), Qt.DisplayRole))
323                    for i in range(proxy.rowCount())]
324            current = [i for i, variant in gdss
325                       if variant.isValid() and
326                       str(variant.toString()) == self.currentGds["dataset_id"]]
327            if current:
328                self.treeWidget.selectionModel().select(
329                    proxy.index(current[0], 0),
330                    QItemSelectionModel.Select |
331                    QItemSelectionModel.Rows
332                )
333
334        for i in range(8):
335            self.treeWidget.resizeColumnToContents(i)
336
337        self.treeWidget.setColumnWidth(
338            1, min(self.treeWidget.columnWidth(1), 300))
339        self.treeWidget.setColumnWidth(
340            2, min(self.treeWidget.columnWidth(2), 200))
341
342        self.updateInfo()
343
344    def updateInfo(self):
345        gds_info = self.gds_info
346        text = ("%i datasets\n%i datasets cached\n" %
347                (len(gds_info),
348                 len(glob.glob(serverfiles.localpath("GEO") + "/GDS*"))))
349        filtered = self.treeWidget.model().rowCount()
350        if len(self.gds) != filtered:
351            text += ("%i after filtering") % filtered
352        self.infoBox.setText(text)
353
354    def updateSelection(self, *args):
355        current = self.treeWidget.selectedIndexes()
356        mapToSource = self.treeWidget.model().mapToSource
357        current = [mapToSource(index).row() for index in current]
358        if current:
359            self.currentGds = self.gds[current[0]]
360            self.setAnnotations(self.currentGds)
361            self.infoGDS.setText(self.currentGds.get("description", ""))
362        else:
363            self.currentGds = None
364        self.commitIf()
365
366    def setAnnotations(self, gds):
367        self._annotationsUpdating = True
368        self.annotationsTree.clear()
369
370        annotations = defaultdict(set)
371        subsetscount = {}
372        for desc in gds["subsets"]:
373            annotations[desc["type"]].add(desc["description"])
374            subsetscount[desc["description"]] = str(len(desc["sample_id"]))
375
376        for type, subsets in annotations.items():
377            key = (gds["dataset_id"], type)
378            subsetItem = QTreeWidgetItem(self.annotationsTree, [type])
379            subsetItem.setFlags(subsetItem.flags() | Qt.ItemIsUserCheckable |
380                                Qt.ItemIsTristate)
381            subsetItem.setCheckState(
382                0, self.gdsSelectionStates.get(key, Qt.Checked)
383            )
384            subsetItem.key = key
385            for subset in subsets:
386                key = (gds["dataset_id"], type, subset)
387                item = QTreeWidgetItem(
388                    subsetItem, [subset, subsetscount.get(subset, "")]
389                )
390                item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
391                item.setCheckState(
392                    0, self.gdsSelectionStates.get(key, Qt.Checked)
393                )
394                item.key = key
395        self._annotationsUpdating = False
396        self.annotationsTree.expandAll()
397        for i in range(self.annotationsTree.columnCount()):
398            self.annotationsTree.resizeColumnToContents(i)
399
400    def annotationSelectionChanged(self, item, column):
401        if self._annotationsUpdating:
402            return
403        for i in range(self.annotationsTree.topLevelItemCount()):
404            item = self.annotationsTree.topLevelItem(i)
405            self.gdsSelectionStates[item.key] = item.checkState(0)
406            for j in range(item.childCount()):
407                child = item.child(j)
408                self.gdsSelectionStates[child.key] = child.checkState(0)
409
410    def filter(self):
411        filter_string = unicode(self.filterLineEdit.text(), errors="ignore")
412        proxyModel = self.treeWidget.model()
413        if proxyModel:
414            strings = filter_string.lower().strip().split()
415            proxyModel.setFilterFixedStrings(strings)
416            self.updateInfo()
417
418    def selectedSamples(self):
419        """
420        Return the currently selected sample annotations.
421
422        The return value is a list of selected (sample type, sample value)
423        tuples.
424
425        .. note:: if some Sample annotation type has no selected values.
426                  this method will return all values for it.
427
428        """
429        samples = []
430        unused_types = []
431        used_types = []
432        for stype in childiter(self.annotationsTree.invisibleRootItem()):
433            selected_values = []
434            all_values = []
435            for sval in childiter(stype):
436                value = (str(stype.text(0)), str(sval.text(0)))
437                if self.gdsSelectionStates.get(sval.key, True):
438                    selected_values.append(value)
439                all_values.append(value)
440            if selected_values:
441                samples.extend(selected_values)
442                used_types.append(str(stype.text(0)))
443            else:
444                # If no sample of sample type is selected we don't filter
445                # on it.
446                samples.extend(all_values)
447                unused_types.append(str(stype.text(0)))
448
449        return samples, used_types
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            _, groups = self.selectedSamples()
465            if len(groups) == 1 and self.outputRows:
466                sample_type = groups[0]
467
468            self.setEnabled(False)
469            self.setBlocking(True)
470
471            def get_data(gds_id, report_genes, transpose, sample_type):
472                gds = geo.GDS(gds_id)
473                data = gds.getdata(
474                    report_genes=report_genes, transpose=transpose,
475                    sample_type=sample_type
476                )
477                return data
478
479            get_data = partial(
480                get_data, self.currentGds["dataset_id"],
481                report_genes=self.mergeSpots,
482                transpose=self.outputRows,
483                sample_type=sample_type
484            )
485            self._datatask = Task(function=get_data)
486            self._datatask.finished.connect(self._on_dataready)
487            self._executor.submit(self._datatask)
488
489    def _on_dataready(self):
490        self.setEnabled(True)
491        self.setBlocking(False)
492
493        self.progressBarSet(50)
494
495        try:
496            data = self._datatask.result()
497        except urllib2.URLError as error:
498            self.error(0, "Error while connecting to the NCBI ftp server! %r" %
499                       error)
500            self._datatask = None
501            self.progressBarFinished()
502            return
503        self._datatask = None
504
505        samples, _ = self.selectedSamples()
506
507        self.warning(0)
508        message = None
509        if self.outputRows:
510            def samplesinst(ex):
511                out = []
512                for i, a in data.domain.get_metas().items():
513                    out.append((a.name, ex[i].value))
514                if data.domain.class_var.name != 'class':
515                    out.append((data.domain.class_var.name, ex[-1].value))
516                return out
517            samples = set(samples)
518
519            select = [1 if samples.issuperset(samplesinst(ex)) else 0
520                      for ex in data]
521            data = data.select(select)
522            if len(data) == 0:
523                message = "No samples with selected sample annotations."
524        else:
525            samples = set(samples)
526            domain = orange.Domain(
527                [attr for attr in data.domain.attributes
528                 if samples.issuperset(attr.attributes.items())],
529                data.domain.classVar
530            )
531            domain.addmetas(data.domain.getmetas())
532            if len(domain.attributes) == 0:
533                message = "No samples with selected sample annotations."
534            stypes = set(s[0] for s in samples)
535            for attr in domain.attributes:
536                attr.attributes = dict(
537                    (key, value) for key, value in attr.attributes.items()
538                    if key in stypes
539                )
540            data = orange.ExampleTable(domain, data)
541
542        if message is not None:
543            self.warning(0, message)
544
545        data_hints.set_hint(data, "taxid", self.currentGds.get("taxid", ""),
546                            10.0)
547        data_hints.set_hint(data, "genesinrows", self.outputRows, 10.0)
548
549        self.progressBarFinished()
550        self.send("Expression Data", data)
551
552        model = self.treeWidget.model().sourceModel()
553        row = self.gds.index(self.currentGds)
554
555        model.setData(model.index(row, 0),  QVariant(" "), Qt.DisplayRole)
556
557        self.updateInfo()
558        self.selectionChanged = False
559
560    def splitterMoved(self, *args):
561        self.splitterSettings = [str(sp.saveState()) for sp in self.splitters]
562
563    def onDeleteWidget(self):
564        if self._inittask:
565            self._inittask.future().cancel()
566            self._inittask.finished.disconnect(self._initializemodel)
567        if self._datatask:
568            self._datatask.future().cancel()
569            self._datatask.finished.disconnect(self._on_dataready)
570        self._executor.shutdown(wait=False)
571
572        super(OWGEODatasets, self).onDeleteWidget()
573
574
575def get_gds_model(progress=lambda val: None):
576    """
577    Initialize and return a GDS datasets model.
578
579    :param progress: A progress callback.
580    :rval tuple:
581        A tuple of (QStandardItemModel, geo.GDSInfo, [geo.GDS])
582
583    .. note::
584        The returned QStandardItemModel's thread affinity is set to
585        the GUI thread.
586
587    """
588    progress(1)
589    info = geo.GDSInfo()
590    search_keys = ["dataset_id", "title", "platform_organism", "description"]
591    cache_dir = serverfiles.localpath(geo.DOMAIN)
592    gds_link = "http://www.ncbi.nlm.nih.gov/sites/GDSbrowser?acc={0}"
593    pm_link = "http://www.ncbi.nlm.nih.gov/pubmed/{0}"
594    gds_list = []
595
596    def is_cached(gds):
597        return os.path.exists(os.path.join(cache_dir, gds["dataset_id"]) +
598                              ".soft.gz")
599
600    def item(displayvalue, item_values={}):
601        item = QStandardItem()
602        item.setData(displayvalue, Qt.DisplayRole)
603        for role, value in item_values.iteritems():
604            item.setData(value, role)
605        return item
606
607    def gds_to_row(gds):
608        #: Text for easier full search.
609        search_text = unicode(
610            " | ".join([gds.get(key, "").lower()
611                        for key in search_keys]),
612            errors="ignore"
613        )
614        row = [
615            item(" " if is_cached(gds) else "",
616                 {TextFilterRole: search_text}),
617            item(gds["dataset_id"],
618                 {LinkRole: gds_link.format(gds["dataset_id"])}),
619            item(gds["title"]),
620            item(gds["platform_organism"]),
621            item(len(gds["samples"])),
622            item(gds["feature_count"]),
623            item(gds["gene_count"]),
624            item(len(gds["subsets"])),
625            item(gds.get("pubmed_id", ""),
626                 {LinkRole: pm_link.format(gds["pubmed_id"])
627                            if gds.get("pubmed_id")
628                            else QVariant()})
629        ]
630        return row
631
632    model = QStandardItemModel()
633    model.setHorizontalHeaderLabels(
634        ["", "ID", "Title", "Organism", "Samples", "Features",
635         "Genes", "Subsets", "PubMedID"]
636    )
637    progress(20)
638    for gds in info.values():
639        model.appendRow(gds_to_row(gds))
640
641        gds_list.append(gds)
642
643    progress(50)
644
645    if QThread.currentThread() is not QCoreApplication.instance().thread():
646        model.moveToThread(QCoreApplication.instance().thread())
647    return model, info, gds_list
648
649
650if __name__ == "__main__":
651    app = QApplication(sys.argv)
652    w = OWGEODatasets()
653    w.show()
654    app.exec_()
655    w.saveSettings()
Note: See TracBrowser for help on using the repository browser.