source: orange-bioinformatics/widgets/OWQualityControl.py @ 1581:877a0e7feba9

Revision 1581:877a0e7feba9, 24.2 KB checked in by ales_erjavec, 2 years ago (diff)

Fixed scene alignment (align left to prevent scene jittering when resizing). Fixed an RuntimeError due to a deleted QGraphicsWidget whose reference was still held from Python. Fixed an error when changing the distance measure with no input to the widget.

Line 
1"""
2<name>Quality Control</name>
3<description>Experiment quality control</description>
4
5"""
6
7import sys
8
9import Orange
10
11from OWWidget import *
12from OWItemModels import PyListModel, safe_text
13from OWGraphics import GraphicsSimpleTextLayoutItem
14import OWGUI
15
16from OWGenotypeDistances import SetContextHandler
17
18import obiExperiments as exp
19import numpy
20
21from collections import defaultdict
22from contextlib import contextmanager
23from pprint import pprint
24
25DEBUG = False
26
27
28@contextmanager
29def widget_disable(widget):
30    """A context to disable the widget (enabled property) 
31    """
32    widget.setEnabled(False)
33    try:
34        yield
35    finally:
36        widget.setEnabled(True)
37   
38   
39@contextmanager
40def disable_updates(widget):
41    """
42    A context that sets '_disable_updates' member to True, and
43    then restores it.
44   
45    """
46    widget._disable_updates = True
47    try:
48        yield
49    finally:
50        widget._disable_updates = False
51
52
53def group_label(splits, groups):
54    """Return group label.
55    """
56    labels = ["%s=%s" % (split, group) \
57              for split, group in zip(splits, groups)]
58    return " | ".join(labels)
59
60
61def sort_label(sort, attr):
62    """Return within group sorted items label for attribute.
63    """
64    items = [(key, attr.attributes.get(key, "?")) \
65             for key in sort]
66    labels = ["%s=%s" % tuple(item) for item in items]
67    return " | ".join(labels)
68
69
70def float_if_posible(val):
71    """Return val as float if possible otherwise return the value unchanged.
72   
73    """
74    try:
75        return float(val)
76    except ValueError:
77        return val
78   
79   
80def experiment_description(feature):
81    """Return experiment description from ``feature.attributes``.
82    """
83    text = ""
84    if feature.attributes:
85        items = feature.attributes.items()
86        items = [(safe_text(key), safe_text(value)) for key, value in items]
87        labels = map("%s = %s".__mod__, items)
88        text += "<b>%s</b><br/>" % safe_text(feature.name)
89        text += "<br/>".join(labels)
90    return text
91
92
93class OWQualityControl(OWWidget):
94    contextHandlers = {"": SetContextHandler("")}
95    settingsList = ["selected_distance_index"]
96   
97    DISTANCE_FUNCTIONS = [("Distance from Pearson correlation",
98                           exp.dist_pcorr),
99                          ("Euclidean distance", 
100                           exp.dist_eucl),
101                          ("Distance from Spearman correlation", 
102                           exp.dist_spearman)]
103
104    def __init__(self, parent=None, signalManager=None,
105                 title="Quality Control"):
106        OWWidget.__init__(self, parent, signalManager, title,
107                          wantGraph=True)
108
109        self.inputs = [("Experiment Data", Orange.data.Table, self.set_data)]
110
111        ## Settings
112        self.selected_distance_index = 0
113
114        ## Attributes
115        self.data = None
116        self.distances = None
117        self.groups = None
118        self.unique_pos = None
119        self.base_group_index = 0
120
121        ## GUI
122        box = OWGUI.widgetBox(self.controlArea, "Info")
123        self.info_box = OWGUI.widgetLabel(box, "\n")
124
125        ## Separate By box
126        box = OWGUI.widgetBox(self.controlArea, "Separate By")
127        self.split_by_model = PyListModel()
128        self.split_by_view = QListView()
129        self.split_by_view.setSelectionMode(QListView.ExtendedSelection)
130        self.split_by_view.setModel(self.split_by_model)
131        box.layout().addWidget(self.split_by_view)
132
133        self.connect(self.split_by_view.selectionModel(),
134                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
135                     self.on_split_key_changed)
136
137        ## Sort By box
138        box = OWGUI.widgetBox(self.controlArea, "Sort By")
139        self.sort_by_model = PyListModel()
140        self.sort_by_view = QListView()
141        self.sort_by_view.setSelectionMode(QListView.ExtendedSelection)
142        self.sort_by_view.setModel(self.sort_by_model)
143        box.layout().addWidget(self.sort_by_view)
144       
145        self.connect(self.sort_by_view.selectionModel(),
146                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
147                     self.on_sort_key_changed)
148
149        ## Distance box
150        box = OWGUI.widgetBox(self.controlArea, "Distance Measure")
151        OWGUI.comboBox(box, self, "selected_distance_index",
152                       items=[t[0] for t in self.DISTANCE_FUNCTIONS],
153                       callback=self.on_distance_measure_changed)
154
155        self.connect(self.graphButton,
156                     SIGNAL("clicked()"),
157                     self.save_graph)
158       
159        self.scene = QGraphicsScene()
160        self.scene_view = QualityGraphicsView(self.scene)
161        self.scene_view.setRenderHints(QPainter.Antialiasing)
162        self.scene_view.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
163        self.mainArea.layout().addWidget(self.scene_view)
164       
165        self.connect(self.scene_view,
166                     SIGNAL("view_size_changed(QSize)"),
167                     self.on_view_resize)
168
169        self._disable_updates = False
170        self._cached_distances = {}
171        self._base_index_hints = {}
172        self.main_widget = None
173       
174        self.resize(800, 600)
175
176    def clear(self):
177        """Clear the widget state.
178        """
179        self.data = None
180        self.distances = None
181        self.groups = None
182        self.unique_pos = None
183       
184        with disable_updates(self):
185            self.split_by_model[:] = []
186            self.sort_by_model[:] = []
187       
188        self.main_widget = None
189        self.scene.clear()
190        self.info_box.setText("\n")
191        self._cached_distances = {}
192
193    def set_data(self, data=None):
194        """Set input experiment data.
195        """
196        self.clear()
197       
198        self.error(0)
199        self.warning(0)
200       
201        if data is not None:
202            keys = self.get_suitable_keys(data)
203            if not keys:
204                self.error(0, "Data has no suitable feature labels.")
205                data = None
206               
207        self.data = data
208
209    def handleNewSignals(self):
210        """Called after all signals have been set.
211        """
212        if self.data:
213            self.on_new_data()
214        else:
215            self.closeContext("")
216            self.clear()
217
218    def update_label_candidates(self):
219        """Update the label candidates selection GUI
220        (Group/Sort By views).
221       
222        """
223        keys = self.get_suitable_keys(self.data)
224        with disable_updates(self):
225            self.split_by_model[:] = keys
226            self.sort_by_model[:] = keys
227       
228    def get_suitable_keys(self, data):
229        """ Return suitable attr label keys from the data where
230        the key has at least two unique values in the data.
231       
232        """
233        attrs = [attr.attributes.items() for attr in data.domain.attributes]
234        attrs  = reduce(list.__add__, attrs, [])
235        # in case someone put non string values in attributes dict
236        attrs = [(str(key), str(value)) for key, value in attrs]
237        attrs = set(attrs)
238        values = defaultdict(set)
239        for key, value in attrs:
240            values[key].add(value)
241        keys = [key for key in values if len(values[key]) > 1]
242        return keys
243
244    def selected_split_by_labels(self):
245        """Return the current selected split labels.
246        """
247        sel_m = self.split_by_view.selectionModel()
248        indices = [r.row() for r in sel_m.selectedRows()]
249        return [self.sort_by_model[i] for i in indices]
250
251    def selected_sort_by_labels(self):
252        """Return the current selected sort labels
253        """
254        sel_m = self.sort_by_view.selectionModel()
255        indices = [r.row() for r in sel_m.selectedRows()]
256        return [self.sort_by_model[i] for i in indices]
257
258    def selected_distance(self):
259        """Return the selected distance function.
260        """
261        return self.DISTANCE_FUNCTIONS[self.selected_distance_index][1]
262   
263    def selected_base_group_index(self):
264        """Return the selected base group index
265        """
266        return self.base_group_index
267   
268    def selected_base_indices(self, base_group_index=None):
269        indices = []
270        for g, ind in self.groups:
271            if base_group_index is None:
272                label = group_label(self.selected_split_by_labels(), g)
273                ind = [i for i in ind if i is not None]
274                i = self._base_index_hints.get(label, ind[0] if ind else None)
275            else:
276                i = ind[base_group_index]
277            indices.append(i)
278        return indices
279
280    def on_new_data(self):
281        """We have new data and need to recompute all.
282        """
283        self.closeContext("")
284       
285        self.update_label_candidates()
286        self.info_box.setText("%s genes \n%s experiments" % (
287                                len(self.data), 
288                                len(self.data.domain.attributes)
289                                )
290                              )
291       
292        self.base_group_index = 0
293       
294        keys = self.get_suitable_keys(self.data)
295        self.openContext("", keys)
296       
297        ## Restore saved context
298        context = self.currentContexts[""]
299        split_by_labels = getattr(context, "split_by_labels", set())
300        sort_by_labels = getattr(context, "sort_by_labels", set())
301       
302        def select(model, selection_model, selected_items):
303            """Select items in a Qt item model view
304            """
305            all_items = list(model)
306            try:
307                indices = [all_items.index(item) for item in selected_items]
308            except:
309                indices = []
310            for ind in indices:
311                selection_model.select(model.index(ind), 
312                                       QItemSelectionModel.Select)
313               
314        with disable_updates(self):
315            select(self.split_by_view.model(),
316                   self.split_by_view.selectionModel(),
317                   split_by_labels)
318           
319            select(self.sort_by_view.model(),
320                   self.sort_by_view.selectionModel(),
321                   sort_by_labels)
322       
323        with widget_disable(self):
324            self.split_and_update()
325       
326    def on_split_key_changed(self, *args):
327        """Split key has changed
328        """
329        with widget_disable(self):
330            if not self._disable_updates:
331                self.base_group_index = 0
332                context = self.currentContexts[""]
333                context.split_by_labels = self.selected_split_by_labels()
334                self.split_and_update()
335   
336    def on_sort_key_changed(self, *args):
337        """Sort key has changed
338        """
339        with widget_disable(self):
340            if not self._disable_updates:
341                self.base_group_index = 0
342                context = self.currentContexts[""]
343                context.sort_by_labels = self.selected_sort_by_labels()
344                self.split_and_update()
345       
346    def on_distance_measure_changed(self):
347        """Distance measure has changed
348        """
349        if self.data is not None:
350            with widget_disable(self):
351                self.update_distances()
352                self.replot_experiments()
353       
354    def on_view_resize(self, size):
355        """The view with the quality plot has changed
356        """
357        if self.main_widget:
358            current = self.main_widget.size()
359            self.main_widget.resize(size.width() - 6, 
360                                    current.height())
361           
362            self.scene.setSceneRect(self.scene.itemsBoundingRect())
363       
364    def on_rug_item_clicked(self, item):
365        """An ``item`` in the quality plot has been clicked.
366        """
367        update = False
368        sort_by_labels = self.selected_sort_by_labels()
369        if sort_by_labels and item.in_group:
370            ## The item is part of the group
371            if item.group_index != self.base_group_index:
372                self.base_group_index = item.group_index
373                update = True
374           
375        else:
376            if sort_by_labels:
377                # If the user clicked on an background item it
378                # invalidates the sorted labels selection
379                with disable_updates(self):
380                    self.sort_by_view.selectionModel().clear()
381                    update = True
382                   
383            index = item.index
384            group = item.group
385            label = group_label(self.selected_split_by_labels(), group)
386           
387            if self._base_index_hints.get(label, 0) != index:
388                self._base_index_hints[label] = index
389                update = True
390           
391        if update:
392            with widget_disable(self):
393                self.split_and_update()
394       
395    def split_and_update(self):
396        """
397        Split the data based on the selected sort/split labels
398        and update the quality plot.
399       
400        """
401        split_labels = self.selected_split_by_labels()
402        sort_labels = self.selected_sort_by_labels()
403       
404        self.warning(0)
405        if not split_labels:
406            self.warning(0, "No separate by label selected.")
407           
408        self.groups, self.unique_pos = \
409                exp.separate_by(self.data, split_labels,
410                                consider=sort_labels,
411                                add_empty=True)
412       
413       
414        self.groups = sorted(self.groups.items(),
415                             key=lambda t: map(float_if_posible, t[0]))
416        self.unique_pos = sorted(self.unique_pos.items(),
417                                 key=lambda t: map(float_if_posible, t[0]))
418       
419       
420        if self.groups:
421            if sort_labels:
422                group_base = self.selected_base_group_index()
423                base_indices = self.selected_base_indices(group_base)
424            else:
425                base_indices = self.selected_base_indices()
426            self.update_distances(base_indices)
427            self.replot_experiments()
428
429    def get_cached_distances(self, measure):
430        if measure not in self._cached_distances:
431            attrs = self.data.domain.attributes
432            mat = Orange.misc.SymMatrix(len(attrs))
433            self._cached_distances[measure] = \
434                (mat, set(zip(range(len(attrs)), range(len(attrs)))))
435           
436        return self._cached_distances[measure]
437       
438    def get_cached_distance(self, measure, i, j):
439        matrix, computed = self.get_cached_distances(measure)
440        key = (i, j) if i < j else (j, i) 
441        if key in computed:
442            return matrix[i, j]
443        else:
444            return None
445       
446    def get_distance(self, measure, i, j):
447        d = self.get_cached_distance(measure, i, j)
448        if d is None:
449            vec_i = exp.linearize(self.data, [i])
450            vec_j = exp.linearize(self.data, [j])
451            d = distance(vec_i, vec_j)
452            mat, computed = self.get_cached_distances(measure)
453            mat[i, j] = d
454            key = key = (i, j) if i < j else (j, i)
455            computed.add(key)
456        return d
457   
458    def store_distance(self, measure, i, j, dist):
459        matrix, computed = self.get_cached_distances(measure)
460        key = key = (i, j) if i < j else (j, i)
461        matrix[i, j] = dist
462        computed.add(key)
463       
464    def update_distances(self, base_indices=()):
465        """Recompute the experiment distances.
466        """
467        distance = self.selected_distance()
468        if base_indices == ():
469            base_group_index = self.selected_base_group_index()
470            base_indices = [ind[base_group_index] \
471                            for _, ind in self.groups]
472           
473        assert(len(base_indices) == len(self.groups)) 
474       
475        base_distances = []
476        attributes = self.data.domain.attributes
477        pb = OWGUI.ProgressBar(self, len(self.groups) * \
478                               len(attributes))
479       
480        cached_distances, filled_set = self.get_cached_distances(distance)
481       
482        for (group, indices), base_index in zip(self.groups, base_indices):
483            # Base column of the group
484            if base_index is not None:
485                base_vec = exp.linearize(self.data, [base_index])
486                distances = []
487                # Compute the distances between base column
488                # and all the rest data columns.
489                for i in range(len(attributes)):
490                    if i == base_index:
491                        distances.append(0.0)
492                    elif self.get_cached_distance(distance, i, base_index) is not None:
493                        distances.append(self.get_cached_distance(distance, i, base_index))
494                    else:
495                        vec_i = exp.linearize(self.data, [i])
496                        dist = distance(base_vec, vec_i)
497                        self.store_distance(distance, i, base_index, dist)
498                        distances.append(dist)
499                    pb.advance()
500                   
501                base_distances.append(distances)
502            else:
503                base_distances.append(None)
504               
505        pb.finish()
506        self.distances = base_distances
507
508    def replot_experiments(self):
509        """Replot the whole quality plot.
510        """
511        self.scene.clear()
512        labels = []
513       
514        max_dist = numpy.max(filter(None, self.distances))
515        rug_widgets = []
516       
517        group_pen = QPen(QColor(0, 0, 0))
518        group_pen.setWidth(2)
519        group_pen.setCapStyle(Qt.RoundCap)
520        background_pen = QPen(QColor(0, 0, 250, 150))
521        background_pen.setWidth(1)
522        background_pen.setCapStyle(Qt.RoundCap)
523       
524        main_widget = QualityControlWidget()
525        layout = QGraphicsGridLayout()
526        split_by = self.selected_split_by_labels()
527        sort_by = self.selected_sort_by_labels()
528        attributes = self.data.domain.attributes
529        if self.data:
530            for (group, indices), dist_vec in zip(self.groups, self.distances):
531                indices_set = set(indices)
532                rug_items = []
533                if dist_vec is not None:
534                    for i, attr in enumerate(attributes):
535                        # Is this a within group distance or background
536                        in_group = i in indices_set
537                        if in_group:
538                            rug_item = ClickableRugItem(dist_vec[i] / max_dist,
539                                           1.0, self.on_rug_item_clicked)
540                            rug_item.setPen(group_pen)
541                            tooltip = experiment_description(attr)
542                            rug_item.setToolTip(tooltip)
543                            rug_item.group_index = indices.index(i)
544                            rug_item.setZValue(rug_item.zValue() + 1)
545                        else:
546                            rug_item = ClickableRugItem(dist_vec[i] / max_dist,
547                                           0.85, self.on_rug_item_clicked)
548                            rug_item.setPen(background_pen)
549                            tooltip = experiment_description(attr)
550                            rug_item.setToolTip(tooltip)
551                           
552                        rug_item.group = group
553                        rug_item.index = i
554                        rug_item.in_group = in_group
555                       
556                        rug_items.append(rug_item)
557                   
558                rug_widget = RugGraphicsWidget()
559                rug_widget.set_rug(rug_items)
560               
561                rug_widgets.append(rug_widget)
562               
563                label = group_label(self.selected_split_by_labels(), group)
564                label_item = QGraphicsSimpleTextItem(label, main_widget)
565                label_item = GraphicsSimpleTextLayoutItem(label_item)
566                label_item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
567                labels.append(label_item)
568       
569        for i, (label, w) in enumerate(zip(labels, rug_widgets)):
570            layout.addItem(label, i, 0, Qt.AlignVCenter)
571            layout.addItem(w, i, 1)
572            layout.setRowMaximumHeight(i, 30)
573           
574        main_widget.setLayout(layout)
575        self.scene.addItem(main_widget)
576        main_widget.show()
577        self.main_widget = main_widget
578        self.rug_widgets = rug_widgets
579        self.labels = labels
580        self.on_view_resize(self.scene_view.size())
581       
582    def save_graph(self):
583        from OWDlgs import OWChooseImageSizeDlg
584        dlg = OWChooseImageSizeDlg(self.scene, parent=self)
585        dlg.exec_()
586
587
588class RugGraphicsWidget(QGraphicsWidget):
589    def __init__(self, parent=None, rug=None):
590        QGraphicsWidget.__init__(self, parent)
591        self.rug_items = []
592        self.set_rug(rug)
593        self.setMaximumHeight(30)
594        self.setMinimumHeight(30)
595       
596    def clear(self):
597        """
598        Clear all rug items from this widget and remove them
599        from the scene.
600         
601        """
602        for item in self.rug_items:
603            item.setParent(None)
604            if self.scene() is not None:
605                self.scene().removeItem(item)
606
607    def set_rug(self, rug):
608        """
609        Set the rug items.
610       
611        ``rug`` must be a list of floats or already initialized
612        instances of RugItem. The widget takes ownership of all
613        items.
614         
615        """
616        rug = rug if rug is not None else []
617        self.clear()
618        self.add_rug(rug)
619
620    def add_rug(self, rug):
621        """
622        Add rug items.
623       
624        See :obj:`set_rug`
625       
626        """
627        items = []
628        for item in rug:
629            if isinstance(item, float):
630                item = RugItem(value=item)
631                items.append(item)
632            elif isinstance(item, RugItem):
633                items.append(item)
634
635        for item in items:
636            item.setParentItem(self)
637
638        self.rug_items.extend(items)
639
640        self.update_rug_geometry()
641       
642    def update_rug_geometry(self):
643        """Recompute the rug items positions within this widget.
644        """
645        size = self.size()
646        height = size.height()
647        width = size.width()
648       
649        for item in self.rug_items:
650            offset = (1.0 - item.height) * height / 2.0
651            item.setPos(width * item.value, 0)
652            item.setLine(0., offset, 0., height - offset)
653
654    def resizeEvent(self, event):
655        """Reimplemented from QGraphicsWidget
656        """
657        QGraphicsWidget.resizeEvent(self, event)
658        self.update_rug_geometry()
659
660    def setGeometry(self, geom):
661        """Reimplemented from QGraphicsWidget
662        """
663        QGraphicsWidget.setGeometry(self, geom)
664
665
666class RugItem(QGraphicsLineItem):
667    def __init__(self, value, height):
668        QGraphicsLineItem.__init__(self)
669        self.value = value
670        self.height = height
671
672    def set_height(self, height):
673        """Set the height of this item (in ratio of the rug height)
674        """
675        self.height = height
676       
677class ClickableRugItem(RugItem):
678    def __init__(self, value, height, on_pressed):
679        RugItem.__init__(self, value, height)
680        self.on_pressed = on_pressed
681        self.setAcceptedMouseButtons(Qt.LeftButton)
682        self.setAcceptHoverEvents(True)
683       
684    def mousePressEvent(self, event):
685        if event.button() == Qt.LeftButton and self.on_pressed:
686            self.on_pressed(self)
687           
688    def hoverEnterEvent(self, event):
689        pen = QPen(self.pen())
690        pen.setWidthF(3)
691        self.setPen(pen)
692        return RugItem.hoverEnterEvent(self, event)
693   
694    def hoverLeaveEvent(self, event):
695        pen = QPen(self.pen())
696        pen.setWidth(2)
697        self.setPen(pen)
698        return RugItem.hoverLeaveEvent(self, event)
699
700
701class QualityGraphicsView(QGraphicsView):
702    def resizeEvent(self, event):
703        QGraphicsView.resizeEvent(self, event)
704        self.emit(SIGNAL("view_size_changed(QSize)"),
705                  event.size())
706
707
708class QualityControlWidget(QGraphicsWidget):
709    if DEBUG:
710        def paint(self, painter, options, widget=0):
711            rect =  self.geometry()
712            rect.translate(-self.pos())
713            painter.drawRect(rect)
714           
715if __name__ == "__main__":
716    app = QApplication(sys.argv)
717    w = OWQualityControl()
718#    data = Orange.data.Table("doc:dicty-abc-sample.tab")
719    data = Orange.data.Table("doc:pipa.tab")
720
721    w.set_data(data)
722    w.show()
723    w.handleNewSignals()
724    app.exec_()
725    w.set_data(None)
726    w.handleNewSignals()
727    w.saveSettings()
Note: See TracBrowser for help on using the repository browser.