def test_task(self): results = [] task = Task(function=QThread.currentThread) task.resultReady.connect(results.append) task.start() self.app.processEvents() self.assertSequenceEqual(results, [QThread.currentThread()]) thread = QThread() thread.start() try: task = Task(function=QThread.currentThread) task.moveToThread(thread) self.assertIsNot(task.thread(), QThread.currentThread()) self.assertIs(task.thread(), thread) results = Future() def record(value): # record the result value and the calling thread results.set_result((QThread.currentThread(), value)) task.resultReady.connect(record, Qt.DirectConnection) task.start() f = task.future() emit_thread, thread_ = results.result(3) self.assertIs(f.result(3), thread) self.assertIs(emit_thread, thread) self.assertIs(thread_, thread) finally: thread.quit() thread.wait()
class X11EventPoller(QObject): keyPressed = pyqtSignal(object, object) def __init__(self): QObject.__init__(self) self._display = Display() self._thread = QThread() self.moveToThread(self._thread) self._thread.start() def start(self): QTimer.singleShot(0, self.run) def run(self): ctx = self._display.record_create_context(0, [record.CurrentClients], [{ 'core_requests': (0, 0), 'core_replies': (0, 0), 'ext_requests': (0, 0, 0, 0), 'ext_replies': (0, 0, 0, 0), 'delivered_events': (0, 0), 'device_events': (X.KeyPress, X.KeyRelease), 'errors': (0, 0), 'client_started': False, 'client_died': False, }]) self._display.record_enable_context(ctx, self._record_callback) self._display.record_free_context(ctx) def _record_callback(self, reply): QApplication.processEvents() if reply.category != record.FromServer: return if reply.client_swapped: # received swapped protocol data, cowardly ignored return if not len(reply.data) or reply.data[0] < 2: # not an event return data = reply.data while len(data): event, data = rq.EventField(None).parse_binary_value(data, self._display.display, None, None) self.keyPressed.emit(event, data) def destroy(self): # self._thread.terminate() self._thread.wait()
class OWSOM(OWWidget): name = "Self-Organizing Map" description = "Computation of self-organizing map." icon = "icons/SOM.svg" keywords = ["SOM"] class Inputs: data = Input("Data", Table) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) settingsHandler = DomainContextHandler() auto_dimension = Setting(True) size_x = Setting(10) size_y = Setting(10) hexagonal = Setting(1) initialization = Setting(0) attr_color = ContextSetting(None) size_by_instances = Setting(True) pie_charts = Setting(False) selection = Setting(None, schema_only=True) graph_name = "view" _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2) _grid_pen.setCosmetic(True) OptControls = namedtuple( "OptControls", ("shape", "auto_dim", "spin_x", "spin_y", "initialization", "start")) class Warning(OWWidget.Warning): ignoring_disc_variables = Msg("SOM ignores discrete variables.") missing_colors = \ Msg("Some data instances have undefined value of '{}'.") missing_values = \ Msg("{} data instance{} with undefined value(s) {} not shown.") single_attribute = Msg("Data contains a single numeric column.") class Error(OWWidget.Error): no_numeric_variables = Msg("Data contains no numeric columns.") no_defined_rows = Msg("All rows contain at least one undefined value.") def __init__(self): super().__init__() self.__pending_selection = self.selection self._optimizer = None self._optimizer_thread = None self.stop_optimization = False self.data = self.cont_x = None self.cells = self.member_data = None self.selection = None self.colors = self.thresholds = self.bin_labels = None self._set_input_summary(None) self._set_output_summary(None) box = gui.vBox(self.controlArea, box="SOM") shape = gui.comboBox(box, self, "", items=("Hexagonal grid", "Square grid")) shape.setCurrentIndex(1 - self.hexagonal) box2 = gui.indentedBox(box, 10) auto_dim = gui.checkBox(box2, self, "auto_dimension", "Set dimensions automatically", callback=self.on_auto_dimension_changed) self.manual_box = box3 = gui.hBox(box2) spinargs = dict(value="", widget=box3, master=self, minv=5, maxv=100, step=5, alignment=Qt.AlignRight) spin_x = gui.spin(**spinargs) spin_x.setValue(self.size_x) gui.widgetLabel(box3, "×") spin_y = gui.spin(**spinargs) spin_y.setValue(self.size_y) gui.rubber(box3) self.manual_box.setEnabled(not self.auto_dimension) initialization = gui.comboBox(box, self, "initialization", items=("Initialize with PCA", "Random initialization", "Replicable random")) start = gui.button(box, self, "Restart", callback=self.restart_som_pressed, sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)) self.opt_controls = self.OptControls(shape, auto_dim, spin_x, spin_y, initialization, start) box = gui.vBox(self.controlArea, "Color") gui.comboBox(box, self, "attr_color", searchable=True, callback=self.on_attr_color_change, model=DomainModel(placeholder="(Same color)", valid_types=DomainModel.PRIMITIVE)) gui.checkBox(box, self, "pie_charts", label="Show pie charts", callback=self.on_pie_chart_change) gui.checkBox(box, self, "size_by_instances", label="Size by number of instances", callback=self.on_attr_size_change) gui.rubber(self.controlArea) self.scene = QGraphicsScene(self) self.view = SomView(self.scene) self.view.setMinimumWidth(400) self.view.setMinimumHeight(400) self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.view.setRenderHint(QPainter.Antialiasing) self.view.selection_changed.connect(self.on_selection_change) self.view.selection_moved.connect(self.on_selection_move) self.view.selection_mark_changed.connect(self.on_selection_mark_change) self.mainArea.layout().addWidget(self.view) self.elements = None self.grid = None self.grid_cells = None self.legend = None @Inputs.data def set_data(self, data): def prepare_data(): if len(cont_attrs) < len(attrs): self.Warning.ignoring_disc_variables() if len(cont_attrs) == 1: self.Warning.single_attribute() x = Table.from_table(Domain(cont_attrs), data).X if sp.issparse(x): self.data = data self.cont_x = x.tocsr() else: mask = np.all(np.isfinite(x), axis=1) if not np.any(mask): self.Error.no_defined_rows() else: if np.all(mask): self.data = data self.cont_x = x.copy() else: self.data = data[mask] self.cont_x = x[mask] self.cont_x -= np.min(self.cont_x, axis=0)[None, :] sums = np.sum(self.cont_x, axis=0)[None, :] sums[sums == 0] = 1 self.cont_x /= sums def set_warnings(): missing = len(data) - len(self.data) if missing == 1: self.Warning.missing_values(1, "", "is") elif missing > 1: self.Warning.missing_values(missing, "s", "are") self.stop_optimization_and_wait() self.closeContext() self.clear() self.Error.clear() self.Warning.clear() if data is not None: attrs = data.domain.attributes cont_attrs = [var for var in attrs if var.is_continuous] if not cont_attrs: self.Error.no_numeric_variables() else: prepare_data() if self.data is not None: self.controls.attr_color.model().set_domain(data.domain) self.attr_color = data.domain.class_var set_warnings() self.openContext(self.data) self.set_color_bins() self.create_legend() self.recompute_dimensions() self._set_input_summary(data) self.start_som() def _set_input_summary(self, data): summary = len(data) if data else self.info.NoInput details = format_summary_details(data) if data else "" self.info.set_input_summary(summary, details) def _set_output_summary(self, output): summary = len(output) if output else self.info.NoOutput details = format_summary_details(output) if output else "" self.info.set_output_summary(summary, details) def clear(self): self.data = self.cont_x = None self.cells = self.member_data = None self.attr_color = None self.colors = self.thresholds = self.bin_labels = None if self.elements is not None: self.scene.removeItem(self.elements) self.elements = None self.clear_selection() self.controls.attr_color.model().set_domain(None) self.Warning.clear() self.Error.clear() def recompute_dimensions(self): if not self.auto_dimension or self.cont_x is None: return dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0]))))) self.opt_controls.spin_x.setValue(dim) self.opt_controls.spin_y.setValue(dim) def on_auto_dimension_changed(self): self.manual_box.setEnabled(not self.auto_dimension) if self.auto_dimension: self.recompute_dimensions() else: spin_x = self.opt_controls.spin_x spin_y = self.opt_controls.spin_y dimx = int(5 * np.round(spin_x.value() / 5)) dimy = int(5 * np.round(spin_y.value() / 5)) spin_x.setValue(dimx) spin_y.setValue(dimy) def on_attr_color_change(self): self.controls.pie_charts.setEnabled(self.attr_color is not None) self.set_color_bins() self.create_legend() self.rescale() self._redraw() def on_attr_size_change(self): self._redraw() def on_pie_chart_change(self): self._redraw() def clear_selection(self): self.selection = None self.redraw_selection() def on_selection_change(self, selection, action=SomView.SelectionSet): if self.data is None: # clicks on empty canvas return if self.selection is None: self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16) if action == SomView.SelectionSet: self.selection[:] = 0 self.selection[selection] = 1 elif action == SomView.SelectionAddToGroup: self.selection[selection] = max(1, np.max(self.selection)) elif action == SomView.SelectionNewGroup: self.selection[selection] = 1 + np.max(self.selection) elif action & SomView.SelectionRemove: self.selection[selection] = 0 self.redraw_selection() self.update_output() def on_selection_move(self, event: QKeyEvent): if self.selection is None or not np.any(self.selection): if event.key() in (Qt.Key_Right, Qt.Key_Down): x = y = 0 else: x = self.size_x - 1 y = self.size_y - 1 else: x, y = np.nonzero(self.selection) if len(x) > 1: return if event.key() == Qt.Key_Up and y > 0: y -= 1 if event.key() == Qt.Key_Down and y < self.size_y - 1: y += 1 if event.key() == Qt.Key_Left and x: x -= 1 if event.key() == Qt.Key_Right and x < self.size_x - 1: x += 1 x -= self.hexagonal and x == self.size_x - 1 and y % 2 if self.selection is not None and self.selection[x, y]: return selection = np.zeros(self.grid_cells.shape, dtype=bool) selection[x, y] = True self.on_selection_change(selection) def on_selection_mark_change(self, marks): self.redraw_selection(marks=marks) def redraw_selection(self, marks=None): if self.grid_cells is None: return sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2) sel_pen.setCosmetic(True) mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4) mark_pen.setCosmetic(True) pens = [self._grid_pen, sel_pen] mark_brush = QBrush(QColor(224, 255, 255)) sels = self.selection is not None and np.max(self.selection) palette = LimitedDiscretePalette(number_of_colors=sels + 1) brushes = [QBrush(Qt.NoBrush)] + \ [QBrush(palette[i].lighter(165)) for i in range(sels)] for y in range(self.size_y): for x in range(self.size_x - (y % 2) * self.hexagonal): cell = self.grid_cells[y, x] marked = marks is not None and marks[x, y] sel_group = self.selection is not None and self.selection[x, y] if marked: cell.setBrush(mark_brush) cell.setPen(mark_pen) else: cell.setBrush(brushes[sel_group]) cell.setPen(pens[bool(sel_group)]) cell.setZValue(marked or sel_group) def restart_som_pressed(self): if self._optimizer_thread is not None: self.stop_optimization = True self._optimizer.stop_optimization = True else: self.start_som() def start_som(self): self.read_controls() self.update_layout() self.clear_selection() if self.cont_x is not None: self.enable_controls(False) self._recompute_som() else: self.update_output() def read_controls(self): c = self.opt_controls self.hexagonal = c.shape.currentIndex() == 0 self.size_x = c.spin_x.value() self.size_y = c.spin_y.value() def enable_controls(self, enable): c = self.opt_controls c.shape.setEnabled(enable) c.auto_dim.setEnabled(enable) c.start.setText("Start" if enable else "Stop") def update_layout(self): self.set_legend_pos() if self.elements: # Prevent having redrawn grid but with old elements self.scene.removeItem(self.elements) self.elements = None self.redraw_grid() self.rescale() def _redraw(self): self.Warning.missing_colors.clear() if self.elements: self.scene.removeItem(self.elements) self.elements = None self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal) if self.cells is None: return sizes = self.cells[:, :, 1] - self.cells[:, :, 0] sizes = sizes.astype(float) if not self.size_by_instances: sizes[sizes != 0] = 0.8 else: sizes *= 0.8 / np.max(sizes) self.elements = QGraphicsItemGroup() self.scene.addItem(self.elements) if self.attr_color is None: self._draw_same_color(sizes) elif self.pie_charts: self._draw_pie_charts(sizes) else: self._draw_colored_circles(sizes) @property def _grid_factors(self): return (0.5, sqrt3_2) if self.hexagonal else (0, 1) def _draw_same_color(self, sizes): fx, fy = self._grid_factors color = QColor(64, 64, 64) for y in range(self.size_y): for x in range(self.size_x - self.hexagonal * (y % 2)): r = sizes[x, y] n = len(self.get_member_indices(x, y)) if not r: continue ellipse = ColoredCircle(r / 2, color, 0) ellipse.setPos(x + (y % 2) * fx, y * fy) ellipse.setToolTip(f"{n} instances") self.elements.addToGroup(ellipse) def _get_color_column(self): color_column = \ self.data.get_column_view(self.attr_color)[0].astype(float, copy=False) if self.attr_color.is_discrete: with np.errstate(invalid="ignore"): int_col = color_column.astype(int) int_col[np.isnan(color_column)] = len(self.colors) else: int_col = np.zeros(len(color_column), dtype=int) # The following line is unnecessary because rows with missing # numeric data are excluded. Uncomment it if you change SOM to # tolerate missing values. # int_col[np.isnan(color_column)] = len(self.colors) for i, thresh in enumerate(self.thresholds, start=1): int_col[color_column >= thresh] = i return int_col def _tooltip(self, colors, distribution): if self.attr_color.is_discrete: values = self.attr_color.values else: values = self._bin_names() tot = np.sum(distribution) nbhp = "\N{NON-BREAKING HYPHEN}" return '<table style="white-space: nowrap">' + "".join(f""" <tr> <td> <font color={color.name()}>■</font> <b>{escape(val).replace("-", nbhp)}</b>: </td> <td> {n} ({n / tot * 100:.1f} %) </td> </tr> """ for color, val, n in zip(colors, values, distribution) if n) \ + "</table>" def _draw_pie_charts(self, sizes): fx, fy = self._grid_factors color_column = self._get_color_column() colors = self.colors.qcolors_w_nan for y in range(self.size_y): for x in range(self.size_x - self.hexagonal * (y % 2)): r = sizes[x, y] if not r: self.grid_cells[y, x].setToolTip("") continue members = self.get_member_indices(x, y) color_dist = np.bincount(color_column[members], minlength=len(colors)) rel_color_dist = color_dist.astype(float) / len(members) pie = PieChart(rel_color_dist, r / 2, colors) pie.setToolTip(self._tooltip(colors, color_dist)) self.elements.addToGroup(pie) pie.setPos(x + (y % 2) * fx, y * fy) def _draw_colored_circles(self, sizes): fx, fy = self._grid_factors color_column = self._get_color_column() qcolors = self.colors.qcolors_w_nan for y in range(self.size_y): for x in range(self.size_x - self.hexagonal * (y % 2)): r = sizes[x, y] if not r: continue members = self.get_member_indices(x, y) color_dist = color_column[members] color_dist = color_dist[color_dist < len(self.colors)] if len(color_dist) != len(members): self.Warning.missing_colors(self.attr_color.name) bc = np.bincount(color_dist, minlength=len(self.colors)) color = qcolors[np.argmax(bc)] ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members)) ellipse.setPos(x + (y % 2) * fx, y * fy) ellipse.setToolTip(self._tooltip(qcolors, bc)) self.elements.addToGroup(ellipse) def redraw_grid(self): if self.grid is not None: self.scene.removeItem(self.grid) self.grid = QGraphicsItemGroup() self.grid.setZValue(-200) self.grid_cells = np.full((self.size_y, self.size_x), None) for y in range(self.size_y): for x in range(self.size_x - (y % 2) * self.hexagonal): if self.hexagonal: cell = QGraphicsPathItem(_hexagon_path) cell.setPos(x + (y % 2) / 2, y * sqrt3_2) else: cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1) self.grid_cells[y, x] = cell cell.setPen(self._grid_pen) self.grid.addToGroup(cell) self.scene.addItem(self.grid) def get_member_indices(self, x, y): i, j = self.cells[x, y] return self.member_data[i:j] def _recompute_som(self): if self.cont_x is None: return som = SOM(self.size_x, self.size_y, hexagonal=self.hexagonal, pca_init=self.initialization == 0, random_seed=0 if self.initialization == 2 else None) class Optimizer(QObject): update = Signal(float, np.ndarray, np.ndarray) done = Signal(SOM) stopped = Signal() stop_optimization = False def __init__(self, data, som): super().__init__() self.som = som self.data = data def callback(self, progress): self.update.emit(progress, self.som.weights.copy(), self.som.ssum_weights.copy()) return not self.stop_optimization def run(self): try: self.som.fit(self.data, N_ITERATIONS, callback=self.callback) # Report an exception, but still remove the thread finally: self.done.emit(self.som) self.stopped.emit() def thread_finished(): self._optimizer = None self._optimizer_thread = None self.progressBarInit() self._optimizer = Optimizer(self.cont_x, som) self._optimizer_thread = QThread() self._optimizer_thread.setStackSize(5 * 2**20) self._optimizer.update.connect(self.__update) self._optimizer.done.connect(self.__done) self._optimizer.stopped.connect(self._optimizer_thread.quit) self._optimizer.moveToThread(self._optimizer_thread) self._optimizer_thread.started.connect(self._optimizer.run) self._optimizer_thread.finished.connect(thread_finished) self.stop_optimization = False self._optimizer_thread.start() @Slot(float, object, object) def __update(self, _progress, weights, ssum_weights): self.progressBarSet(_progress) self._assign_instances(weights, ssum_weights) self._redraw() @Slot(object) def __done(self, som): self.enable_controls(True) self.progressBarFinished() self._assign_instances(som.weights, som.ssum_weights) self._redraw() # This is the first time we know what was selected (assuming that # initialization is not set to random) if self.__pending_selection is not None: self.on_selection_change(self.__pending_selection) self.__pending_selection = None self.update_output() def stop_optimization_and_wait(self): if self._optimizer_thread is not None: self.stop_optimization = True self._optimizer.stop_optimization = True self._optimizer_thread.quit() self._optimizer_thread.wait() self._optimizer_thread = None def onDeleteWidget(self): self.stop_optimization_and_wait() self.clear() super().onDeleteWidget() def _assign_instances(self, weights, ssum_weights): if self.cont_x is None: return # the widget is shutting down while signals still processed assignments = SOM.winner_from_weights(self.cont_x, weights, ssum_weights, self.hexagonal) members = defaultdict(list) for i, (x, y) in enumerate(assignments): members[(x, y)].append(i) members.pop(None, None) self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int) self.member_data = np.empty(self.cont_x.shape[0], dtype=int) index = 0 for x in range(self.size_x): for y in range(self.size_y): nmembers = len(members[(x, y)]) self.member_data[index:index + nmembers] = members[(x, y)] self.cells[x, y] = [index, index + nmembers] index += nmembers def resizeEvent(self, event): super().resizeEvent(event) self.create_legend() # re-wrap lines if necessary self.rescale() def rescale(self): if self.legend: leg_height = self.legend.boundingRect().height() leg_extra = 1.5 else: leg_height = 0 leg_extra = 1 vw, vh = self.view.width(), self.view.height() - leg_height scale = min(vw / (self.size_x + 1), vh / ((self.size_y + leg_extra) * self._grid_factors[1])) self.view.setTransform(QTransform.fromScale(scale, scale)) if self.hexagonal: self.view.setSceneRect(0, -1, self.size_x - 1, (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale) else: self.view.setSceneRect(-0.25, -0.25, self.size_x - 0.5, self.size_y - 0.5 + leg_height / scale) def update_output(self): if self.data is None: self.Outputs.selected_data.send(None) self.Outputs.annotated_data.send(None) self._set_output_summary(None) return indices = np.zeros(len(self.data), dtype=int) if self.selection is not None and np.any(self.selection): for y in range(self.size_y): for x in range(self.size_x): rows = self.get_member_indices(x, y) indices[rows] = self.selection[x, y] if np.any(indices): sel_data = create_groups_table(self.data, indices, False, "Group") self.Outputs.selected_data.send(sel_data) self._set_output_summary(sel_data) else: self.Outputs.selected_data.send(None) self._set_output_summary(None) if np.max(indices) > 1: annotated = create_groups_table(self.data, indices) else: annotated = create_annotated_table(self.data, np.flatnonzero(indices)) self.Outputs.annotated_data.send(annotated) def set_color_bins(self): if self.attr_color is None: self.thresholds = self.bin_labels = self.colors = None elif self.attr_color.is_discrete: self.thresholds = self.bin_labels = None self.colors = self.attr_color.palette else: col = self.data.get_column_view(self.attr_color)[0].astype(float) if self.attr_color.is_time: binning = time_binnings(col, min_bins=4)[-1] else: binning = decimal_binnings(col, min_bins=4)[-1] self.thresholds = binning.thresholds[1:-1] self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1]) palette = BinnedContinuousPalette.from_palette( self.attr_color.palette, binning.thresholds) self.colors = palette def create_legend(self): if self.legend is not None: self.scene.removeItem(self.legend) self.legend = None if self.attr_color is None: return if self.attr_color.is_discrete: names = self.attr_color.values else: names = self._bin_names() items = [] size = 8 for name, color in zip(names, self.colors.qcolors): item = QGraphicsItemGroup() item.addToGroup( CanvasRectangle(None, -size / 2, -size / 2, size, size, Qt.gray, color)) item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter)) items.append(item) self.legend = wrap_legend_items(items, hspacing=20, vspacing=16 + size, max_width=self.view.width() - 25) self.legend.setFlags(self.legend.ItemIgnoresTransformations) self.legend.setTransform( QTransform.fromTranslate(-self.legend.boundingRect().width() / 2, 0)) self.scene.addItem(self.legend) self.set_legend_pos() def _bin_names(self): labels, short_labels = self.bin_labels return \ [f"< {labels[0]}"] \ + [f"{x} - {y}" for x, y in zip(labels, short_labels[1:])] \ + [f"≥ {labels[-1]}"] def set_legend_pos(self): if self.legend is None: return self.legend.setPos(self.size_x / 2, (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1]) def send_report(self): self.report_plot() if self.attr_color: self.report_caption( f"Self-organizing map colored by '{self.attr_color.name}'")
class OWNxExplorer(OWDataProjectionWidget): name = "Network Explorer" description = "Visually explore the network and its properties." icon = "icons/NetworkExplorer.svg" priority = 6420 class Inputs: node_data = Input("Node Data", Table) node_subset = Input("Node Subset", Table) network = Input("Network", Network, default=True) node_distances = Input("Node Distances", Orange.misc.DistMatrix) class Outputs(OWDataProjectionWidget.Outputs): subgraph = Output("Selected sub-network", Network) unselected_subgraph = Output("Remaining sub-network", Network) distances = Output("Distance matrix", Orange.misc.DistMatrix) UserAdviceMessages = [ widget.Message("Double clicks select connected components", widget.Message.Information), ] GRAPH_CLASS = GraphView graph = SettingProvider(GraphView) layout_density = Setting(10) observe_weights = Setting(True) mark_hops = Setting(1) mark_min_conn = Setting(5) mark_max_conn = Setting(5) mark_most_conn = Setting(1) alpha_value = 255 # Override the setting from parent class Warning(OWDataProjectionWidget.Warning): distance_matrix_mismatch = widget.Msg( "Distance matrix size doesn't match the number of network nodes " "and will be ignored.") no_graph_found = widget.Msg("Node data is given, graph data is missing.") class Error(OWDataProjectionWidget.Error): data_size_mismatch = widget.Msg( "Length of the data does not match the number of nodes.") network_too_large = widget.Msg("Network is too large to visualize.") single_node_graph = widget.Msg("I don't do single-node graphs today.") def __init__(self): # These are already needed in super().__init__() self.number_of_nodes = 0 self.number_of_edges = 0 self.nHighlighted = 0 self.nSelected = 0 self.nodes_per_edge = 0 self.edges_per_node = 0 self.mark_mode = 0 self.mark_text = "" super().__init__() self.network = None self.node_data = None self.distance_matrix = None self.edges = None self.positions = None self._optimizer = None self._animation_thread = None self._stop_optimization = False self.marked_nodes = None self.searchStringTimer = QTimer(self) self.searchStringTimer.timeout.connect(self.update_marks) self.set_mark_mode() self.setMinimumWidth(600) def sizeHint(self): return QSize(800, 600) def _add_controls(self): self.gui = OWPlotGUI(self) self._add_info_box() self.gui.point_properties_box(self.controlArea) self._add_effects_box() self.gui.plot_properties_box(self.controlArea) self._add_mark_box() self.controls.attr_label.activated.connect(self.on_change_label_attr) def _add_info_box(self): info = gui.vBox(self.controlArea, box="Layout") gui.label( info, self, "Nodes: %(number_of_nodes)i (%(nodes_per_edge).2f per edge); " "%(nSelected)i selected") gui.label( info, self, "Edges: %(number_of_edges)i (%(edges_per_node).2f per node)") lbox = gui.hBox(info) self.relayout_button = gui.button( lbox, self, 'Improve', callback=self.improve, autoDefault=False, tooltip="Optimize the current layout, with a small initial jerk") self.stop_button = gui.button( lbox, self, 'Stop', callback=self.stop_relayout, autoDefault=False, hidden=True) self.randomize_button = gui.button( lbox, self, 'Re-layout', callback=self.restart, autoDefault=False, tooltip="Restart laying out from random positions") gui.hSlider(info, self, "layout_density", minValue=1, maxValue=50, label="Gravity", orientation=Qt.Horizontal, callback_finished=self.improve, tooltip="Lower values improve optimization,\n" "higher work better for graph with many small " "components") gui.checkBox(info, self, "observe_weights", label="Make edges with large weights shorter", callback=self.improve) def _add_effects_box(self): gbox = self.gui.create_gridbox(self.controlArea, box="Widths and Sizes") self.gui.add_widget(self.gui.PointSize, gbox) gbox.layout().itemAtPosition(1, 0).widget().setText("Node Size:") self.gui.add_control( gbox, gui.hSlider, "Edge width:", master=self, value='graph.edge_width', minValue=1, maxValue=10, step=1, callback=self.graph.update_edges) box = gui.vBox(None) gbox.layout().addWidget(box, 3, 0, 1, 2) gui.separator(box) self.checkbox_relative_edges = gui.checkBox( box, self, 'graph.relative_edge_widths', 'Scale edge widths to weights', callback=self.graph.update_edges) self.checkbox_show_weights = gui.checkBox( box, self, 'graph.show_edge_weights', 'Show edge weights', callback=self.graph.update_edge_labels) self.checkbox_show_weights = gui.checkBox( box, self, 'graph.label_selected_edges', 'Label only edges of selected nodes', callback=self.graph.update_edge_labels) # This is ugly: create a slider that controls alpha_value so that # parent can enable and disable it - although it's never added to any # layout and visible to the user gui.hSlider(None, self, "graph.alpha_value") def _add_mark_box(self): hbox = gui.hBox(None, box=True) self.mainArea.layout().addWidget(hbox) vbox = gui.hBox(hbox) def spin(value, label, minv, maxv): return gui.spin( vbox, self, value, label=label, minv=minv, maxv=maxv, step=1, alignment=Qt.AlignRight, callback=self.update_marks).box def text_line(): def set_search_string_timer(): self.searchStringTimer.stop() self.searchStringTimer.start(300) return gui.lineEdit( gui.hBox(vbox), self, "mark_text", label="Text: ", orientation=Qt.Horizontal, minimumWidth=50, callback=set_search_string_timer, callbackOnType=True).box def _mark_by_labels(marker): txt = self.mark_text.lower() if not txt: return None labels = self.get_label_data() if labels is None: return None return marker(np.char.array(labels), txt) def mark_label_starts(): return _mark_by_labels( lambda labels, txt: np.flatnonzero(labels.lower().startswith(txt))) def mark_label_contains(): return _mark_by_labels( lambda labels, txt: np.flatnonzero(labels.lower().find(txt) != -1)) def mark_text(): txt = self.mark_text.lower() if not txt or self.data is None: return None return np.array( [i for i, inst in enumerate(self.data) if txt in "\x00".join(map(str, inst.list)).lower()]) def mark_reachable(): selected = self.graph.get_selection() if selected is None: return None return self.get_reachable(selected) def mark_close(): selected = self.graph.get_selection() if selected is None: return None neighbours = set(selected) last_round = list(neighbours) for _ in range(self.mark_hops): next_round = set() for neigh in last_round: next_round |= set(self.network.neighbours(neigh)) neighbours |= next_round last_round = next_round neighbours -= set(selected) return np.array(list(neighbours)) def mark_from_input(): if self.subset_data is None or self.data is None: return None ids = set(self.subset_data.ids) return np.array( [i for i, ex in enumerate(self.data) if ex.id in ids]) def mark_most_connections(): n = self.mark_most_conn if n >= self.number_of_nodes: return np.arange(self.number_of_nodes) degrees = self.network.degrees() # pylint: disable=invalid-unary-operand-type min_degree = np.partition(degrees, -n)[-n] return np.flatnonzero(degrees >= min_degree) def mark_more_than_any_neighbour(): degrees = self.network.degrees() return np.array( [node for node, degree in enumerate(degrees) if degree > np.max(degrees[self.network.neighbours(node)], initial=0)]) def mark_more_than_average_neighbour(): degrees = self.network.degrees() return np.array( [node for node, degree, neighbours in ( (node, degree, self.network.neighbours(node)) for node, degree in enumerate(degrees)) if degree > (np.mean(degrees[neighbours]) if neighbours.size else 0) ] ) self.mark_criteria = [ ("(Select criteria for marking)", None, lambda: np.zeros((0,))), ("Mark nodes whose label starts with", text_line(), mark_label_starts), ("Mark nodes whose label contains", text_line(), mark_label_contains), ("Mark nodes whose data that contains", text_line(), mark_text), ("Mark nodes reachable from selected", None, mark_reachable), ("Mark nodes in vicinity of selection", spin("mark_hops", "Number of hops:", 1, 20), mark_close), ("Mark nodes from subset signal", None, mark_from_input), ("Mark nodes with few connections", spin("mark_max_conn", "Max. connections:", 0, 1000), lambda: np.flatnonzero(self.network.degrees() <= self.mark_max_conn)), ("Mark nodes with many connections", spin("mark_min_conn", "Min. connections:", 1, 1000), lambda: np.flatnonzero(self.network.degrees() >= self.mark_min_conn)), ("Mark nodes with most connections", spin("mark_most_conn", "Number of marked:", 1, 1000), mark_most_connections), ("Mark nodes with more connections than any neighbour", None, mark_more_than_any_neighbour), ("Mark nodes with more connections than average neighbour", None, mark_more_than_average_neighbour) ] cb = gui.comboBox( hbox, self, "mark_mode", items=[item for item, *_ in self.mark_criteria], maximumContentsLength=-1, callback=self.set_mark_mode) hbox.layout().insertWidget(0, cb) gui.rubber(hbox) self.btselect = gui.button( hbox, self, "Select", callback=self.select_marked) self.btadd = gui.button( hbox, self, "Add to Selection", callback=self.select_add_marked) self.btgroup = gui.button( hbox, self, "Add New Group", callback=self.select_as_group) def set_mark_mode(self, mode=None): if mode is not None: self.mark_mode = mode for i, (_, widget, _) in enumerate(self.mark_criteria): if widget: if i == self.mark_mode: widget.show() else: widget.hide() self.searchStringTimer.stop() self.update_marks() def update_marks(self): if self.network is None: return self.marked_nodes = self.mark_criteria[self.mark_mode][2]() if self.marked_nodes is not None and not self.marked_nodes.size: self.marked_nodes = None self.graph.update_marks() if self.graph.label_only_selected: self.graph.update_labels() self.update_selection_buttons() def update_selection_buttons(self): if self.marked_nodes is None: self.btselect.hide() self.btadd.hide() self.btgroup.hide() return else: self.btselect.show() selection = self.graph.get_selection() if not len(selection) or np.max(selection) == 0: self.btadd.hide() self.btgroup.hide() elif np.max(selection) == 1: self.btadd.setText("Add to Selection") self.btadd.show() self.btgroup.hide() else: self.btadd.setText("Add to Group") self.btadd.show() self.btgroup.show() def selection_changed(self): super().selection_changed() self.nSelected = 0 if self.selection is None else len(self.selection) self.update_selection_buttons() self.update_marks() def select_marked(self): self.graph.selection_select(self.marked_nodes) def select_add_marked(self): self.graph.selection_append(self.marked_nodes) def select_as_group(self): self.graph.selection_new_group(self.marked_nodes) def on_change_label_attr(self): if self.mark_mode in (1, 2): self.update_marks() @Inputs.node_data def set_node_data(self, data): self.node_data = data @Inputs.node_subset def set_node_subset(self, data): # It would be better to call super, but this fails because super # is decorated to set the partial summary for signal "Subset Data", # which does not exist for this widget (OWNxExplorer.Inputs is not # derived from OWDataProjectionWidget.Inputs in order to rename the # signal) self.subset_data = data @Inputs.node_distances def set_items_distance_matrix(self, matrix): self.distance_matrix = matrix self.positions = None @Inputs.network def set_graph(self, graph): def set_graph_none(error=None): if error is not None: error() self.network = None self.number_of_nodes = self.edges_per_node = 0 self.number_of_edges = self.nodes_per_edge = 0 def compute_stats(): self.number_of_nodes = graph.number_of_nodes() self.number_of_edges = graph.number_of_edges() self.edges_per_node = self.number_of_edges / self.number_of_nodes self.nodes_per_edge = \ self.number_of_nodes / max(1, self.number_of_edges) self.mark_text = "" self.set_mark_mode(0) self.positions = None if not graph or graph.number_of_nodes() == 0: set_graph_none() return if graph.number_of_nodes() + graph.number_of_edges() > 100000: set_graph_none(self.Error.network_too_large) return self.Error.clear() self.network = graph compute_stats() def handleNewSignals(self): network = self.network def set_actual_data(): self.closeContext() self.Error.data_size_mismatch.clear() self.Warning.no_graph_found.clear() self._invalid_data = False if network is None: if self.node_data is not None: self.Warning.no_graph_found() return n_nodes = len(self.network.nodes) if self.node_data is not None: if len(self.node_data) != n_nodes: self.Error.data_size_mismatch() self._invalid_data = True self.data = None else: self.data = self.node_data if self.node_data is None: if isinstance(network.nodes, Table): self.data = network.nodes elif isinstance(network.nodes, np.ndarray) \ and (len(network.nodes.shape) == 1 or network.nodes.shape[1] == 1): self.data = Table.from_numpy( Domain([], None, [StringVariable("label")]), np.zeros((len(network.nodes),0)), None, metas=network.nodes.reshape((n_nodes, 1)) ) else: self.data = None if self.data is not None: # Replicate the necessary parts of set_data self.valid_data = np.full(len(self.data), True, dtype=bool) self.init_attr_values() self.openContext(self.data) self.cb_class_density.setEnabled(self.can_draw_density()) def set_actual_edges(): def set_checkboxes(value): self.checkbox_show_weights.setEnabled(value) self.checkbox_relative_edges.setEnabled(value) self.Warning.distance_matrix_mismatch.clear() if self.network is None: self.edges = None set_checkboxes(False) return set_checkboxes(True) if network.number_of_edges(0): self.edges = network.edges[0].edges.tocoo() else: self.edges = sp.coo_matrix((0, 3)) if self.distance_matrix is not None: if len(self.distance_matrix) != self.number_of_nodes: self.Warning.distance_matrix_mismatch() else: self.edges.data = np.fromiter( (self.distance_matrix[u, v] for u, v in zip(self.edges.row, self.edges.col)), dtype=np.int32, count=len(self.edges.row) ) if np.allclose(self.edges.data, 0): self.edges.data[:] = 1 set_checkboxes(False) elif len(set(self.edges.data)) == 1: set_checkboxes(False) self.stop_optimization_and_wait() set_actual_data() super()._handle_subset_data() if self.positions is None: set_actual_edges() self.set_random_positions() self.graph.reset_graph() self.relayout(True) else: self.graph.update_point_props() self.update_marks() self.update_selection_buttons() def init_attr_values(self): super().init_attr_values() if self.node_data is None \ and self.data is not None \ and isinstance(self.network.nodes, np.ndarray): assert len(self.data.domain.metas) == 1 self.attr_label = self.data.domain.metas[0] def randomize(self): self.set_random_positions() self.graph.update_coordinates() def set_random_positions(self): if self.network is None: self.position = None else: self.positions = np.random.uniform(size=(self.number_of_nodes, 2)) def get_reachable(self, initial): to_check = list(initial) reachable = set(to_check) for node in to_check: new_checks = set(self.network.neighbours(node)) - reachable to_check += new_checks reachable |= new_checks return np.array(to_check) def send_data(self): super().send_data() Outputs = self.Outputs selected_indices = self.graph.get_selection() if selected_indices is None or len(selected_indices) == 0: Outputs.subgraph.send(None) Outputs.unselected_subgraph.send(self.network) Outputs.distances.send(None) return selection = self.graph.selection subgraph = self.network.subgraph(selected_indices) subgraph.nodes = \ self._get_selected_data(self.data, selected_indices, selection) Outputs.subgraph.send(subgraph) Outputs.unselected_subgraph.send( self.network.subgraph(np.flatnonzero(selection == 0))) distances = self.distance_matrix if distances is None: Outputs.distances.send(None) else: Outputs.distances.send(distances.submatrix(sorted(selected_indices))) def get_coordinates_data(self): if self.positions is not None: return self.positions.T else: return None, None def get_embedding(self): return self.positions def get_subset_mask(self): if self.data is None: return None return super().get_subset_mask() def get_edges(self): return self.edges def is_directed(self): return self.network is not None and self.network.edges[0].directed def get_marked_nodes(self): return self.marked_nodes def set_buttons(self, running): self.stop_button.setHidden(not running) self.relayout_button.setHidden(running) self.randomize_button.setHidden(running) def stop_relayout(self): self._stop_optimization = True self.set_buttons(running=False) def restart(self): self.relayout(restart=True) def improve(self): self.relayout(restart=False) # TODO: Stop relayout if new data is received def relayout(self, restart): if self.edges is None: return if restart or self.positions is None: self.set_random_positions() self.progressbar = gui.ProgressBar(self, 100) self.set_buttons(running=True) self._stop_optimization = False Simplifications = self.graph.Simplifications self.graph.set_simplifications( Simplifications.NoDensity + Simplifications.NoLabels * (len(self.graph.labels) > 20) + Simplifications.NoEdgeLabels * (len(self.graph.edge_labels) > 20) + Simplifications.NoEdges * (self.number_of_edges > 30000)) large_graph = self.number_of_nodes + self.number_of_edges > 30000 class LayoutOptimizer(QObject): update = Signal(np.ndarray, float) done = Signal(np.ndarray) stopped = Signal() def __init__(self, widget): super().__init__() self.widget = widget def send_update(self, positions, progress): if not large_graph: self.update.emit(np.array(positions), progress) return not self.widget._stop_optimization def run(self): widget = self.widget edges = widget.edges nnodes = widget.number_of_nodes init_temp = 0.05 if restart else 0.2 k = widget.layout_density / 10 / np.sqrt(nnodes) sample_ratio = None if nnodes < 1000 else 1000 / nnodes fruchterman_reingold( widget.positions, edges, widget.observe_weights, FR_ALLOWED_TIME, k, init_temp, sample_ratio, callback_step=4, callback=self.send_update) self.done.emit(widget.positions) self.stopped.emit() def update(positions, progress): self.progressbar.advance(progress) self.positions = positions self.graph.update_coordinates() def done(positions): self.positions = positions self.set_buttons(running=False) self.graph.set_simplifications( self.graph.Simplifications.NoSimplifications) self.graph.update_coordinates() self.progressbar.finish() def thread_finished(): self._optimizer = None self._animation_thread = None self._optimizer = LayoutOptimizer(self) self._animation_thread = QThread() self._optimizer.update.connect(update) self._optimizer.done.connect(done) self._optimizer.stopped.connect(self._animation_thread.quit) self._optimizer.moveToThread(self._animation_thread) self._animation_thread.started.connect(self._optimizer.run) self._animation_thread.finished.connect(thread_finished) self._animation_thread.start() def stop_optimization_and_wait(self): if self._animation_thread is not None: self._stop_optimization = True self._animation_thread.quit() self._animation_thread.wait() self._animation_thread = None def onDeleteWidget(self): self.stop_optimization_and_wait() super().onDeleteWidget() def send_report(self): if self.network is None: return self.report_items('Graph info', [ ("Number of vertices", self.network.number_of_nodes()), ("Number of edges", self.network.number_of_edges()), ("Vertices per edge", round(self.nodes_per_edge, 3)), ("Edges per vertex", round(self.edges_per_node, 3)), ]) if self.data is not None: self.report_data("Data", self.data) if any((self.attr_color, self.attr_shape, self.attr_size, self.attr_label)): self.report_items( "Visual settings", [("Color", self._get_caption_var_name(self.attr_color)), ("Label", self._get_caption_var_name(self.attr_label)), ("Shape", self._get_caption_var_name(self.attr_shape)), ("Size", self._get_caption_var_name(self.attr_size))]) self.report_plot()
class VizRankDialog(QDialog, ProgressBarMixin, WidgetMessagesMixin): """ Base class for VizRank dialogs, providing a GUI with a table and a button, and the skeleton for managing the evaluation of visualizations. Derived classes must provide methods - `iterate_states` for generating combinations (e.g. pairs of attritutes), - `compute_score(state)` for computing the score of a combination, - `row_for_state(state)` that returns a list of items inserted into the table for the given state. and, optionally, - `state_count` that returns the number of combinations (used for progress bar) - `on_selection_changed` that handles event triggered when the user selects a table row. The method should emit signal `VizRankDialog.selectionChanged(object)`. - `bar_length` returns the length of the bar corresponding to the score. The class provides a table and a button. A widget constructs a single instance of this dialog in its `__init__`, like (in Sieve) by using a convenience method :obj:`add_vizrank`:: self.vizrank, self.vizrank_button = SieveRank.add_vizrank( box, self, "Score Combinations", self.set_attr) When the widget receives new data, it must call the VizRankDialog's method :obj:`VizRankDialog.initialize()` to clear the GUI and reset the state. Clicking the Start button calls method `run` (and renames the button to Pause). Run sets up a progress bar by getting the number of combinations from :obj:`VizRankDialog.state_count()`. It restores the paused state (if any) and calls generator :obj:`VizRankDialog.iterate_states()`. For each generated state, it calls :obj:`VizRankDialog.score(state)`, which must return the score (lower is better) for this state. If the returned state is not `None`, the data returned by `row_for_state` is inserted at the appropriate place in the table. Args: master (Orange.widget.OWWidget): widget to which the dialog belongs Attributes: master (Orange.widget.OWWidget): widget to which the dialog belongs captionTitle (str): the caption for the dialog. This can be a class attribute. `captionTitle` is used by the `ProgressBarMixin`. """ captionTitle = "" NEGATIVE_COLOR = QColor(70, 190, 250) POSITIVE_COLOR = QColor(170, 242, 43) processingStateChanged = Signal(int) progressBarValueChanged = Signal(float) messageActivated = Signal(Msg) messageDeactivated = Signal(Msg) selectionChanged = Signal(object) class Information(WidgetMessagesMixin.Information): nothing_to_rank = Msg("There is nothing to rank.") def __init__(self, master): """Initialize the attributes and set up the interface""" QDialog.__init__(self, master, windowTitle=self.captionTitle) WidgetMessagesMixin.__init__(self) self.setLayout(QVBoxLayout()) self.insert_message_bar() self.layout().insertWidget(0, self.message_bar) self.master = master self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.scores = [] self.add_to_model = queue.Queue() self.update_timer = QTimer(self) self.update_timer.timeout.connect(self._update) self.update_timer.setInterval(200) self._thread = None self._worker = None self.filter = QLineEdit() self.filter.setPlaceholderText("Filter ...") self.filter.textChanged.connect(self.filter_changed) self.layout().addWidget(self.filter) # Remove focus from line edit self.setFocus(Qt.ActiveWindowFocusReason) self.rank_model = QStandardItemModel(self) self.model_proxy = QSortFilterProxyModel(self, filterCaseSensitivity=False) self.model_proxy.setSourceModel(self.rank_model) self.rank_table = view = QTableView( selectionBehavior=QTableView.SelectRows, selectionMode=QTableView.SingleSelection, showGrid=False, editTriggers=gui.TableView.NoEditTriggers) if self._has_bars: view.setItemDelegate(TableBarItem()) else: view.setItemDelegate(HorizontalGridDelegate()) view.setModel(self.model_proxy) view.selectionModel().selectionChanged.connect( self.on_selection_changed) view.horizontalHeader().setStretchLastSection(True) view.horizontalHeader().hide() self.layout().addWidget(view) self.button = gui.button(self, self, "Start", callback=self.toggle, default=True) @property def _has_bars(self): return type(self).bar_length is not VizRankDialog.bar_length @classmethod def add_vizrank(cls, widget, master, button_label, set_attr_callback): """ Equip the widget with VizRank button and dialog, and monkey patch the widget's `closeEvent` and `hideEvent` to close/hide the vizrank, too. Args: widget (QWidget): the widget into whose layout to insert the button master (Orange.widgets.widget.OWWidget): the master widget button_label: the label for the button set_attr_callback: the callback for setting the projection chosen in the vizrank Returns: tuple with Vizrank dialog instance and push button """ # Monkey patching could be avoided by mixing-in the class (not # necessarily a good idea since we can make a mess of multiple # defined/derived closeEvent and hideEvent methods). Furthermore, # per-class patching would be better than per-instance, but we don't # want to mess with meta-classes either. vizrank = cls(master) button = gui.button(widget, master, button_label, callback=vizrank.reshow, enabled=False) vizrank.selectionChanged.connect(lambda args: set_attr_callback(*args)) master_close_event = master.closeEvent master_hide_event = master.hideEvent master_delete_event = master.onDeleteWidget def closeEvent(event): vizrank.close() master_close_event(event) def hideEvent(event): vizrank.hide() master_hide_event(event) def deleteEvent(): vizrank.keep_running = False if vizrank._thread is not None and vizrank._thread.isRunning(): vizrank._thread.quit() vizrank._thread.wait() master_delete_event() master.closeEvent = closeEvent master.hideEvent = hideEvent master.onDeleteWidget = deleteEvent return vizrank, button def reshow(self): """Put the widget on top of all windows """ self.show() self.raise_() self.activateWindow() def initialize(self): """ Clear and initialize the dialog. This method must be called by the widget when the data is reset, e.g. from `set_data` handler. """ if self._thread is not None and self._thread.isRunning(): self.keep_running = False self._thread.quit() self._thread.wait() self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.update_timer.stop() self.progressBarFinished() self.scores = [] self._update_model() # empty queue self.rank_model.clear() self.button.setText("Start") self.button.setEnabled(self.check_preconditions()) self._thread = QThread(self) self._worker = Worker(self) self._worker.moveToThread(self._thread) self._worker.stopped.connect(self._thread.quit) self._worker.stopped.connect(self._select_first_if_none) self._worker.stopped.connect(self._stopped) self._worker.done.connect(self._done) self._thread.started.connect(self._worker.do_work) def filter_changed(self, text): self.model_proxy.setFilterFixedString(text) def stop_and_reset(self, reset_method=None): if self.keep_running: self.scheduled_call = reset_method or self.initialize self.keep_running = False else: self.initialize() def check_preconditions(self): """Check whether there is sufficient data for ranking.""" return True def on_selection_changed(self, selected, deselected): """ Set the new visualization in the widget when the user select a row in the table. If derived class does not reimplement this, the table gives the information but the user can't click it to select the visualization. Args: selected: the index of the selected item deselected: the index of the previously selected item """ pass def iterate_states(self, initial_state): """ Generate all possible states (e.g. attribute combinations) for the given data. The content of the generated states is specific to the visualization. This method must be defined in the derived classes. Args: initial_state: initial state; None if this is the first call """ raise NotImplementedError def state_count(self): """ Return the number of states for the progress bar. Derived classes should implement this to ensure the proper behaviour of the progress bar""" return 0 def compute_score(self, state): """ Abstract method for computing the score for the given state. Smaller scores are better. Args: state: the state, e.g. the combination of attributes as generated by :obj:`state_count`. """ raise NotImplementedError def bar_length(self, score): """Compute the bar length (between 0 and 1) corresponding to the score. Return `None` if the score cannot be normalized. """ return None def row_for_state(self, score, state): """ Abstract method that return the items that are inserted into the table. Args: score: score, computed by :obj:`compute_score` state: the state, e.g. combination of attributes """ raise NotImplementedError def _select_first_if_none(self): if not self.rank_table.selectedIndexes(): self.rank_table.selectRow(0) def _done(self): self.button.setText("Finished") self.button.setEnabled(False) self.keep_running = False self.saved_state = None def _stopped(self): self.update_timer.stop() self.progressBarFinished() self._update_model() self.stopped() if self.scheduled_call: self.scheduled_call() def _update(self): self._update_model() self._update_progress() def _update_progress(self): self.progressBarSet( int(self.saved_progress * 100 / max(1, self.state_count()))) def _update_model(self): try: while True: pos, row_items = self.add_to_model.get_nowait() self.rank_model.insertRow(pos, row_items) except queue.Empty: pass def toggle(self): """Start or pause the computation.""" self.keep_running = not self.keep_running if self.keep_running: self.button.setText("Pause") self.progressBarInit() self.update_timer.start() self.before_running() self._thread.start() else: self.button.setText("Continue") self._thread.quit() # Need to sync state (the worker must read the keep_running # state and stop) for reliable restart. self._thread.wait() def before_running(self): """Code that is run before running vizrank in its own thread""" pass def stopped(self): """Code that is run after stopping the vizrank thread""" pass
class VizRankDialog(QDialog, ProgressBarMixin, WidgetMessagesMixin): """ Base class for VizRank dialogs, providing a GUI with a table and a button, and the skeleton for managing the evaluation of visualizations. Derived classes must provide methods - `iterate_states` for generating combinations (e.g. pairs of attritutes), - `compute_score(state)` for computing the score of a combination, - `row_for_state(state)` that returns a list of items inserted into the table for the given state. and, optionally, - `state_count` that returns the number of combinations (used for progress bar) - `on_selection_changed` that handles event triggered when the user selects a table row. The method should emit signal `VizRankDialog.selectionChanged(object)`. - `bar_length` returns the length of the bar corresponding to the score. The class provides a table and a button. A widget constructs a single instance of this dialog in its `__init__`, like (in Sieve) by using a convenience method :obj:`add_vizrank`:: self.vizrank, self.vizrank_button = SieveRank.add_vizrank( box, self, "Score Combinations", self.set_attr) When the widget receives new data, it must call the VizRankDialog's method :obj:`VizRankDialog.initialize()` to clear the GUI and reset the state. Clicking the Start button calls method `run` (and renames the button to Pause). Run sets up a progress bar by getting the number of combinations from :obj:`VizRankDialog.state_count()`. It restores the paused state (if any) and calls generator :obj:`VizRankDialog.iterate_states()`. For each generated state, it calls :obj:`VizRankDialog.score(state)`, which must return the score (lower is better) for this state. If the returned state is not `None`, the data returned by `row_for_state` is inserted at the appropriate place in the table. Args: master (Orange.widget.OWWidget): widget to which the dialog belongs Attributes: master (Orange.widget.OWWidget): widget to which the dialog belongs captionTitle (str): the caption for the dialog. This can be a class attribute. `captionTitle` is used by the `ProgressBarMixin`. """ captionTitle = "" processingStateChanged = Signal(int) progressBarValueChanged = Signal(float) messageActivated = Signal(Msg) messageDeactivated = Signal(Msg) selectionChanged = Signal(object) class Information(WidgetMessagesMixin.Information): nothing_to_rank = Msg("There is nothing to rank.") def __init__(self, master): """Initialize the attributes and set up the interface""" QDialog.__init__(self, master, windowTitle=self.captionTitle) WidgetMessagesMixin.__init__(self) self.setLayout(QVBoxLayout()) self.insert_message_bar() self.layout().insertWidget(0, self.message_bar) self.master = master self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.scores = [] self.add_to_model = queue.Queue() self.update_timer = QTimer(self) self.update_timer.timeout.connect(self._update) self.update_timer.setInterval(200) self._thread = None self._worker = None self.filter = QLineEdit() self.filter.setPlaceholderText("Filter ...") self.filter.textChanged.connect(self.filter_changed) self.layout().addWidget(self.filter) # Remove focus from line edit self.setFocus(Qt.ActiveWindowFocusReason) self.rank_model = QStandardItemModel(self) self.model_proxy = QSortFilterProxyModel( self, filterCaseSensitivity=False) self.model_proxy.setSourceModel(self.rank_model) self.rank_table = view = QTableView( selectionBehavior=QTableView.SelectRows, selectionMode=QTableView.SingleSelection, showGrid=False, editTriggers=gui.TableView.NoEditTriggers) if self._has_bars: view.setItemDelegate(TableBarItem()) else: view.setItemDelegate(HorizontalGridDelegate()) view.setModel(self.model_proxy) view.selectionModel().selectionChanged.connect( self.on_selection_changed) view.horizontalHeader().setStretchLastSection(True) view.horizontalHeader().hide() self.layout().addWidget(view) self.button = gui.button( self, self, "Start", callback=self.toggle, default=True) @property def _has_bars(self): return type(self).bar_length is not VizRankDialog.bar_length @classmethod def add_vizrank(cls, widget, master, button_label, set_attr_callback): """ Equip the widget with VizRank button and dialog, and monkey patch the widget's `closeEvent` and `hideEvent` to close/hide the vizrank, too. Args: widget (QWidget): the widget into whose layout to insert the button master (Orange.widgets.widget.OWWidget): the master widget button_label: the label for the button set_attr_callback: the callback for setting the projection chosen in the vizrank Returns: tuple with Vizrank dialog instance and push button """ # Monkey patching could be avoided by mixing-in the class (not # necessarily a good idea since we can make a mess of multiple # defined/derived closeEvent and hideEvent methods). Furthermore, # per-class patching would be better than per-instance, but we don't # want to mess with meta-classes either. vizrank = cls(master) button = gui.button( widget, master, button_label, callback=vizrank.reshow, enabled=False) vizrank.selectionChanged.connect(lambda args: set_attr_callback(*args)) master_close_event = master.closeEvent master_hide_event = master.hideEvent master_delete_event = master.onDeleteWidget def closeEvent(event): vizrank.close() master_close_event(event) def hideEvent(event): vizrank.hide() master_hide_event(event) def deleteEvent(): vizrank.keep_running = False if vizrank._thread is not None and vizrank._thread.isRunning(): vizrank._thread.quit() vizrank._thread.wait() master_delete_event() master.closeEvent = closeEvent master.hideEvent = hideEvent master.onDeleteWidget = deleteEvent return vizrank, button def reshow(self): """Put the widget on top of all windows """ self.show() self.raise_() self.activateWindow() def initialize(self): """ Clear and initialize the dialog. This method must be called by the widget when the data is reset, e.g. from `set_data` handler. """ if self._thread is not None and self._thread.isRunning(): self.keep_running = False self._thread.quit() self._thread.wait() self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.update_timer.stop() self.progressBarFinished() self.scores = [] self._update_model() # empty queue self.rank_model.clear() self.button.setText("Start") self.button.setEnabled(self.check_preconditions()) self._thread = QThread(self) self._worker = Worker(self) self._worker.moveToThread(self._thread) self._worker.stopped.connect(self._thread.quit) self._worker.stopped.connect(self._select_first_if_none) self._worker.stopped.connect(self._stopped) self._worker.done.connect(self._done) self._thread.started.connect(self._worker.do_work) def filter_changed(self, text): self.model_proxy.setFilterFixedString(text) def stop_and_reset(self, reset_method=None): if self.keep_running: self.scheduled_call = reset_method or self.initialize self.keep_running = False else: self.initialize() def check_preconditions(self): """Check whether there is sufficient data for ranking.""" return True def on_selection_changed(self, selected, deselected): """ Set the new visualization in the widget when the user select a row in the table. If derived class does not reimplement this, the table gives the information but the user can't click it to select the visualization. Args: selected: the index of the selected item deselected: the index of the previously selected item """ pass def iterate_states(self, initial_state): """ Generate all possible states (e.g. attribute combinations) for the given data. The content of the generated states is specific to the visualization. This method must be defined in the derived classes. Args: initial_state: initial state; None if this is the first call """ raise NotImplementedError def state_count(self): """ Return the number of states for the progress bar. Derived classes should implement this to ensure the proper behaviour of the progress bar""" return 0 def compute_score(self, state): """ Abstract method for computing the score for the given state. Smaller scores are better. Args: state: the state, e.g. the combination of attributes as generated by :obj:`state_count`. """ raise NotImplementedError def bar_length(self, score): """Compute the bar length (between 0 and 1) corresponding to the score. Return `None` if the score cannot be normalized. """ return None def row_for_state(self, score, state): """ Abstract method that return the items that are inserted into the table. Args: score: score, computed by :obj:`compute_score` state: the state, e.g. combination of attributes """ raise NotImplementedError def _select_first_if_none(self): if not self.rank_table.selectedIndexes(): self.rank_table.selectRow(0) def _done(self): self.button.setText("Finished") self.button.setEnabled(False) self.keep_running = False self.saved_state = None def _stopped(self): self.update_timer.stop() self.progressBarFinished() self._update_model() self.stopped() if self.scheduled_call: self.scheduled_call() def _update(self): self._update_model() self._update_progress() def _update_progress(self): self.progressBarSet(int(self.saved_progress * 100 / max(1, self.state_count()))) def _update_model(self): try: while True: pos, row_items = self.add_to_model.get_nowait() self.rank_model.insertRow(pos, row_items) except queue.Empty: pass def toggle(self): """Start or pause the computation.""" self.keep_running = not self.keep_running if self.keep_running: self.button.setText("Pause") self.progressBarInit() self.update_timer.start() self.before_running() self._thread.start() else: self.button.setText("Continue") self._thread.quit() # Need to sync state (the worker must read the keep_running # state and stop) for reliable restart. self._thread.wait() def before_running(self): """Code that is run before running vizrank in its own thread""" pass def stopped(self): """Code that is run after stopping the vizrank thread""" pass
class OWDtoxsAlignment(widget.OWWidget): name = "Dtoxs Alignment" description = "Step 1 of the Dtoxs SOP. Uses a Burrows-Wheeler Aligner (BWA)." category = "RNASeq" icon = "icons/dtoxs-alignment2.svg" priority = 10 image_name = "biodepot/dtoxs_alignment" image_version = "latest" inputs = [("References", str, "set_refs"), ("Seqs", str, "set_seqs")] outputs = [("Counts", str)] auto_run = Setting(True) want_main_area = False def __init__(self): super().__init__() # Docker Client # This client talks to your local docker self.docker = DockerClient('unix:///var/run/docker.sock', 'local') # The directory of the seq data needed to run # the container will be set by the user before the # container can be run self.host_ref_dir = None self.host_seq_dir = None self.ref_dir_set = False self.seq_dir_set = False # The default write location of all widgets should be # ~/BioDepot/WidgetName/ # TODO is this an issue if multiple containers write to the same place? # TODO add timestamp to directory name to differentiate runs counts_dir = '~/BioDepot/Dtoxs_Alignment/Counts' if not os.path.exists(counts_dir): os.makedirs(counts_dir) self.host_counts_dir = self.docker.toHostDir(counts_dir) # GUI box = gui.widgetBox(self.controlArea, "Info") self.infoLabel = gui.widgetLabel( box, 'Connect to a Directory widget ' 'to specify location of reference ' 'and seq fastq files') self.infoLabel.setWordWrap(True) gui.checkBox(self.controlArea, self, 'auto_run', 'Run automatically when input set') self.btn_run = gui.button(self.controlArea, self, "Run", callback=self.btn_run_pushed) self.is_running = False """ Called when the user pushes 'Run' """ def btn_run_pushed(self): self.start_container(run_btn_pushed=True) """ Set references """ def set_refs(self, path): # When a user removes a connected Directory widget, # it sends a signal with path=None if path is None: self.ref_dir_set = False else: self.host_ref_dir = self.docker.toHostDir(path) if self.host_ref_dir is None: # TODO emit error self.ref_dir_set = False print('References set to invalid directory') else: self.ref_dir_set = True self.start_container(run_btn_pushed=False) """ Set seqs """ def set_seqs(self, path): # When a user removes a connected Directory widget, # it sends a signal with path=None if path is None: self.seq_dir_set = False else: self.host_seq_dir = self.docker.toHostDir(path) if self.host_seq_dir is None: # TODO emit error self.seq_dir_set = False print('Seq set to invalid directory') else: self.seq_dir_set = True self.start_container(run_btn_pushed=False) """ Pull image """ def pull_image(self): self.infoLabel.setText('Pulling \'' + self.image_name + ":" + self.image_version + '\' from Dockerhub...') self.setStatusMessage("Downloading...") self.progressBarInit() self.is_running = True self.btn_run.setEnabled(False) # Pull the image in a new thread self.pull_image_thread = QThread() self.pull_image_worker = PullImageWorker(self.docker, self.image_name, self.image_version) self.pull_image_worker.progress[int].connect(self.pull_image_progress) self.pull_image_worker.finished.connect(self.pull_image_finished) self.pull_image_worker.moveToThread(self.pull_image_thread) self.pull_image_thread.started.connect(self.pull_image_worker.work) self.pull_image_thread.start() @pyqtSlot(int, name="pullImageProgress") def pull_image_progress(self, val): self.progressBarSet(val) @pyqtSlot(name="pullImageFinished") def pull_image_finished(self): self.pull_image_thread.terminate() self.pull_image_thread.wait() self.infoLabel.setText('Finished pulling \'' + self.image_name + ":" + self.image_version + '\'') self.progressBarFinished() self.start_container() """ Alignment """ def start_container(self, run_btn_pushed=True): # Make sure both inputs are set if self.ref_dir_set and self.seq_dir_set: if not self.is_running and (self.auto_run or run_btn_pushed): # Make sure the docker image is downloaded if not self.docker.has_image(self.image_name, self.image_version): self.pull_image() else: self.run_container() else: self.infoLabel.setText( 'References and Seqs directories set.\nWaiting to run...') elif self.ref_dir_set: self.infoLabel.setText("Waiting for user to set Seqs directory.") elif self.seq_dir_set: self.infoLabel.setText( "Waiting for user to set References directory.") def run_container(self): self.is_running = True self.infoLabel.setText('Running alignment...') self.setStatusMessage('Running...') self.progressBarInit() # Run the container in a new thread self.run_container_thread = QThread() self.run_container_worker = RunAlignmentWorker(self.docker, self.image_name, self.host_ref_dir, self.host_seq_dir, self.host_counts_dir) self.run_container_worker.progress[int].connect( self.run_container_progress) self.run_container_worker.finished.connect(self.run_container_finished) self.run_container_worker.moveToThread(self.run_container_thread) self.run_container_thread.started.connect( self.run_container_worker.work) self.run_container_thread.start() @pyqtSlot(int, name="runContainerProgress") def run_container_progress(self, val): self.progressBarSet(val) @pyqtSlot(name="runContainerFinished") def run_container_finished(self): self.run_container_thread.terminate() self.run_container_thread.wait() self.infoLabel.setText("Finished running alignment!") self.btn_run.setEnabled(True) self.is_running = False self.btn_run.setText('Run again') self.setStatusMessage('Finished!') self.progressBarFinished() self.send("Counts", self.docker.toContainerDir(self.host_counts_dir))