{
    "file_diffs": [
        {
            "old_file_content": "from collections import defaultdict, namedtuple\nfrom contextlib import contextmanager\nfrom typing import Optional, Union\nfrom xml.sax.saxutils import escape\n\nimport numpy as np\n\nfrom AnyQt.QtCore import Qt, QRectF, pyqtSignal as Signal, QObject, QThread, \\\n    pyqtSlot as Slot\nfrom AnyQt.QtGui import QTransform, QPen, QBrush, QColor, QPainter, \\\n    QPainterPath, QKeyEvent\nfrom AnyQt.QtWidgets import \\\n    QGraphicsView, QGraphicsScene, \\\n    QGraphicsItem, QGraphicsRectItem, QGraphicsItemGroup, QSizePolicy, \\\n    QGraphicsPathItem\n\nfrom Orange.widgets.utils.signals import lazy_table_transform\n\nfrom Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable\nfrom Orange.data.util import array_equal, SharedComputeValue, get_unique_names\nfrom Orange.preprocess import decimal_binnings, time_binnings\nfrom Orange.projection.som import SOM\n\nfrom Orange.widgets import gui\nfrom Orange.widgets.utils.localization import pl\nfrom Orange.widgets.widget import OWWidget, Msg, Input, Output\nfrom Orange.widgets.settings import \\\n    DomainContextHandler, ContextSetting, Setting\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.annotated_data import \\\n    add_columns, group_values, \\\n    ANNOTATED_DATA_SIGNAL_NAME, ANNOTATED_DATA_FEATURE_NAME\nfrom Orange.widgets.utils.colorpalettes import \\\n    BinnedContinuousPalette, LimitedDiscretePalette, DiscretePalette\nfrom Orange.widgets.visualize.utils import CanvasRectangle, CanvasText\nfrom Orange.widgets.visualize.utils.plotutils import wrap_legend_items\n\n\nsqrt3_2 = np.sqrt(3) / 2\n\n\nclass SomView(QGraphicsView):\n    SelectionSet, SelectionNewGroup, SelectionAddToGroup, SelectionRemove \\\n        = range(4)\n    selection_changed = Signal(np.ndarray, int)\n    selection_moved = Signal(QKeyEvent)\n    selection_mark_changed = Signal(np.ndarray)\n\n    def __init__(self, scene):\n        super().__init__(scene)\n        self.__button_down_pos = None\n        self.size_x = self.size_y = 1\n        self.hexagonal = True\n\n    def set_dimensions(self, size_x, size_y, hexagonal):\n        self.size_x = size_x\n        self.size_y = size_y\n        self.hexagonal = hexagonal\n\n    def _get_marked_cells(self, event):\n        x0, y0 = self.__button_down_pos.x(), self.__button_down_pos.y()\n        pos = self.mapToScene(event.pos())\n        x1, y1 = pos.x(), pos.y()\n        x0, x1 = sorted((x0, x1))\n        y0, y1 = sorted((y0, y1))\n        selection = np.zeros((self.size_x, self.size_y), dtype=bool)\n        if self.hexagonal:\n            y0 = max(0, int(y0 / sqrt3_2 + 0.5))\n            y1 = min(self.size_y, int(np.ceil(y1 / sqrt3_2 + 0.5)))\n            for y in range(y0, y1):\n                if x1 < 0:\n                    continue\n                x0_ = max(0, int(x0 + 0.5 - (y % 2) / 2))\n                x1_ = min(self.size_x - y % 2,\n                          int(np.ceil(x1 + 0.5 - (y % 2) / 2)))\n                selection[x0_:x1_, y] = True\n        elif not(x1 < -0.5 or x0 > self.size_x - 0.5\n                 or y1 < -0.5 or y0 > self.size_y - 0.5):\n\n            def roundclip(z, zmax):\n                return int(np.clip(np.round(z), 0, zmax - 1))\\\n\n            x0 = roundclip(x0, self.size_x)\n            y0 = roundclip(y0, self.size_y)\n            x1 = roundclip(x1, self.size_x)\n            y1 = roundclip(y1, self.size_y)\n            selection[x0:x1 + 1, y0:y1 + 1] = True\n\n        return selection\n\n    def mousePressEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n\n        self.__button_down_pos = self.mapToScene(event.pos())\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        if event.buttons() != Qt.LeftButton:\n            return\n\n        self.selection_mark_changed.emit(self._get_marked_cells(event))\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n\n        if event.modifiers() & Qt.ShiftModifier:\n            if event.modifiers() & Qt.ControlModifier:\n                action = self.SelectionAddToGroup\n            else:\n                action = self.SelectionNewGroup\n        elif event.modifiers() & Qt.AltModifier:\n            action = self.SelectionRemove\n        else:\n            action = self.SelectionSet\n        selection = self._get_marked_cells(event)\n        self.selection_changed.emit(selection, action)\n        event.accept()\n\n    def keyPressEvent(self, event: QKeyEvent):\n        if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right):\n            self.selection_moved.emit(event)\n        else:\n            super().keyPressEvent(event)\n\n\nclass PieChart(QGraphicsItem):\n    def __init__(self, dist, r, colors):\n        super().__init__()\n        self.dist = dist\n        self.r = r\n        self.colors = colors\n\n    def boundingRect(self):\n        return QRectF(- self.r, - self.r, 2 * self.r, 2 * self.r)\n\n    def paint(self, painter, _option, _index):\n        painter.save()\n        start_angle = 0\n        pen = QPen(QBrush(Qt.black), 2)\n        pen.setCosmetic(True)\n        painter.setPen(pen)\n        painter.drawEllipse(self.boundingRect())\n        for angle, color in zip(self.dist * 16 * 360, self.colors):\n            if angle == 0:\n                continue\n            painter.setBrush(QBrush(color))\n            painter.setPen(Qt.NoPen)\n            painter.drawPie(self.boundingRect(), int(start_angle), int(angle))\n            start_angle += angle\n        painter.restore()\n\n\nclass ColoredCircle(QGraphicsItem):\n    def __init__(self, r, color, proportion):\n        super().__init__()\n        self.r = r\n        self.color = color\n        self.proportion = proportion\n\n    def boundingRect(self):\n        return QRectF(-self.r, -self.r, 2 * self.r, 2 * self.r)\n\n    def paint(self, painter, _option, _index):\n        painter.save()\n        pen = QPen(QBrush(self.color), 2)\n        pen.setCosmetic(True)\n        painter.setPen(pen)\n        painter.setBrush(QBrush(self.color.lighter(int(200 - 80 * self.proportion))))\n        painter.drawEllipse(self.boundingRect())\n        painter.restore()\n\n\n@contextmanager\ndef disconnected_spin(spin):\n    spin.blockSignals(True)\n    try:\n        yield\n    finally:\n        spin.blockSignals(False)\n\n\nN_ITERATIONS = 200\n\n\nclass SomSharedValueCompute:\n    def __init__(self, domain: Domain, model: SOM,\n                 offsets: Union[np.ndarray, None],\n                 scales: Union[np.ndarray, None]):\n        # offsets and scales are made immutable so that they are hashable\n        def immutable(a):\n            if a is not None:\n                a = a.copy()\n                a.flags.writeable = False\n            return a\n\n        self.domain = domain\n        self.model = model\n        self.offsets = immutable(offsets)\n        self.scales = immutable(scales)\n        self.__hash = None\n\n    def __getstate__(self):\n        state = self.__dict__.copy()\n        state[\"__hash\"] = None\n        return state\n\n    def __call__(self, data):\n        x = data.transform(self.domain).X\n        cont_x, mask, _, _ = SOM.prepare_data(x, self.offsets, self.scales)\n        winners = np.full((len(data), 2), np.nan)\n        distances = np.full(len(data), np.nan)\n        winners[mask], distances[mask] = self.model.winners(cont_x)\n        winners += 1\n        return winners, distances\n\n    # `offsets` and `scales` are ndarray's (and `model` contains ndarray's)\n    # Unless we __eq__ by identity, we can't properly define hash.\n    def __eq__(self, other):\n        return type(self) is type(other) and \\\n               self.domain == other.domain \\\n               and self.model == other.model \\\n               and np.array_equal(self.offsets, other.offsets) \\\n               and np.array_equal(self.scales, other.scales)\n\n    def __hash__(self):\n        if self.__hash is None:\n            self.__hash = hash((\n                self.domain, self.model,\n                tuple(self.offsets), tuple(self.scales)))\n        return self.__hash\n\n\nclass SomCellCompute(SharedComputeValue):\n    def __init__(self, compute_shared, dim_x, hexagonal):\n        super().__init__(compute_shared)\n        self.dim_x = dim_x\n        self.hexagonal = hexagonal\n\n    def __eq__(self, other):\n        return super().__eq__(other) \\\n            and self.dim_x == other.dim_x \\\n            and self.hexagonal == other.hexagonal\n\n    def __hash__(self):\n        return hash((super().__hash__(), self.dim_x, self.hexagonal))\n\n    def compute(self, _, shared_data):\n        coords = shared_data[0]\n        # coords are 1-based, subtract 1\n        col = (coords[:, 0] - 1) + (coords[:, 1] - 1) * self.dim_x\n        if self.hexagonal:\n            # 1 cell less after every two rows\n            col -= col // (self.dim_x * 2 - 1)\n        return col\n\n\nclass SomCoordsCompute(SharedComputeValue):\n    def __init__(self, shared, column):\n        super().__init__(shared)\n        self.column = column\n\n    def compute(self, _, shared_data):\n        return shared_data[0][:, self.column]\n\n    def __eq__(self, other):\n        return self.column == other.column and super().__eq__(other)\n\n    def __hash__(self):\n        return hash((super().__hash__(), self.column))\n\n\nclass SomErrorCompute(SharedComputeValue):\n    InheritEq = True\n\n    def compute(self, _, shared_data):\n        return shared_data[1]\n\n\nclass GetGroups:\n    # This assigns instances to selection groups; two instances that are not\n    # same cannot be considered equal, period.\n    InheritEq = True\n\n    def __init__(self, id_to_group, default, offset):\n        self.id_to_group = id_to_group\n        self.default = default + offset\n        self.offset = offset\n\n    def __call__(self, data, *args, **kwargs):\n        return np.array([self.id_to_group.get(id, self.default) - self.offset\n                        for id in data.ids])\n\n\nclass OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = \"self-organizing map, som\"\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"  # QGraphicsView\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Information(OWWidget.Information):\n        modified = Msg(\n            'The parameter settings have been changed. Press \"Start\" to '\n            \"rerun with the new settings.\"\n        )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        no_defined_colors = \\\n            Msg(\"'{}' has no defined values.\")\n        missing_values = Msg(\"{}\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        not_enough_data = Msg(\"SOM needs at least two data rows without missing values.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.scales = self.offsets = None\n        self.som = self.cells = self.member_data = None\n        self.selection = None\n\n        # self.colors holds a palette or None when we need to draw same-colored\n        # circles. This happens by user's choice or when the color attribute\n        # is numeric and has no defined values, so we can't construct bins\n        self.colors: Optional[DiscretePalette] = None\n        self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n        shape.currentIndexChanged.connect(self.on_parameter_change)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\",\n            widget=box3,\n            master=self,\n            minv=5,\n            maxv=100,\n            step=5,\n            alignment=Qt.AlignRight,\n            callback=self.on_parameter_change,\n        )\n        self.spin_x = gui.spin(**spinargs)\n        with disconnected_spin(self.spin_x):\n            self.spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        self.spin_y = gui.spin(**spinargs)\n        with disconnected_spin(self.spin_y):\n            self.spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box,\n            self,\n            \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\", \"Replicable random\"),\n            callback=self.on_parameter_change,\n        )\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, self.spin_x, self.spin_y, initialization, start\n        )\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @staticmethod\n    def _cont_domain(data):\n        attrs = data.domain.attributes\n        cont_attrs = [var for var in attrs if var.is_continuous]\n        if not cont_attrs:\n            return None\n        return Domain(cont_attrs)\n\n    @Inputs.data\n    def set_data(self, data):\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing:\n                self.Warning.missing_values(\n                    f'{missing} data {pl(missing, \"instance\")} with undefined value(s) {pl(missing, \"is|are\")} not shown.')\n\n        cont_x = self.cont_x.copy() if self.cont_x is not None else None\n        self.data = self.cont_x = None\n        self.offsets = self.scales = None\n        new_cont_x = None\n        self.closeContext()\n        self.clear_messages()\n\n        if data is not None:\n            cont_domain = self._cont_domain(data)\n            if cont_domain is None:\n                self.Error.no_numeric_variables()\n            else:\n                cont_attrs = cont_domain.attributes\n                if len(cont_attrs) < len(data.domain.attributes):\n                    self.Warning.ignoring_disc_variables()\n                if len(cont_attrs) == 1:\n                    self.Warning.single_attribute()\n\n                new_cont_x, mask, self.offsets, self.scales \\\n                    = SOM.prepare_data(data.transform(cont_domain).X)\n                rows = np.sum(mask)\n                if rows == len(mask):\n                    self.data = data\n                elif rows > 1:\n                    self.data = data[mask]\n                else:\n                    self.Error.not_enough_data()\n\n        invalidated = cont_x is None or new_cont_x is None \\\n            or not array_equal(cont_x, new_cont_x)\n        if invalidated:\n            self.stop_optimization_and_wait()\n            self.clear()\n\n        if self.data is not None:\n            self.cont_x = new_cont_x\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        if invalidated:\n            with disconnected_spin(self.spin_x), disconnected_spin(self.spin_y):\n                self.recompute_dimensions()\n            self.start_som()\n        else:\n            self._redraw()\n\n    def clear(self):\n        self.som = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n        self.on_parameter_change()\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def on_parameter_change(self):\n        self.Information.modified()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n\n        selection_np = np.array(self.selection)\n        if action == SomView.SelectionSet:\n            selection_np[:] = 0\n            selection_np[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            selection_np[selection] = max(1, np.max(selection_np))\n        elif action == SomView.SelectionNewGroup:\n            selection_np[selection] = 1 + np.max(selection_np)\n        elif action & SomView.SelectionRemove:\n            selection_np[selection] = 0\n        self.selection = selection_np.tolist()\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            x, y = x[0], y[0]\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x][y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection.tolist())\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x][y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        self.Information.modified.clear()\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.colors is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        # if self.colors is None, we use _draw_same_color and don't call\n        # this function\n        assert self.colors is not None\n\n        color_column = self.data.get_column(self.attr_color)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        values = list(values) + [\"(N/A)\"]\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        assert self.colors is not None  # if it were, we'd call _draw_same_color\n\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        assert self.colors is not None  # if it were, we'd call _draw_same_color\n\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        self.som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n        self.setInvalidated(True)\n\n        self._optimizer = Optimizer(self.cont_x, self.som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self.setInvalidated(False)\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments, _ = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                # -1.5: 1 is necessary, 0.5 is to add some border\n                -0.5, -1.5 / np.sqrt(3), self.size_x,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale - 1 / np.sqrt(3))\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.som is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        ngroups = int(self.selection is not None) and np.max(self.selection)\n        indices = np.zeros(len(self.data), dtype=int)\n        id_to_group = {}\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    group = self.selection[x][y]\n                    indices[rows] = group\n                    if group > 0:\n                        for id_ in self.data.ids[rows]:\n                            id_to_group[id_] = group\n\n        cont_domain = self._cont_domain(self.data)\n        shared_compute = SomSharedValueCompute(\n            cont_domain, self.som, self.offsets, self.scales)\n        cell = DiscreteVariable(\n            \"som_cell\",\n            values=tuple(\n                f\"r{row + 1}c{col + 1}\"\n                for row in range(self.size_y)\n                for col in range(self.size_x - (self.hexagonal and row % 2))),\n            compute_value=SomCellCompute(shared_compute,\n                                         self.size_x, self.hexagonal))\n        coordx = ContinuousVariable(\n            \"som_row\",\n            number_of_decimals=0,\n            compute_value=SomCoordsCompute(shared_compute, 1))\n        coordy = ContinuousVariable(\n            \"som_col\",\n            number_of_decimals=0,\n            compute_value=SomCoordsCompute(shared_compute, 0))\n        error = ContinuousVariable(\n            \"som_error\",\n            compute_value=SomErrorCompute(shared_compute)\n        )\n        som_attrs = (cell, coordx, coordy, error)\n\n        grp_values, _ = group_values(\n                indices, include_unselected=False, values=None)\n\n        def make_domain(values, default_grp, offset):\n            grp_var = DiscreteVariable(\n                get_unique_names(self.data.domain, ANNOTATED_DATA_FEATURE_NAME),\n                values,\n                compute_value=GetGroups(id_to_group, default_grp, offset))\n\n            if not self.data.domain.class_vars:\n                class_vars, metas = grp_var, som_attrs\n            else:\n                class_vars, metas = (), (grp_var,) + som_attrs\n            return add_columns(self.data.domain, (), class_vars, metas)\n\n        if np.any(indices):\n            sel_domain = make_domain(grp_values, np.nan, 1)\n            mask = np.flatnonzero(indices)\n            self.Outputs.selected_data.send(\n                lazy_table_transform(sel_domain, self.data[mask]))\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if ngroups > 1:\n            sel_domain = make_domain(grp_values + [\"Unselected\", ], ngroups, 1)\n        else:\n            sel_domain = make_domain((\"No\", \"Yes\"), 0, 0)\n        self.Outputs.annotated_data.send(\n            lazy_table_transform(sel_domain, self.data))\n\n    def set_color_bins(self):\n        self.Warning.no_defined_colors.clear()\n\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n            return\n\n        if self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n            return\n\n        col = self.data.get_column(self.attr_color)\n        col = col[np.isfinite(col)]\n        if not col.size:\n            self.Warning.no_defined_colors(self.attr_color)\n            self.thresholds = self.bin_labels = self.colors = None\n            return\n\n        if self.attr_color.is_time:\n            binning = time_binnings(col, min_bins=4)[-1]\n        else:\n            binning = decimal_binnings(col, min_bins=4)[-1]\n        self.thresholds = binning.thresholds[1:-1]\n        self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n        if not self.bin_labels[0] and binning.labels:\n            # Nan's are already filtered out, but it doesn't hurt much\n            # to use nanmax/nanmin\n            if np.nanmin(col) == np.nanmax(col):\n                # Handle a degenerate case with a single value\n                # Use the second threshold (because value must be smaller),\n                # but the first threshold as label (because that's the\n                # actual value in the data.\n                self.thresholds = binning.thresholds[1:]\n                self.bin_labels = (binning.labels[:1],\n                                   binning.short_labels[:1])\n        palette = BinnedContinuousPalette.from_palette(\n            self.attr_color.palette, binning.thresholds)\n        self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.colors is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels or ([], [])\n        if len(labels) <= 1:\n            return labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.colors:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")\n\n    @classmethod\n    def migrate_settings(cls, settings, _):\n        # previously selection was saved as np.ndarray which is not supported\n        # by widget-base, change selection to list\n        selection = settings.get('selection')\n        if selection is not None and isinstance(selection, np.ndarray):\n            settings['selection'] = selection.tolist()\n\n\ndef _draw_hexagon():\n    path = QPainterPath()\n    s = 0.5 / (np.sqrt(3) / 2)\n    path.moveTo(-0.5, -s / 2)\n    path.lineTo(-0.5, s / 2)\n    path.lineTo(0, s)\n    path.lineTo(0.5, s / 2)\n    path.lineTo(0.5, -s / 2)\n    path.lineTo(0, -s)\n    path.lineTo(-0.5, -s / 2)\n    return path\n\n\n_hexagon_path = _draw_hexagon()\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWSOM).run(Table(\"iris\"))\n",
            "new_file_content": "from collections import defaultdict, namedtuple\nfrom contextlib import contextmanager\nfrom typing import Optional, Union\nfrom xml.sax.saxutils import escape\n\nimport numpy as np\n\nfrom AnyQt.QtCore import Qt, QRectF, pyqtSignal as Signal, QObject, QThread, \\\n    pyqtSlot as Slot\nfrom AnyQt.QtGui import QTransform, QPen, QBrush, QColor, QPainter, \\\n    QPainterPath, QKeyEvent\nfrom AnyQt.QtWidgets import \\\n    QGraphicsView, QGraphicsScene, \\\n    QGraphicsItem, QGraphicsRectItem, QGraphicsItemGroup, QSizePolicy, \\\n    QGraphicsPathItem\n\nfrom Orange.widgets.utils.signals import lazy_table_transform\n\nfrom Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable\nfrom Orange.data.util import array_equal, SharedComputeValue, get_unique_names\nfrom Orange.preprocess import decimal_binnings, time_binnings\nfrom Orange.projection.som import SOM\n\nfrom Orange.widgets import gui\nfrom Orange.widgets.utils.localization import pl\nfrom Orange.widgets.widget import OWWidget, Msg, Input, Output\nfrom Orange.widgets.settings import \\\n    DomainContextHandler, ContextSetting, Setting\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.annotated_data import \\\n    add_columns, group_values, \\\n    ANNOTATED_DATA_SIGNAL_NAME, ANNOTATED_DATA_FEATURE_NAME\nfrom Orange.widgets.utils.colorpalettes import \\\n    BinnedContinuousPalette, LimitedDiscretePalette, DiscretePalette\nfrom Orange.widgets.visualize.utils import CanvasRectangle, CanvasText\nfrom Orange.widgets.visualize.utils.plotutils import wrap_legend_items\n\n\nsqrt3_2 = np.sqrt(3) / 2\n\n\nclass SomView(QGraphicsView):\n    SelectionSet, SelectionNewGroup, SelectionAddToGroup, SelectionRemove \\\n        = range(4)\n    selection_changed = Signal(np.ndarray, int)\n    selection_moved = Signal(QKeyEvent)\n    selection_mark_changed = Signal(np.ndarray)\n\n    def __init__(self, scene):\n        super().__init__(scene)\n        self.__button_down_pos = None\n        self.size_x = self.size_y = 1\n        self.hexagonal = True\n\n    def set_dimensions(self, size_x, size_y, hexagonal):\n        self.size_x = size_x\n        self.size_y = size_y\n        self.hexagonal = hexagonal\n\n    def _get_marked_cells(self, event):\n        x0, y0 = self.__button_down_pos.x(), self.__button_down_pos.y()\n        pos = self.mapToScene(event.pos())\n        x1, y1 = pos.x(), pos.y()\n        x0, x1 = sorted((x0, x1))\n        y0, y1 = sorted((y0, y1))\n        selection = np.zeros((self.size_x, self.size_y), dtype=bool)\n        if self.hexagonal:\n            y0 = max(0, int(y0 / sqrt3_2 + 0.5))\n            y1 = min(self.size_y, int(np.ceil(y1 / sqrt3_2 + 0.5)))\n            for y in range(y0, y1):\n                if x1 < 0:\n                    continue\n                x0_ = max(0, int(x0 + 0.5 - (y % 2) / 2))\n                x1_ = min(self.size_x - y % 2,\n                          int(np.ceil(x1 + 0.5 - (y % 2) / 2)))\n                selection[x0_:x1_, y] = True\n        elif not(x1 < -0.5 or x0 > self.size_x - 0.5\n                 or y1 < -0.5 or y0 > self.size_y - 0.5):\n\n            def roundclip(z, zmax):\n                return int(np.clip(np.round(z), 0, zmax - 1))\\\n\n            x0 = roundclip(x0, self.size_x)\n            y0 = roundclip(y0, self.size_y)\n            x1 = roundclip(x1, self.size_x)\n            y1 = roundclip(y1, self.size_y)\n            selection[x0:x1 + 1, y0:y1 + 1] = True\n\n        return selection\n\n    def mousePressEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n\n        self.__button_down_pos = self.mapToScene(event.pos())\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        if event.buttons() != Qt.LeftButton:\n            return\n\n        self.selection_mark_changed.emit(self._get_marked_cells(event))\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n\n        if event.modifiers() & Qt.ShiftModifier:\n            if event.modifiers() & Qt.ControlModifier:\n                action = self.SelectionAddToGroup\n            else:\n                action = self.SelectionNewGroup\n        elif event.modifiers() & Qt.AltModifier:\n            action = self.SelectionRemove\n        else:\n            action = self.SelectionSet\n        selection = self._get_marked_cells(event)\n        self.selection_changed.emit(selection, action)\n        event.accept()\n\n    def keyPressEvent(self, event: QKeyEvent):\n        if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right):\n            self.selection_moved.emit(event)\n        else:\n            super().keyPressEvent(event)\n\n\nclass PieChart(QGraphicsItem):\n    def __init__(self, dist, r, colors):\n        super().__init__()\n        self.dist = dist\n        self.r = r\n        self.colors = colors\n\n    def boundingRect(self):\n        return QRectF(- self.r, - self.r, 2 * self.r, 2 * self.r)\n\n    def paint(self, painter, _option, _index):\n        painter.save()\n        start_angle = 0\n        pen = QPen(QBrush(Qt.black), 2)\n        pen.setCosmetic(True)\n        painter.setPen(pen)\n        painter.drawEllipse(self.boundingRect())\n        for angle, color in zip(self.dist * 16 * 360, self.colors):\n            if angle == 0:\n                continue\n            painter.setBrush(QBrush(color))\n            painter.setPen(Qt.NoPen)\n            painter.drawPie(self.boundingRect(), int(start_angle), int(angle))\n            start_angle += angle\n        painter.restore()\n\n\nclass ColoredCircle(QGraphicsItem):\n    def __init__(self, r, color, proportion):\n        super().__init__()\n        self.r = r\n        self.color = color\n        self.proportion = proportion\n\n    def boundingRect(self):\n        return QRectF(-self.r, -self.r, 2 * self.r, 2 * self.r)\n\n    def paint(self, painter, _option, _index):\n        painter.save()\n        pen = QPen(QBrush(self.color), 2)\n        pen.setCosmetic(True)\n        painter.setPen(pen)\n        painter.setBrush(QBrush(self.color.lighter(int(200 - 80 * self.proportion))))\n        painter.drawEllipse(self.boundingRect())\n        painter.restore()\n\n\n@contextmanager\ndef disconnected_spin(spin):\n    spin.blockSignals(True)\n    try:\n        yield\n    finally:\n        spin.blockSignals(False)\n\n\nN_ITERATIONS = 200\n\n\nclass SomSharedValueCompute:\n    def __init__(self, domain: Domain, model: SOM,\n                 offsets: Union[np.ndarray, None],\n                 scales: Union[np.ndarray, None]):\n        # offsets and scales are made immutable so that they are hashable\n        def immutable(a):\n            if a is not None:\n                a = a.copy()\n                a.flags.writeable = False\n            return a\n\n        self.domain = domain\n        self.model = model\n        self.offsets = immutable(offsets)\n        self.scales = immutable(scales)\n        self.__hash = None\n\n    def __getstate__(self):\n        state = self.__dict__.copy()\n        state[\"__hash\"] = None\n        return state\n\n    def __call__(self, data):\n        x = data.transform(self.domain).X\n        cont_x, mask, _, _ = SOM.prepare_data(x, self.offsets, self.scales)\n        winners = np.full((len(data), 2), np.nan)\n        distances = np.full(len(data), np.nan)\n        winners[mask], distances[mask] = self.model.winners(cont_x)\n        winners += 1\n        return winners, distances\n\n    # `offsets` and `scales` are ndarray's (and `model` contains ndarray's)\n    # Unless we __eq__ by identity, we can't properly define hash.\n    def __eq__(self, other):\n        return type(self) is type(other) and \\\n               self.domain == other.domain \\\n               and self.model == other.model \\\n               and np.array_equal(self.offsets, other.offsets) \\\n               and np.array_equal(self.scales, other.scales)\n\n    def __hash__(self):\n        if self.__hash is None:\n            self.__hash = hash((\n                self.domain, self.model,\n                tuple(self.offsets), tuple(self.scales)))\n        return self.__hash\n\n\nclass SomCellCompute(SharedComputeValue):\n    def __init__(self, compute_shared, dim_x, hexagonal):\n        super().__init__(compute_shared)\n        self.dim_x = dim_x\n        self.hexagonal = hexagonal\n\n    def __eq__(self, other):\n        return super().__eq__(other) \\\n            and self.dim_x == other.dim_x \\\n            and self.hexagonal == other.hexagonal\n\n    def __hash__(self):\n        return hash((super().__hash__(), self.dim_x, self.hexagonal))\n\n    def compute(self, _, shared_data):\n        coords = shared_data[0]\n        # coords are 1-based, subtract 1\n        col = (coords[:, 0] - 1) + (coords[:, 1] - 1) * self.dim_x\n        if self.hexagonal:\n            # 1 cell less after every two rows\n            col -= col // (self.dim_x * 2 - 1)\n        return col\n\n\nclass SomCoordsCompute(SharedComputeValue):\n    def __init__(self, shared, column):\n        super().__init__(shared)\n        self.column = column\n\n    def compute(self, _, shared_data):\n        return shared_data[0][:, self.column]\n\n    def __eq__(self, other):\n        return self.column == other.column and super().__eq__(other)\n\n    def __hash__(self):\n        return hash((super().__hash__(), self.column))\n\n\nclass SomErrorCompute(SharedComputeValue):\n    InheritEq = True\n\n    def compute(self, _, shared_data):\n        return shared_data[1]\n\n\nclass GetGroups:\n    # This assigns instances to selection groups; two instances that are not\n    # same cannot be considered equal, period.\n    InheritEq = True\n\n    def __init__(self, id_to_group, default, offset):\n        self.id_to_group = id_to_group\n        self.default = default + offset\n        self.offset = offset\n\n    def __call__(self, data, *args, **kwargs):\n        return np.array([self.id_to_group.get(id, self.default) - self.offset\n                        for id in data.ids])\n\n\nclass OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = \"self-organizing map, som\"\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"  # QGraphicsView\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Information(OWWidget.Information):\n        modified = Msg(\n            'The parameter settings have been changed. Press \"Start\" to '\n            \"rerun with the new settings.\"\n        )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        no_defined_colors = \\\n            Msg(\"'{}' has no defined values.\")\n        missing_values = Msg(\"{}\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        not_enough_data = Msg(\"SOM needs at least two data rows without missing values.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.scales = self.offsets = None\n        self.som = self.cells = self.member_data = None\n        self.selection = None\n\n        # self.colors holds a palette or None when we need to draw same-colored\n        # circles. This happens by user's choice or when the color attribute\n        # is numeric and has no defined values, so we can't construct bins\n        self.colors: Optional[DiscretePalette] = None\n        self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n        shape.currentIndexChanged.connect(self.on_parameter_change)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\",\n            widget=box3,\n            master=self,\n            minv=5,\n            maxv=100,\n            step=5,\n            alignment=Qt.AlignRight,\n            callback=self.on_parameter_change,\n        )\n        self.spin_x = gui.spin(**spinargs)\n        with disconnected_spin(self.spin_x):\n            self.spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        self.spin_y = gui.spin(**spinargs)\n        with disconnected_spin(self.spin_y):\n            self.spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box,\n            self,\n            \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\", \"Replicable random\"),\n            callback=self.on_parameter_change,\n        )\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, self.spin_x, self.spin_y, initialization, start\n        )\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @staticmethod\n    def _cont_domain(data):\n        attrs = data.domain.attributes\n        cont_attrs = [var for var in attrs if var.is_continuous]\n        if not cont_attrs:\n            return None\n        return Domain(cont_attrs)\n\n    @Inputs.data\n    def set_data(self, data):\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing:\n                self.Warning.missing_values(\n                    f'{missing} data {pl(missing, \"instance\")} with undefined value(s) {pl(missing, \"is|are\")} not shown.')\n\n        cont_x = self.cont_x.copy() if self.cont_x is not None else None\n        self.data = self.cont_x = None\n        self.offsets = self.scales = None\n        new_cont_x = None\n        self.closeContext()\n        self.clear_messages()\n\n        if data is not None:\n            cont_domain = self._cont_domain(data)\n            if cont_domain is None:\n                self.Error.no_numeric_variables()\n            else:\n                cont_attrs = cont_domain.attributes\n                if len(cont_attrs) < len(data.domain.attributes):\n                    self.Warning.ignoring_disc_variables()\n                if len(cont_attrs) == 1:\n                    self.Warning.single_attribute()\n\n                new_cont_x, mask, self.offsets, self.scales \\\n                    = SOM.prepare_data(data.transform(cont_domain).X)\n                rows = np.sum(mask)\n                if rows == len(mask):\n                    self.data = data\n                elif rows > 1:\n                    self.data = data[mask]\n                else:\n                    self.Error.not_enough_data()\n\n        invalidated = cont_x is None or new_cont_x is None \\\n            or not array_equal(cont_x, new_cont_x)\n        if invalidated:\n            self.stop_optimization_and_wait()\n            self.clear()\n\n        if self.data is not None:\n            self.cont_x = new_cont_x\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        if invalidated:\n            with disconnected_spin(self.spin_x), disconnected_spin(self.spin_y):\n                self.recompute_dimensions()\n            self.start_som()\n        else:\n            self._redraw()\n\n    def clear(self):\n        self.som = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n        self.on_parameter_change()\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def on_parameter_change(self):\n        self.Information.modified()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n\n        selection_np = np.array(self.selection)\n        if action == SomView.SelectionSet:\n            selection_np[:] = 0\n            selection_np[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            selection_np[selection] = max(1, np.max(selection_np))\n        elif action == SomView.SelectionNewGroup:\n            selection_np[selection] = 1 + np.max(selection_np)\n        elif action & SomView.SelectionRemove:\n            selection_np[selection] = 0\n        self.selection = selection_np.tolist()\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            x, y = x[0], y[0]\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x][y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection.tolist())\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x][y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        self.Information.modified.clear()\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.colors is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        # if self.colors is None, we use _draw_same_color and don't call\n        # this function\n        assert self.colors is not None\n\n        color_column = self.data.get_column(self.attr_color)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        values = list(values) + [\"(N/A)\"]\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        assert self.colors is not None  # if it were, we'd call _draw_same_color\n\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        assert self.colors is not None  # if it were, we'd call _draw_same_color\n\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        self.som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n        self.setInvalidated(True)\n\n        self._optimizer = Optimizer(self.cont_x, self.som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self.setInvalidated(False)\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments, _ = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                # -1.5: 1 is necessary, 0.5 is to add some border\n                -0.5, -1.5 / np.sqrt(3), self.size_x,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale - 1 / np.sqrt(3))\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.som is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        ngroups = int(self.selection is not None) and np.max(self.selection)\n        indices = np.zeros(len(self.data), dtype=int)\n        id_to_group = {}\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    group = self.selection[x][y]\n                    indices[rows] = group\n                    if group > 0:\n                        for id_ in self.data.ids[rows]:\n                            id_to_group[id_] = group\n\n        cont_domain = self._cont_domain(self.data)\n        shared_compute = SomSharedValueCompute(\n            cont_domain, self.som, self.offsets, self.scales)\n        cell = DiscreteVariable(\n            \"som_cell\",\n            values=tuple(\n                f\"r{row + 1}c{col + 1}\"\n                for row in range(self.size_y)\n                for col in range(self.size_x - (self.hexagonal and row % 2))),\n            compute_value=SomCellCompute(shared_compute,\n                                         self.size_x, self.hexagonal))\n        coordx = ContinuousVariable(\n            \"som_row\",\n            number_of_decimals=0,\n            compute_value=SomCoordsCompute(shared_compute, 1))\n        coordy = ContinuousVariable(\n            \"som_col\",\n            number_of_decimals=0,\n            compute_value=SomCoordsCompute(shared_compute, 0))\n        error = ContinuousVariable(\n            \"som_error\",\n            compute_value=SomErrorCompute(shared_compute)\n        )\n        som_attrs = (cell, coordx, coordy, error)\n\n        grp_values, _ = group_values(\n                indices, include_unselected=False, values=None)\n\n        def make_domain(values, default_grp, offset):\n            grp_var = DiscreteVariable(\n                get_unique_names(self.data.domain, ANNOTATED_DATA_FEATURE_NAME),\n                values,\n                compute_value=GetGroups(id_to_group, default_grp, offset))\n\n            if not self.data.domain.class_vars:\n                self.data.domain.class_vars = ()\n                class_vars, metas = (grp_var,), som_attrs\n            else:\n                class_vars, metas = (), (grp_var,) + som_attrs\n            return add_columns(self.data.domain, (), class_vars, metas)\n\n        if np.any(indices):\n            sel_domain = make_domain(grp_values, np.nan, 1)\n            mask = np.flatnonzero(indices)\n            self.Outputs.selected_data.send(\n                lazy_table_transform(sel_domain, self.data[mask]))\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if ngroups > 1:\n            sel_domain = make_domain(grp_values + [\"Unselected\", ], ngroups, 1)\n        else:\n            sel_domain = make_domain((\"No\", \"Yes\"), 0, 0)\n        self.Outputs.annotated_data.send(\n            lazy_table_transform(sel_domain, self.data))\n\n    def set_color_bins(self):\n        self.Warning.no_defined_colors.clear()\n\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n            return\n\n        if self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n            return\n\n        col = self.data.get_column(self.attr_color)\n        col = col[np.isfinite(col)]\n        if not col.size:\n            self.Warning.no_defined_colors(self.attr_color)\n            self.thresholds = self.bin_labels = self.colors = None\n            return\n\n        if self.attr_color.is_time:\n            binning = time_binnings(col, min_bins=4)[-1]\n        else:\n            binning = decimal_binnings(col, min_bins=4)[-1]\n        self.thresholds = binning.thresholds[1:-1]\n        self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n        if not self.bin_labels[0] and binning.labels:\n            # Nan's are already filtered out, but it doesn't hurt much\n            # to use nanmax/nanmin\n            if np.nanmin(col) == np.nanmax(col):\n                # Handle a degenerate case with a single value\n                # Use the second threshold (because value must be smaller),\n                # but the first threshold as label (because that's the\n                # actual value in the data.\n                self.thresholds = binning.thresholds[1:]\n                self.bin_labels = (binning.labels[:1],\n                                   binning.short_labels[:1])\n        palette = BinnedContinuousPalette.from_palette(\n            self.attr_color.palette, binning.thresholds)\n        self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.colors is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels or ([], [])\n        if len(labels) <= 1:\n            return labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.colors:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")\n\n    @classmethod\n    def migrate_settings(cls, settings, _):\n        # previously selection was saved as np.ndarray which is not supported\n        # by widget-base, change selection to list\n        selection = settings.get('selection')\n        if selection is not None and isinstance(selection, np.ndarray):\n            settings['selection'] = selection.tolist()\n\n\ndef _draw_hexagon():\n    path = QPainterPath()\n    s = 0.5 / (np.sqrt(3) / 2)\n    path.moveTo(-0.5, -s / 2)\n    path.lineTo(-0.5, s / 2)\n    path.lineTo(0, s)\n    path.lineTo(0.5, s / 2)\n    path.lineTo(0.5, -s / 2)\n    path.lineTo(0, -s)\n    path.lineTo(-0.5, -s / 2)\n    return path\n\n\n_hexagon_path = _draw_hexagon()\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWSOM).run(Table(\"iris\"))\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/unsupervised/owsom.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "84c11e311",
                "new_commit_hash": "6d5a11aad",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/unsupervised/owsom.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/unsupervised/owsom.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 1018,
                            "length": 7
                        },
                        "new_range": {
                            "start": 1018,
                            "length": 8
                        },
                        "section": "class OWSOM(OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "                compute_value=GetGroups(id_to_group, default_grp, offset))",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "            if not self.data.domain.class_vars:",
                                "type": "context"
                            },
                            {
                                "content": "                class_vars, metas = grp_var, som_attrs",
                                "type": "deleted"
                            },
                            {
                                "content": "                self.data.domain.class_vars = ()",
                                "type": "added"
                            },
                            {
                                "content": "                class_vars, metas = (grp_var,), som_attrs",
                                "type": "added"
                            },
                            {
                                "content": "            else:",
                                "type": "context"
                            },
                            {
                                "content": "                class_vars, metas = (), (grp_var,) + som_attrs",
                                "type": "context"
                            },
                            {
                                "content": "            return add_columns(self.data.domain, (), class_vars, metas)",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/owsom.py",
                            "type": "class",
                            "name": "OWSOM",
                            "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = \"self-organizing map, som\"\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"  # QGraphicsView\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Information(OWWidget.Information):\n        modified = Msg(\n            'The parameter settings have been changed. Press \"Start\" to '\n            \"rerun with the new settings.\"\n        )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        no_defined_colors = \\\n            Msg(\"'{}' has no defined values.\")\n        missing_values = Msg(\"{}\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        not_enough_data = Msg(\"SOM needs at least two data rows without missing values.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.scales = self.offsets = None\n        self.som = self.cells = self.member_data = None\n        self.selection = None\n\n        # self.colors holds a palette or None when we need to draw same-colored\n        # circles. This happens by user's choice or when the color attribute\n        # is numeric and has no defined values, so we can't construct bins\n        self.colors: Optional[DiscretePalette] = None\n        self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n        shape.currentIndexChanged.connect(self.on_parameter_change)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\",\n            widget=box3,\n            master=self,\n            minv=5,\n            maxv=100,\n            step=5,\n            alignment=Qt.AlignRight,\n            callback=self.on_parameter_change,\n        )\n        self.spin_x = gui.spin(**spinargs)\n        with disconnected_spin(self.spin_x):\n            self.spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        self.spin_y = gui.spin(**spinargs)\n        with disconnected_spin(self.spin_y):\n            self.spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box,\n            self,\n            \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\", \"Replicable random\"),\n            callback=self.on_parameter_change,\n        )\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, self.spin_x, self.spin_y, initialization, start\n        )\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @staticmethod\n    def _cont_domain(data):\n        attrs = data.domain.attributes\n        cont_attrs = [var for var in attrs if var.is_continuous]\n        if not cont_attrs:\n            return None\n        return Domain(cont_attrs)\n\n    @Inputs.data\n    def set_data(self, data):\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing:\n                self.Warning.missing_values(\n                    f'{missing} data {pl(missing, \"instance\")} with undefined value(s) {pl(missing, \"is|are\")} not shown.')\n\n        cont_x = self.cont_x.copy() if self.cont_x is not None else None\n        self.data = self.cont_x = None\n        self.offsets = self.scales = None\n        new_cont_x = None\n        self.closeContext()\n        self.clear_messages()\n\n        if data is not None:\n            cont_domain = self._cont_domain(data)\n            if cont_domain is None:\n                self.Error.no_numeric_variables()\n            else:\n                cont_attrs = cont_domain.attributes\n                if len(cont_attrs) < len(data.domain.attributes):\n                    self.Warning.ignoring_disc_variables()\n                if len(cont_attrs) == 1:\n                    self.Warning.single_attribute()\n\n                new_cont_x, mask, self.offsets, self.scales \\\n                    = SOM.prepare_data(data.transform(cont_domain).X)\n                rows = np.sum(mask)\n                if rows == len(mask):\n                    self.data = data\n                elif rows > 1:\n                    self.data = data[mask]\n                else:\n                    self.Error.not_enough_data()\n\n        invalidated = cont_x is None or new_cont_x is None \\\n            or not array_equal(cont_x, new_cont_x)\n        if invalidated:\n            self.stop_optimization_and_wait()\n            self.clear()\n\n        if self.data is not None:\n            self.cont_x = new_cont_x\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        if invalidated:\n            with disconnected_spin(self.spin_x), disconnected_spin(self.spin_y):\n                self.recompute_dimensions()\n            self.start_som()\n        else:\n            self._redraw()\n\n    def clear(self):\n        self.som = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n        self.on_parameter_change()\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def on_parameter_change(self):\n        self.Information.modified()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n\n        selection_np = np.array(self.selection)\n        if action == SomView.SelectionSet:\n            selection_np[:] = 0\n            selection_np[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            selection_np[selection] = max(1, np.max(selection_np))\n        elif action == SomView.SelectionNewGroup:\n            selection_np[selection] = 1 + np.max(selection_np)\n        elif action & SomView.SelectionRemove:\n            selection_np[selection] = 0\n        self.selection = selection_np.tolist()\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            x, y = x[0], y[0]\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x][y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection.tolist())\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x][y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        self.Information.modified.clear()\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.colors is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        # if self.colors is None, we use _draw_same_color and don't call\n        # this function\n        assert self.colors is not None\n\n        color_column = self.data.get_column(self.attr_color)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        values = list(values) + [\"(N/A)\"]\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        assert self.colors is not None  # if it were, we'd call _draw_same_color\n\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        assert self.colors is not None  # if it were, we'd call _draw_same_color\n\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        self.som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n        self.setInvalidated(True)\n\n        self._optimizer = Optimizer(self.cont_x, self.som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self.setInvalidated(False)\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments, _ = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                # -1.5: 1 is necessary, 0.5 is to add some border\n                -0.5, -1.5 / np.sqrt(3), self.size_x,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale - 1 / np.sqrt(3))\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.som is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        ngroups = int(self.selection is not None) and np.max(self.selection)\n        indices = np.zeros(len(self.data), dtype=int)\n        id_to_group = {}\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    group = self.selection[x][y]\n                    indices[rows] = group\n                    if group > 0:\n                        for id_ in self.data.ids[rows]:\n                            id_to_group[id_] = group\n\n        cont_domain = self._cont_domain(self.data)\n        shared_compute = SomSharedValueCompute(\n            cont_domain, self.som, self.offsets, self.scales)\n        cell = DiscreteVariable(\n            \"som_cell\",\n            values=tuple(\n                f\"r{row + 1}c{col + 1}\"\n                for row in range(self.size_y)\n                for col in range(self.size_x - (self.hexagonal and row % 2))),\n            compute_value=SomCellCompute(shared_compute,\n                                         self.size_x, self.hexagonal))\n        coordx = ContinuousVariable(\n            \"som_row\",\n            number_of_decimals=0,\n            compute_value=SomCoordsCompute(shared_compute, 1))\n        coordy = ContinuousVariable(\n            \"som_col\",\n            number_of_decimals=0,\n            compute_value=SomCoordsCompute(shared_compute, 0))\n        error = ContinuousVariable(\n            \"som_error\",\n            compute_value=SomErrorCompute(shared_compute)\n        )\n        som_attrs = (cell, coordx, coordy, error)\n\n        grp_values, _ = group_values(\n                indices, include_unselected=False, values=None)\n\n        def make_domain(values, default_grp, offset):\n            grp_var = DiscreteVariable(\n                get_unique_names(self.data.domain, ANNOTATED_DATA_FEATURE_NAME),\n                values,\n                compute_value=GetGroups(id_to_group, default_grp, offset))\n\n            if not self.data.domain.class_vars:\n                class_vars, metas = grp_var, som_attrs\n            else:\n                class_vars, metas = (), (grp_var,) + som_attrs\n            return add_columns(self.data.domain, (), class_vars, metas)\n\n        if np.any(indices):\n            sel_domain = make_domain(grp_values, np.nan, 1)\n            mask = np.flatnonzero(indices)\n            self.Outputs.selected_data.send(\n                lazy_table_transform(sel_domain, self.data[mask]))\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if ngroups > 1:\n            sel_domain = make_domain(grp_values + [\"Unselected\", ], ngroups, 1)\n        else:\n            sel_domain = make_domain((\"No\", \"Yes\"), 0, 0)\n        self.Outputs.annotated_data.send(\n            lazy_table_transform(sel_domain, self.data))\n\n    def set_color_bins(self):\n        self.Warning.no_defined_colors.clear()\n\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n            return\n\n        if self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n            return\n\n        col = self.data.get_column(self.attr_color)\n        col = col[np.isfinite(col)]\n        if not col.size:\n            self.Warning.no_defined_colors(self.attr_color)\n            self.thresholds = self.bin_labels = self.colors = None\n            return\n\n        if self.attr_color.is_time:\n            binning = time_binnings(col, min_bins=4)[-1]\n        else:\n            binning = decimal_binnings(col, min_bins=4)[-1]\n        self.thresholds = binning.thresholds[1:-1]\n        self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n        if not self.bin_labels[0] and binning.labels:\n            # Nan's are already filtered out, but it doesn't hurt much\n            # to use nanmax/nanmin\n            if np.nanmin(col) == np.nanmax(col):\n                # Handle a degenerate case with a single value\n                # Use the second threshold (because value must be smaller),\n                # but the first threshold as label (because that's the\n                # actual value in the data.\n                self.thresholds = binning.thresholds[1:]\n                self.bin_labels = (binning.labels[:1],\n                                   binning.short_labels[:1])\n        palette = BinnedContinuousPalette.from_palette(\n            self.attr_color.palette, binning.thresholds)\n        self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.colors is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels or ([], [])\n        if len(labels) <= 1:\n            return labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.colors:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")\n\n    @classmethod\n    def migrate_settings(cls, settings, _):\n        # previously selection was saved as np.ndarray which is not supported\n        # by widget-base, change selection to list\n        selection = settings.get('selection')\n        if selection is not None and isinstance(selection, np.ndarray):\n            settings['selection'] = selection.tolist()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 298,
                            "end_lineno": 1141,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/unsupervised/owsom.py",
                            "type": "method",
                            "name": "OWSOM.update_output",
                            "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = \"self-organizing map, som\"\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"  # QGraphicsView\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Information(OWWidget.Information):\n        modified = Msg(\n            'The parameter settings have been changed. Press \"Start\" to '\n            \"rerun with the new settings.\"\n        )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        no_defined_colors = \\\n            Msg(\"'{}' has no defined values.\")\n        missing_values = Msg(\"{}\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        not_enough_data = Msg(\"SOM needs at least two data rows without missing values.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.scales = self.offsets = None\n        self.som = self.cells = self.member_data = None\n        self.selection = None\n\n        # self.colors holds a palette or None when we need to draw same-colored\n        # circles. This happens by user's choice or when the color attribute\n        # is numeric and has no defined values, so we can't construct bins\n        self.colors: Optional[DiscretePalette] = None\n        self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n        shape.currentIndexChanged.connect(self.on_parameter_change)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\",\n            widget=box3,\n            master=self,\n            minv=5,\n            maxv=100,\n            step=5,\n            alignment=Qt.AlignRight,\n            callback=self.on_parameter_change,\n        )\n        self.spin_x = gui.spin(**spinargs)\n        with disconnected_spin(self.spin_x):\n            self.spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        self.spin_y = gui.spin(**spinargs)\n        with disconnected_spin(self.spin_y):\n            self.spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box,\n            self,\n            \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\", \"Replicable random\"),\n            callback=self.on_parameter_change,\n        )\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, self.spin_x, self.spin_y, initialization, start\n        )\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @staticmethod\n    def _cont_domain(data):\n        attrs = data.domain.attributes\n        cont_attrs = [var for var in attrs if var.is_continuous]\n        if not cont_attrs:\n            return None\n        return Domain(cont_attrs)\n\n    @Inputs.data\n    def set_data(self, data):\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing:\n                self.Warning.missing_values(\n                    f'{missing} data {pl(missing, \"instance\")} with undefined value(s) {pl(missing, \"is|are\")} not shown.')\n\n        cont_x = self.cont_x.copy() if self.cont_x is not None else None\n        self.data = self.cont_x = None\n        self.offsets = self.scales = None\n        new_cont_x = None\n        self.closeContext()\n        self.clear_messages()\n\n        if data is not None:\n            cont_domain = self._cont_domain(data)\n            if cont_domain is None:\n                self.Error.no_numeric_variables()\n            else:\n                cont_attrs = cont_domain.attributes\n                if len(cont_attrs) < len(data.domain.attributes):\n                    self.Warning.ignoring_disc_variables()\n                if len(cont_attrs) == 1:\n                    self.Warning.single_attribute()\n\n                new_cont_x, mask, self.offsets, self.scales \\\n                    = SOM.prepare_data(data.transform(cont_domain).X)\n                rows = np.sum(mask)\n                if rows == len(mask):\n                    self.data = data\n                elif rows > 1:\n                    self.data = data[mask]\n                else:\n                    self.Error.not_enough_data()\n\n        invalidated = cont_x is None or new_cont_x is None \\\n            or not array_equal(cont_x, new_cont_x)\n        if invalidated:\n            self.stop_optimization_and_wait()\n            self.clear()\n\n        if self.data is not None:\n            self.cont_x = new_cont_x\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        if invalidated:\n            with disconnected_spin(self.spin_x), disconnected_spin(self.spin_y):\n                self.recompute_dimensions()\n            self.start_som()\n        else:\n            self._redraw()\n\n    def clear(self):\n        self.som = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n        self.on_parameter_change()\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def on_parameter_change(self):\n        self.Information.modified()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n\n        selection_np = np.array(self.selection)\n        if action == SomView.SelectionSet:\n            selection_np[:] = 0\n            selection_np[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            selection_np[selection] = max(1, np.max(selection_np))\n        elif action == SomView.SelectionNewGroup:\n            selection_np[selection] = 1 + np.max(selection_np)\n        elif action & SomView.SelectionRemove:\n            selection_np[selection] = 0\n        self.selection = selection_np.tolist()\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            x, y = x[0], y[0]\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x][y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection.tolist())\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x][y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        self.Information.modified.clear()\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.colors is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        # if self.colors is None, we use _draw_same_color and don't call\n        # this function\n        assert self.colors is not None\n\n        color_column = self.data.get_column(self.attr_color)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        values = list(values) + [\"(N/A)\"]\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        assert self.colors is not None  # if it were, we'd call _draw_same_color\n\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        assert self.colors is not None  # if it were, we'd call _draw_same_color\n\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        self.som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n        self.setInvalidated(True)\n\n        self._optimizer = Optimizer(self.cont_x, self.som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self.setInvalidated(False)\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments, _ = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                # -1.5: 1 is necessary, 0.5 is to add some border\n                -0.5, -1.5 / np.sqrt(3), self.size_x,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale - 1 / np.sqrt(3))\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.som is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        ngroups = int(self.selection is not None) and np.max(self.selection)\n        indices = np.zeros(len(self.data), dtype=int)\n        id_to_group = {}\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    group = self.selection[x][y]\n                    indices[rows] = group\n                    if group > 0:\n                        for id_ in self.data.ids[rows]:\n                            id_to_group[id_] = group\n\n        cont_domain = self._cont_domain(self.data)\n        shared_compute = SomSharedValueCompute(\n            cont_domain, self.som, self.offsets, self.scales)\n        cell = DiscreteVariable(\n            \"som_cell\",\n            values=tuple(\n                f\"r{row + 1}c{col + 1}\"\n                for row in range(self.size_y)\n                for col in range(self.size_x - (self.hexagonal and row % 2))),\n            compute_value=SomCellCompute(shared_compute,\n                                         self.size_x, self.hexagonal))\n        coordx = ContinuousVariable(\n            \"som_row\",\n            number_of_decimals=0,\n            compute_value=SomCoordsCompute(shared_compute, 1))\n        coordy = ContinuousVariable(\n            \"som_col\",\n            number_of_decimals=0,\n            compute_value=SomCoordsCompute(shared_compute, 0))\n        error = ContinuousVariable(\n            \"som_error\",\n            compute_value=SomErrorCompute(shared_compute)\n        )\n        som_attrs = (cell, coordx, coordy, error)\n\n        grp_values, _ = group_values(\n                indices, include_unselected=False, values=None)\n\n        def make_domain(values, default_grp, offset):\n            grp_var = DiscreteVariable(\n                get_unique_names(self.data.domain, ANNOTATED_DATA_FEATURE_NAME),\n                values,\n                compute_value=GetGroups(id_to_group, default_grp, offset))\n\n            if not self.data.domain.class_vars:\n                class_vars, metas = grp_var, som_attrs\n            else:\n                class_vars, metas = (), (grp_var,) + som_attrs\n            return add_columns(self.data.domain, (), class_vars, metas)\n\n        if np.any(indices):\n            sel_domain = make_domain(grp_values, np.nan, 1)\n            mask = np.flatnonzero(indices)\n            self.Outputs.selected_data.send(\n                lazy_table_transform(sel_domain, self.data[mask]))\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if ngroups > 1:\n            sel_domain = make_domain(grp_values + [\"Unselected\", ], ngroups, 1)\n        else:\n            sel_domain = make_domain((\"No\", \"Yes\"), 0, 0)\n        self.Outputs.annotated_data.send(\n            lazy_table_transform(sel_domain, self.data))\n\n    def set_color_bins(self):\n        self.Warning.no_defined_colors.clear()\n\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n            return\n\n        if self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n            return\n\n        col = self.data.get_column(self.attr_color)\n        col = col[np.isfinite(col)]\n        if not col.size:\n            self.Warning.no_defined_colors(self.attr_color)\n            self.thresholds = self.bin_labels = self.colors = None\n            return\n\n        if self.attr_color.is_time:\n            binning = time_binnings(col, min_bins=4)[-1]\n        else:\n            binning = decimal_binnings(col, min_bins=4)[-1]\n        self.thresholds = binning.thresholds[1:-1]\n        self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n        if not self.bin_labels[0] and binning.labels:\n            # Nan's are already filtered out, but it doesn't hurt much\n            # to use nanmax/nanmin\n            if np.nanmin(col) == np.nanmax(col):\n                # Handle a degenerate case with a single value\n                # Use the second threshold (because value must be smaller),\n                # but the first threshold as label (because that's the\n                # actual value in the data.\n                self.thresholds = binning.thresholds[1:]\n                self.bin_labels = (binning.labels[:1],\n                                   binning.short_labels[:1])\n        palette = BinnedContinuousPalette.from_palette(\n            self.attr_color.palette, binning.thresholds)\n        self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.colors is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels or ([], [])\n        if len(labels) <= 1:\n            return labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.colors:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")\n\n    @classmethod\n    def migrate_settings(cls, settings, _):\n        # previously selection was saved as np.ndarray which is not supported\n        # by widget-base, change selection to list\n        selection = settings.get('selection')\n        if selection is not None and isinstance(selection, np.ndarray):\n            settings['selection'] = selection.tolist()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 967,
                            "end_lineno": 1039,
                            "parent": {
                                "file_name": "Orange/widgets/unsupervised/owsom.py",
                                "type": "class",
                                "name": "OWSOM",
                                "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = \"self-organizing map, som\"\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"  # QGraphicsView\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Information(OWWidget.Information):\n        modified = Msg(\n            'The parameter settings have been changed. Press \"Start\" to '\n            \"rerun with the new settings.\"\n        )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        no_defined_colors = \\\n            Msg(\"'{}' has no defined values.\")\n        missing_values = Msg(\"{}\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        not_enough_data = Msg(\"SOM needs at least two data rows without missing values.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.scales = self.offsets = None\n        self.som = self.cells = self.member_data = None\n        self.selection = None\n\n        # self.colors holds a palette or None when we need to draw same-colored\n        # circles. This happens by user's choice or when the color attribute\n        # is numeric and has no defined values, so we can't construct bins\n        self.colors: Optional[DiscretePalette] = None\n        self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n        shape.currentIndexChanged.connect(self.on_parameter_change)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\",\n            widget=box3,\n            master=self,\n            minv=5,\n            maxv=100,\n            step=5,\n            alignment=Qt.AlignRight,\n            callback=self.on_parameter_change,\n        )\n        self.spin_x = gui.spin(**spinargs)\n        with disconnected_spin(self.spin_x):\n            self.spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        self.spin_y = gui.spin(**spinargs)\n        with disconnected_spin(self.spin_y):\n            self.spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box,\n            self,\n            \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\", \"Replicable random\"),\n            callback=self.on_parameter_change,\n        )\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, self.spin_x, self.spin_y, initialization, start\n        )\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @staticmethod\n    def _cont_domain(data):\n        attrs = data.domain.attributes\n        cont_attrs = [var for var in attrs if var.is_continuous]\n        if not cont_attrs:\n            return None\n        return Domain(cont_attrs)\n\n    @Inputs.data\n    def set_data(self, data):\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing:\n                self.Warning.missing_values(\n                    f'{missing} data {pl(missing, \"instance\")} with undefined value(s) {pl(missing, \"is|are\")} not shown.')\n\n        cont_x = self.cont_x.copy() if self.cont_x is not None else None\n        self.data = self.cont_x = None\n        self.offsets = self.scales = None\n        new_cont_x = None\n        self.closeContext()\n        self.clear_messages()\n\n        if data is not None:\n            cont_domain = self._cont_domain(data)\n            if cont_domain is None:\n                self.Error.no_numeric_variables()\n            else:\n                cont_attrs = cont_domain.attributes\n                if len(cont_attrs) < len(data.domain.attributes):\n                    self.Warning.ignoring_disc_variables()\n                if len(cont_attrs) == 1:\n                    self.Warning.single_attribute()\n\n                new_cont_x, mask, self.offsets, self.scales \\\n                    = SOM.prepare_data(data.transform(cont_domain).X)\n                rows = np.sum(mask)\n                if rows == len(mask):\n                    self.data = data\n                elif rows > 1:\n                    self.data = data[mask]\n                else:\n                    self.Error.not_enough_data()\n\n        invalidated = cont_x is None or new_cont_x is None \\\n            or not array_equal(cont_x, new_cont_x)\n        if invalidated:\n            self.stop_optimization_and_wait()\n            self.clear()\n\n        if self.data is not None:\n            self.cont_x = new_cont_x\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        if invalidated:\n            with disconnected_spin(self.spin_x), disconnected_spin(self.spin_y):\n                self.recompute_dimensions()\n            self.start_som()\n        else:\n            self._redraw()\n\n    def clear(self):\n        self.som = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n        self.on_parameter_change()\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def on_parameter_change(self):\n        self.Information.modified()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n\n        selection_np = np.array(self.selection)\n        if action == SomView.SelectionSet:\n            selection_np[:] = 0\n            selection_np[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            selection_np[selection] = max(1, np.max(selection_np))\n        elif action == SomView.SelectionNewGroup:\n            selection_np[selection] = 1 + np.max(selection_np)\n        elif action & SomView.SelectionRemove:\n            selection_np[selection] = 0\n        self.selection = selection_np.tolist()\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            x, y = x[0], y[0]\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x][y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection.tolist())\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x][y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        self.Information.modified.clear()\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.colors is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        # if self.colors is None, we use _draw_same_color and don't call\n        # this function\n        assert self.colors is not None\n\n        color_column = self.data.get_column(self.attr_color)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        values = list(values) + [\"(N/A)\"]\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        assert self.colors is not None  # if it were, we'd call _draw_same_color\n\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        assert self.colors is not None  # if it were, we'd call _draw_same_color\n\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        self.som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n        self.setInvalidated(True)\n\n        self._optimizer = Optimizer(self.cont_x, self.som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self.setInvalidated(False)\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments, _ = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                # -1.5: 1 is necessary, 0.5 is to add some border\n                -0.5, -1.5 / np.sqrt(3), self.size_x,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale - 1 / np.sqrt(3))\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.som is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        ngroups = int(self.selection is not None) and np.max(self.selection)\n        indices = np.zeros(len(self.data), dtype=int)\n        id_to_group = {}\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    group = self.selection[x][y]\n                    indices[rows] = group\n                    if group > 0:\n                        for id_ in self.data.ids[rows]:\n                            id_to_group[id_] = group\n\n        cont_domain = self._cont_domain(self.data)\n        shared_compute = SomSharedValueCompute(\n            cont_domain, self.som, self.offsets, self.scales)\n        cell = DiscreteVariable(\n            \"som_cell\",\n            values=tuple(\n                f\"r{row + 1}c{col + 1}\"\n                for row in range(self.size_y)\n                for col in range(self.size_x - (self.hexagonal and row % 2))),\n            compute_value=SomCellCompute(shared_compute,\n                                         self.size_x, self.hexagonal))\n        coordx = ContinuousVariable(\n            \"som_row\",\n            number_of_decimals=0,\n            compute_value=SomCoordsCompute(shared_compute, 1))\n        coordy = ContinuousVariable(\n            \"som_col\",\n            number_of_decimals=0,\n            compute_value=SomCoordsCompute(shared_compute, 0))\n        error = ContinuousVariable(\n            \"som_error\",\n            compute_value=SomErrorCompute(shared_compute)\n        )\n        som_attrs = (cell, coordx, coordy, error)\n\n        grp_values, _ = group_values(\n                indices, include_unselected=False, values=None)\n\n        def make_domain(values, default_grp, offset):\n            grp_var = DiscreteVariable(\n                get_unique_names(self.data.domain, ANNOTATED_DATA_FEATURE_NAME),\n                values,\n                compute_value=GetGroups(id_to_group, default_grp, offset))\n\n            if not self.data.domain.class_vars:\n                class_vars, metas = grp_var, som_attrs\n            else:\n                class_vars, metas = (), (grp_var,) + som_attrs\n            return add_columns(self.data.domain, (), class_vars, metas)\n\n        if np.any(indices):\n            sel_domain = make_domain(grp_values, np.nan, 1)\n            mask = np.flatnonzero(indices)\n            self.Outputs.selected_data.send(\n                lazy_table_transform(sel_domain, self.data[mask]))\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if ngroups > 1:\n            sel_domain = make_domain(grp_values + [\"Unselected\", ], ngroups, 1)\n        else:\n            sel_domain = make_domain((\"No\", \"Yes\"), 0, 0)\n        self.Outputs.annotated_data.send(\n            lazy_table_transform(sel_domain, self.data))\n\n    def set_color_bins(self):\n        self.Warning.no_defined_colors.clear()\n\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n            return\n\n        if self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n            return\n\n        col = self.data.get_column(self.attr_color)\n        col = col[np.isfinite(col)]\n        if not col.size:\n            self.Warning.no_defined_colors(self.attr_color)\n            self.thresholds = self.bin_labels = self.colors = None\n            return\n\n        if self.attr_color.is_time:\n            binning = time_binnings(col, min_bins=4)[-1]\n        else:\n            binning = decimal_binnings(col, min_bins=4)[-1]\n        self.thresholds = binning.thresholds[1:-1]\n        self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n        if not self.bin_labels[0] and binning.labels:\n            # Nan's are already filtered out, but it doesn't hurt much\n            # to use nanmax/nanmin\n            if np.nanmin(col) == np.nanmax(col):\n                # Handle a degenerate case with a single value\n                # Use the second threshold (because value must be smaller),\n                # but the first threshold as label (because that's the\n                # actual value in the data.\n                self.thresholds = binning.thresholds[1:]\n                self.bin_labels = (binning.labels[:1],\n                                   binning.short_labels[:1])\n        palette = BinnedContinuousPalette.from_palette(\n            self.attr_color.palette, binning.thresholds)\n        self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.colors is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels or ([], [])\n        if len(labels) <= 1:\n            return labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.colors:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")\n\n    @classmethod\n    def migrate_settings(cls, settings, _):\n        # previously selection was saved as np.ndarray which is not supported\n        # by widget-base, change selection to list\n        selection = settings.get('selection')\n        if selection is not None and isinstance(selection, np.ndarray):\n            settings['selection'] = selection.tolist()",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 298,
                                "end_lineno": 1141,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "# pylint: disable=protected-access\n\nimport unittest\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\nimport scipy.sparse as sp\nfrom AnyQt.QtWidgets import QComboBox, QPushButton, QCheckBox\nfrom AnyQt.QtCore import Qt\n\nfrom Orange.data import Table, Domain\nfrom Orange.widgets.tests.base import WidgetTest\nfrom Orange.widgets.tests.utils import simulate\nfrom Orange.widgets.utils.annotated_data import ANNOTATED_DATA_FEATURE_NAME\nfrom Orange.widgets.unsupervised.owsom import OWSOM, SomView, SOM, \\\n    SomSharedValueCompute, SomCellCompute, SomCoordsCompute, SomErrorCompute\n\n\ndef _patch_recompute_som(meth):\n    def winners_from_weights(cont_x, *_1, **_2):\n        n = cont_x.shape[0]\n        w = np.zeros((n, 2), dtype=int)\n        w[n // 5:] = [0, 1]\n        w[n // 3:] = [1, 2]\n        return w, np.arange(n) / n\n\n    def recompute(self):\n        if not self.data:\n            return\n\n        self.som = Mock()\n        self.som.winners = winners_from_weights\n        self._assign_instances(None, None)\n        self._redraw()\n        self.update_output()\n\n    def patched(*args, **kwargs):\n        with patch.object(SOM, \"winner_from_weights\", new=winners_from_weights), \\\n                patch.object(OWSOM, \"_recompute_som\", new=recompute):\n            meth(*args, **kwargs)\n\n    return patched\n\n\nclass TestOWSOM(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWSOM)  #: OWSOM\n        self.iris = Table(\"iris\")\n\n    def tearDown(self):\n        self.widget.onDeleteWidget()\n        super().tearDown()\n\n    def test_requires_continuous(self):\n        widget = self.widget\n        heart = Table(\"heart_disease\")\n\n        self.send_signal(widget.Inputs.data, Table(\"zoo\"))\n        self.assertTrue(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, heart)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(\n            widget.cont_x.shape[1],\n            sum(attr.is_continuous for attr in heart.domain.attributes))\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_single_attribute(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Warning.single_attribute.is_shown())\n        iris = self.iris[:, 0]\n        self.send_signal(widget.Inputs.data, iris)\n        self.assertTrue(widget.Warning.single_attribute.is_shown())\n\n    def test_missing_all_data(self):\n        widget = self.widget\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        with self.iris.unlocked():\n            for i in range(150):\n                self.iris.X[i, i % 4] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n    def test_missing_some_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:50, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n        np.testing.assert_almost_equal(\n            widget.data.Y.flatten(), [1] * 50 + [2] * 50)\n        self.assertEqual(widget.cont_x.shape, (100, 4))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_missing_one_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[5, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_run_actual_optimization(self):\n        # ther tests that compute something use _patch_recompute_som\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        out = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n\n    @_patch_recompute_som\n    def test_single_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:-1] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n\n    @_patch_recompute_som\n    def test_sparse_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X = sp.csc_matrix(self.iris.X)\n\n        # Table.from_table can decide to return dense data\n        with patch.object(Table, \"from_table\", lambda _, x: x):\n            # update_output would fail because of above patch\n            widget.update_output = Mock()\n            self.send_signal(widget.Inputs.data, self.iris)\n        self.assertIs(widget.data, self.iris)\n        self.assertTrue(sp.isspmatrix_csr(widget.cont_x))\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    @_patch_recompute_som\n    def test_auto_compute_dimensions(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        spin_x = widget.opt_controls.spin_x\n        spin_y = widget.opt_controls.spin_y\n\n        spin_x.setValue(100)\n        spin_y.setValue(100)\n        widget.auto_dimension = True\n        widget.controls.auto_dimension.toggled.emit(True)\n        self.assertLess(spin_x.value(), 100)\n        self.assertLess(spin_y.value(), 100)\n\n        spin_x.setValue(9)\n        spin_y.setValue(7)\n        widget.auto_dimension = False\n        widget.controls.auto_dimension.toggled.emit(False)\n        self.assertEqual(spin_x.value(), 10)\n        self.assertEqual(spin_y.value(), 5)\n\n    @_patch_recompute_som\n    def test_redraw_grid(self):\n        widget = self.widget\n\n        widget.auto_dimension = False\n\n        widget.opt_controls.spin_x.setValue(8)\n        widget.opt_controls.spin_y.setValue(5)\n        widget.opt_controls.shape.setCurrentIndex(1)\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 38)\n        self.assertIsNone(widget.grid_cells[1, 7])\n        self.assertIsNone(widget.grid_cells[3, 7])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n    def test_attr_color_change(self):\n        widget = self.widget\n        widget._redraw = Mock()\n\n        heart = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, heart)\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        combo.setCurrentIndex(0)\n        combo.activated[int].emit(0)\n        self.assertFalse(widget.controls.pie_charts.isEnabled())\n        self.assertIsNone(widget.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        gender = heart.domain[\"gender\"]\n        ind_gen = combo.model().indexOf(gender)\n        combo.setCurrentIndex(ind_gen)\n        combo.activated[int].emit(ind_gen)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        np.testing.assert_equal(widget.colors.palette, gender.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        age = heart.domain[\"age\"]\n        ind_age = combo.model().indexOf(age)\n        combo.setCurrentIndex(ind_age)\n        combo.activated[int].emit(ind_age)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        self.assertIsNotNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n    def test_colored_circles_with_constant(self):\n        domain = self.iris.domain\n        self.widget.pie_charts = False\n\n        with self.iris.unlocked():\n            self.iris.X[:, 0] = 1\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        attr0 = domain.attributes[0]\n\n        combo = self.widget.controls.attr_color\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNotNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n        dom1 = Domain(domain.attributes[1:], domain.class_var,\n                      domain.attributes[:1])\n        iris = self.iris.transform(dom1).copy()\n        with iris.unlocked(iris.metas):\n            iris.metas[::2, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, iris)\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNotNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n        iris = self.iris.transform(dom1).copy()\n        with iris.unlocked(iris.metas):\n            iris.metas[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, iris)\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNone(self.widget.colors)\n        self.assertTrue(self.widget.Warning.no_defined_colors.is_shown())\n\n        simulate.combobox_activate_index(combo, 0)\n        self.assertIsNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n    @_patch_recompute_som\n    def test_cell_sizes(self):\n        widget = self.widget\n        widget._draw_same_color = widget._draw_pie_charts = \\\n            widget._draw_colored_circles = draw = Mock()\n\n        widget.size_by_instances = False\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.size_by_instances = True\n        widget.pie_charts = False\n        widget.controls.size_by_instances.toggled.emit(True)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8 * (30 / 100))\n        self.assertAlmostEqual(sizes[0, 1], 0.8 * (20 / 100))\n        self.assertAlmostEqual(sizes[1, 2], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n        widget.size_by_instances = False\n        widget.controls.size_by_instances.toggled.emit(False)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n    @_patch_recompute_som\n    def test_same_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_same_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n        widget.opt_controls.shape.setCurrentIndex(0)\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * (30 / 100)),\n                         (0.5, np.sqrt(3) / 2, 0.4 * (20 / 100)),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0.5, np.sqrt(3) / 2, 0.4 * 20 / 100),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_pie_charts(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.pie_charts = True\n        widget.controls.pie_charts.toggled.emit(True)\n\n        a, b, c = widget.elements.childItems()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0.5, np.sqrt(3) / 2, 0.8 * 20 / 100),\n                                 (1, np.sqrt(3), 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n            self.assertEqual(len(e.colors), len(widget.colors) + 1)\n        np.testing.assert_equal(a.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(b.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(c.dist, [0, 0.5, 0.5, 0])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0, 1, 0.8 * 20 / 100),\n                                 (1, 2, 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n\n        with self.iris.unlocked():\n            self.iris.Y[:15] = np.nan\n        self.send_signal(widget.Inputs.data, self.iris)\n        a = widget.elements.childItems()[0]\n        np.testing.assert_equal(a.dist, [0.5, 0, 0, 0.5])\n\n    @_patch_recompute_som\n    def test_get_color_column(self):\n        widget = self.widget\n\n        table = Table(\"heart_disease\")\n        domain = table.domain\n        new_domain = Domain(\n            domain.attributes[3:], domain.class_var, domain.attributes[:3])\n        new_table = table.transform(new_domain).copy()\n        with new_table.unlocked(new_table.metas):\n            new_table.metas = new_table.metas.astype(object)\n        self.send_signal(widget.Inputs.data, new_table)\n\n        # discrete attribute\n        widget.attr_color = domain[\"rest ECG\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column(\"rest ECG\").astype(int))\n\n        # discrete meta\n        widget.attr_color = domain[\"gender\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column(\"gender\").astype(int))\n\n        # numeric attribute\n        widget.thresholds = np.array([120, 150])\n        widget.attr_color = domain[\"max HR\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column(\"max HR\")):\n            if d < 120:\n                self.assertEqual(c, 0)\n            if 120 <= d < 150:\n                self.assertEqual(c, 1)\n            if d >= 150:\n                self.assertEqual(c, 2)\n\n        # numeric meta\n        widget.thresholds = np.array([50, 60])\n        widget.attr_color = domain[\"age\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column(\"age\")):\n            if d < 50:\n                self.assertEqual(c, 0)\n            if 50 <= d < 60:\n                self.assertEqual(c, 1)\n            if d >= 60:\n                self.assertEqual(c, 2)\n\n        # discrete meta with missing values\n        widget.attr_color = domain[\"gender\"]\n        with widget.data.unlocked():\n            col = widget.data.get_column(\"gender\", copy=True)\n            widget.data.metas[:5, 1] = np.nan  # gender\n        col[:5] = 2\n        np.testing.assert_equal(widget._get_color_column(), col)\n\n    @_patch_recompute_som\n    def test_colored_circles_with_missing_values(self):\n        with self.iris.unlocked():\n            self.iris.Y[:6] = np.nan\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertTrue(self.widget.Warning.missing_colors.is_shown())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_colors.is_shown())\n\n    def test_send_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n    @_patch_recompute_som\n    def test_on_selection_change(self):\n        def selm(*cells):\n            m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n            for x, y in cells:\n                m[x, y] = True\n            return m\n\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        widget.redraw_selection = Mock()\n        widget.update_output = Mock()\n\n        m = selm((0, 0)).astype(int)\n        widget.on_selection_change(selm((0, 0)))\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n        widget.redraw_selection.assert_called_once()\n        widget.update_output.assert_called_once()\n\n        m = selm((0, 1)).astype(int)\n        widget.on_selection_change(selm((0, 1)))\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 1\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionAddToGroup)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 0\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionRemove)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 2\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionNewGroup)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n    @_patch_recompute_som\n    def test_on_selection_change_on_empty(self):\n        \"\"\"Test clicks on empty scene, when no data\"\"\"\n        widget = self.widget\n        widget.on_selection_change([])\n\n    @_patch_recompute_som\n    def test_output_selection(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        out = self.get_output(widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n        self.assertTrue(\n            np.all(out.get_column(ANNOTATED_DATA_FEATURE_NAME) == 0))\n\n        self.widget.cells = np.array(\n            [[[0, 30], [30, 50]] + [[50, 50]] * 6,\n             [[50, 50], [50, 50], [50, 150]] + [[150, 150]] * 5]\n            + [[[150, 150]] * 8] * 6)\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        widget.on_selection_change(m)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:30])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column(ANNOTATED_DATA_FEATURE_NAME),\n            [1] * 30 + [0] * 120)\n\n        m[0, 0] = False\n        m[0, 1] = True\n        widget.on_selection_change(m, SomView.SelectionNewGroup)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:50])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column(ANNOTATED_DATA_FEATURE_NAME),\n            [0] * 30 + [1] * 20 + [2] * 100)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))\n\n    @_patch_recompute_som\n    def test_output_columns(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        m[1, 2] = True\n        widget.on_selection_change(m)\n\n        out = self.get_output(widget.Outputs.annotated_data)\n\n        np.testing.assert_equal(out.get_column(\"som_row\"),\n                                [1] * 30 + [2] * 20 + [3] * 100)\n        np.testing.assert_equal(out.get_column(\"som_col\"),\n                                [1] * 50 + [2] * 100)\n        cell_var = out.domain[\"som_cell\"]\n        np.testing.assert_equal([cell_var.repr_val(v)\n                                 for v in out.get_column(\"som_cell\")],\n                                [\"r1c1\"] * 30 + [\"r2c1\"] * 20 + [\"r3c2\"] * 100)\n        np.testing.assert_equal(out.get_column(\"som_error\"),\n                                np.arange(150) / 150)\n\n    def test_invalidated(self):\n        heart = Table(\"heart_disease\")\n        self.widget._recompute_som = Mock()\n\n        # New data - replot\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_called_once()\n\n        # Same data - no replot\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_not_called()\n\n        # Same data.X - no replot\n        domain = heart.domain\n        domain = Domain(domain.attributes, metas=domain.class_vars)\n        heart_with_metas = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_metas)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data, same set of cont. vars - no replot\n        attrs = [a for a in heart.domain.attributes if a.is_continuous]\n        domain = Domain(attrs)\n        heart_with_cont_features = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_cont_features)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data.X - replot\n        domain = Domain(heart.domain.attributes[:5])\n        heart_with_less_features = heart.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_less_features)\n        self.widget._recompute_som.assert_called_once()\n\n    def test_modified_info(self):\n        w = self.widget\n        self.assertFalse(w.Information.modified.is_shown())\n        self.send_signal(w.Inputs.data, self.iris)\n        self.assertFalse(w.Information.modified.is_shown())\n        restart_button = w.controlArea.findChild(QPushButton)\n\n        # modify grid\n        simulate.combobox_activate_index(w.controlArea.findChild(QComboBox), 1)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify set dimensions automatically\n        w.controlArea.findChild(QCheckBox).setCheckState(Qt.Unchecked)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify dimension spins\n        w.spin_x.setValue(7)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        w.spin_y.setValue(7)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify initialization\n        simulate.combobox_activate_index(w.controlArea.findChildren(QComboBox)[1], 1)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n\nclass TestComputeValues(unittest.TestCase):\n    def test_eq_hash(self):\n        def equ(obj2):\n            self.assertEqual(obj1, obj2)\n            self.assertEqual(hash(obj1), hash(obj2))\n\n        def neq(obj2):\n            self.assertNotEqual(obj1, obj2)\n            self.assertNotEqual(hash(obj1), hash(obj2))\n\n        som1 = Mock()\n        som2 = Mock()\n        domain1 = Table(\"iris\").domain\n        domain2 = Table(\"iris\")[:, :4].domain\n        assert domain1 != domain2\n        offsets1, scales1 = np.array([1, 2, 3]), np.array([4, 5, 6])\n        offsets2, scales2 = np.array([2, 3, 4]), np.array([5, 6, 7])\n\n        shared1 = SomSharedValueCompute(domain1, som1, offsets1, scales1)\n        shared2 = SomSharedValueCompute(domain2, som1, offsets1, scales1)\n\n        obj1 = shared1\n        equ(SomSharedValueCompute(domain1, som1, offsets1, scales1))\n        neq(shared2)\n        neq(SomSharedValueCompute(domain1, som2, offsets1, scales1))\n        neq(SomSharedValueCompute(domain1, som1, offsets2, scales1))\n        neq(SomSharedValueCompute(domain1, som1, offsets1, scales2))\n\n        obj1 = SomCellCompute(shared1, 8, False)\n        equ(SomCellCompute(shared1, 8, False))\n        neq(SomCellCompute(shared2, 8, False))\n        neq(SomCellCompute(shared1, 5, False))\n        neq(SomCellCompute(shared1, 8, True))\n\n        obj1 = SomCoordsCompute(shared1, 0)\n        equ(SomCoordsCompute(shared1, 0))\n        neq(SomCoordsCompute(shared2, 0))\n        neq(SomCoordsCompute(shared1, 1))\n\n        obj1 = SomErrorCompute(shared1)\n        equ(SomErrorCompute(shared1))\n        neq(SomErrorCompute(shared2))\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "new_file_content": "# pylint: disable=protected-access\n\nimport unittest\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\nimport scipy.sparse as sp\nfrom AnyQt.QtWidgets import QComboBox, QPushButton, QCheckBox\nfrom AnyQt.QtCore import Qt\n\nfrom Orange.data import Table, Domain\nfrom Orange.widgets.tests.base import WidgetTest\nfrom Orange.widgets.tests.utils import simulate\nfrom Orange.widgets.utils.annotated_data import ANNOTATED_DATA_FEATURE_NAME\nfrom Orange.widgets.unsupervised.owsom import OWSOM, SomView, SOM, \\\n    SomSharedValueCompute, SomCellCompute, SomCoordsCompute, SomErrorCompute\n\n\ndef _patch_recompute_som(meth):\n    def winners_from_weights(cont_x, *_1, **_2):\n        n = cont_x.shape[0]\n        w = np.zeros((n, 2), dtype=int)\n        w[n // 5:] = [0, 1]\n        w[n // 3:] = [1, 2]\n        return w, np.arange(n) / n\n\n    def recompute(self):\n        if not self.data:\n            return\n\n        self.som = Mock()\n        self.som.winners = winners_from_weights\n        self._assign_instances(None, None)\n        self._redraw()\n        self.update_output()\n\n    def patched(*args, **kwargs):\n        with patch.object(SOM, \"winner_from_weights\", new=winners_from_weights), \\\n                patch.object(OWSOM, \"_recompute_som\", new=recompute):\n            meth(*args, **kwargs)\n\n    return patched\n\n\nclass TestOWSOM(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWSOM)  #: OWSOM\n        self.iris = Table(\"iris\")\n\n    def tearDown(self):\n        self.widget.onDeleteWidget()\n        super().tearDown()\n\n    def test_requires_continuous(self):\n        widget = self.widget\n        heart = Table(\"heart_disease\")\n\n        self.send_signal(widget.Inputs.data, Table(\"zoo\"))\n        self.assertTrue(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, heart)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(\n            widget.cont_x.shape[1],\n            sum(attr.is_continuous for attr in heart.domain.attributes))\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_single_attribute(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Warning.single_attribute.is_shown())\n        iris = self.iris[:, 0]\n        self.send_signal(widget.Inputs.data, iris)\n        self.assertTrue(widget.Warning.single_attribute.is_shown())\n\n    def test_missing_all_data(self):\n        widget = self.widget\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        with self.iris.unlocked():\n            for i in range(150):\n                self.iris.X[i, i % 4] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n    def test_missing_some_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:50, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n        np.testing.assert_almost_equal(\n            widget.data.Y.flatten(), [1] * 50 + [2] * 50)\n        self.assertEqual(widget.cont_x.shape, (100, 4))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_missing_one_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[5, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_run_actual_optimization(self):\n        # ther tests that compute something use _patch_recompute_som\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        out = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n\n    @_patch_recompute_som\n    def test_single_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:-1] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n\n    @_patch_recompute_som\n    def test_sparse_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X = sp.csc_matrix(self.iris.X)\n\n        # Table.from_table can decide to return dense data\n        with patch.object(Table, \"from_table\", lambda _, x: x):\n            # update_output would fail because of above patch\n            widget.update_output = Mock()\n            self.send_signal(widget.Inputs.data, self.iris)\n        self.assertIs(widget.data, self.iris)\n        self.assertTrue(sp.isspmatrix_csr(widget.cont_x))\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    @_patch_recompute_som\n    def test_auto_compute_dimensions(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        spin_x = widget.opt_controls.spin_x\n        spin_y = widget.opt_controls.spin_y\n\n        spin_x.setValue(100)\n        spin_y.setValue(100)\n        widget.auto_dimension = True\n        widget.controls.auto_dimension.toggled.emit(True)\n        self.assertLess(spin_x.value(), 100)\n        self.assertLess(spin_y.value(), 100)\n\n        spin_x.setValue(9)\n        spin_y.setValue(7)\n        widget.auto_dimension = False\n        widget.controls.auto_dimension.toggled.emit(False)\n        self.assertEqual(spin_x.value(), 10)\n        self.assertEqual(spin_y.value(), 5)\n\n    @_patch_recompute_som\n    def test_redraw_grid(self):\n        widget = self.widget\n\n        widget.auto_dimension = False\n\n        widget.opt_controls.spin_x.setValue(8)\n        widget.opt_controls.spin_y.setValue(5)\n        widget.opt_controls.shape.setCurrentIndex(1)\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 38)\n        self.assertIsNone(widget.grid_cells[1, 7])\n        self.assertIsNone(widget.grid_cells[3, 7])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n    def test_attr_color_change(self):\n        widget = self.widget\n        widget._redraw = Mock()\n\n        heart = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, heart)\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        combo.setCurrentIndex(0)\n        combo.activated[int].emit(0)\n        self.assertFalse(widget.controls.pie_charts.isEnabled())\n        self.assertIsNone(widget.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        gender = heart.domain[\"gender\"]\n        ind_gen = combo.model().indexOf(gender)\n        combo.setCurrentIndex(ind_gen)\n        combo.activated[int].emit(ind_gen)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        np.testing.assert_equal(widget.colors.palette, gender.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        age = heart.domain[\"age\"]\n        ind_age = combo.model().indexOf(age)\n        combo.setCurrentIndex(ind_age)\n        combo.activated[int].emit(ind_age)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        self.assertIsNotNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n    def test_colored_circles_with_constant(self):\n        domain = self.iris.domain\n        self.widget.pie_charts = False\n\n        with self.iris.unlocked():\n            self.iris.X[:, 0] = 1\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        attr0 = domain.attributes[0]\n\n        combo = self.widget.controls.attr_color\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNotNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n        dom1 = Domain(domain.attributes[1:], domain.class_var,\n                      domain.attributes[:1])\n        iris = self.iris.transform(dom1).copy()\n        with iris.unlocked(iris.metas):\n            iris.metas[::2, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, iris)\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNotNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n        iris = self.iris.transform(dom1).copy()\n        with iris.unlocked(iris.metas):\n            iris.metas[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, iris)\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNone(self.widget.colors)\n        self.assertTrue(self.widget.Warning.no_defined_colors.is_shown())\n\n        simulate.combobox_activate_index(combo, 0)\n        self.assertIsNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n    @_patch_recompute_som\n    def test_cell_sizes(self):\n        widget = self.widget\n        widget._draw_same_color = widget._draw_pie_charts = \\\n            widget._draw_colored_circles = draw = Mock()\n\n        widget.size_by_instances = False\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.size_by_instances = True\n        widget.pie_charts = False\n        widget.controls.size_by_instances.toggled.emit(True)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8 * (30 / 100))\n        self.assertAlmostEqual(sizes[0, 1], 0.8 * (20 / 100))\n        self.assertAlmostEqual(sizes[1, 2], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n        widget.size_by_instances = False\n        widget.controls.size_by_instances.toggled.emit(False)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n    @_patch_recompute_som\n    def test_same_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_same_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n        widget.opt_controls.shape.setCurrentIndex(0)\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * (30 / 100)),\n                         (0.5, np.sqrt(3) / 2, 0.4 * (20 / 100)),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0.5, np.sqrt(3) / 2, 0.4 * 20 / 100),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_pie_charts(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.pie_charts = True\n        widget.controls.pie_charts.toggled.emit(True)\n\n        a, b, c = widget.elements.childItems()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0.5, np.sqrt(3) / 2, 0.8 * 20 / 100),\n                                 (1, np.sqrt(3), 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n            self.assertEqual(len(e.colors), len(widget.colors) + 1)\n        np.testing.assert_equal(a.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(b.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(c.dist, [0, 0.5, 0.5, 0])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0, 1, 0.8 * 20 / 100),\n                                 (1, 2, 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n\n        with self.iris.unlocked():\n            self.iris.Y[:15] = np.nan\n        self.send_signal(widget.Inputs.data, self.iris)\n        a = widget.elements.childItems()[0]\n        np.testing.assert_equal(a.dist, [0.5, 0, 0, 0.5])\n\n    @_patch_recompute_som\n    def test_get_color_column(self):\n        widget = self.widget\n\n        table = Table(\"heart_disease\")\n        domain = table.domain\n        new_domain = Domain(\n            domain.attributes[3:], domain.class_var, domain.attributes[:3])\n        new_table = table.transform(new_domain).copy()\n        with new_table.unlocked(new_table.metas):\n            new_table.metas = new_table.metas.astype(object)\n        self.send_signal(widget.Inputs.data, new_table)\n\n        # discrete attribute\n        widget.attr_color = domain[\"rest ECG\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column(\"rest ECG\").astype(int))\n\n        # discrete meta\n        widget.attr_color = domain[\"gender\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column(\"gender\").astype(int))\n\n        # numeric attribute\n        widget.thresholds = np.array([120, 150])\n        widget.attr_color = domain[\"max HR\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column(\"max HR\")):\n            if d < 120:\n                self.assertEqual(c, 0)\n            if 120 <= d < 150:\n                self.assertEqual(c, 1)\n            if d >= 150:\n                self.assertEqual(c, 2)\n\n        # numeric meta\n        widget.thresholds = np.array([50, 60])\n        widget.attr_color = domain[\"age\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column(\"age\")):\n            if d < 50:\n                self.assertEqual(c, 0)\n            if 50 <= d < 60:\n                self.assertEqual(c, 1)\n            if d >= 60:\n                self.assertEqual(c, 2)\n\n        # discrete meta with missing values\n        widget.attr_color = domain[\"gender\"]\n        with widget.data.unlocked():\n            col = widget.data.get_column(\"gender\", copy=True)\n            widget.data.metas[:5, 1] = np.nan  # gender\n        col[:5] = 2\n        np.testing.assert_equal(widget._get_color_column(), col)\n\n    @_patch_recompute_som\n    def test_colored_circles_with_missing_values(self):\n        with self.iris.unlocked():\n            self.iris.Y[:6] = np.nan\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertTrue(self.widget.Warning.missing_colors.is_shown())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_colors.is_shown())\n\n    def test_send_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n    @_patch_recompute_som\n    def test_on_selection_change(self):\n        def selm(*cells):\n            m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n            for x, y in cells:\n                m[x, y] = True\n            return m\n\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        widget.redraw_selection = Mock()\n        widget.update_output = Mock()\n\n        m = selm((0, 0)).astype(int)\n        widget.on_selection_change(selm((0, 0)))\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n        widget.redraw_selection.assert_called_once()\n        widget.update_output.assert_called_once()\n\n        m = selm((0, 1)).astype(int)\n        widget.on_selection_change(selm((0, 1)))\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 1\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionAddToGroup)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 0\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionRemove)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 2\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionNewGroup)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n    @_patch_recompute_som\n    def test_on_selection_change_on_empty(self):\n        \"\"\"Test clicks on empty scene, when no data\"\"\"\n        widget = self.widget\n        widget.on_selection_change([])\n\n    @_patch_recompute_som\n    def test_output_selection(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        out = self.get_output(widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n        self.assertTrue(\n            np.all(out.get_column(ANNOTATED_DATA_FEATURE_NAME) == 0))\n\n        self.widget.cells = np.array(\n            [[[0, 30], [30, 50]] + [[50, 50]] * 6,\n             [[50, 50], [50, 50], [50, 150]] + [[150, 150]] * 5]\n            + [[[150, 150]] * 8] * 6)\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        widget.on_selection_change(m)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:30])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column(ANNOTATED_DATA_FEATURE_NAME),\n            [1] * 30 + [0] * 120)\n\n        m[0, 0] = False\n        m[0, 1] = True\n        widget.on_selection_change(m, SomView.SelectionNewGroup)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:50])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column(ANNOTATED_DATA_FEATURE_NAME),\n            [0] * 30 + [1] * 20 + [2] * 100)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))\n\n    @_patch_recompute_som\n    def test_output_columns(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        m[1, 2] = True\n        widget.on_selection_change(m)\n\n        out = self.get_output(widget.Outputs.annotated_data)\n\n        np.testing.assert_equal(out.get_column(\"som_row\"),\n                                [1] * 30 + [2] * 20 + [3] * 100)\n        np.testing.assert_equal(out.get_column(\"som_col\"),\n                                [1] * 50 + [2] * 100)\n        cell_var = out.domain[\"som_cell\"]\n        np.testing.assert_equal([cell_var.repr_val(v)\n                                 for v in out.get_column(\"som_cell\")],\n                                [\"r1c1\"] * 30 + [\"r2c1\"] * 20 + [\"r3c2\"] * 100)\n        np.testing.assert_equal(out.get_column(\"som_error\"),\n                                np.arange(150) / 150)\n\n    def test_invalidated(self):\n        heart = Table(\"heart_disease\")\n        self.widget._recompute_som = Mock()\n\n        # New data - replot\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_called_once()\n\n        # Same data - no replot\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_not_called()\n\n        # Same data.X - no replot\n        domain = heart.domain\n        domain = Domain(domain.attributes, metas=domain.class_vars)\n        heart_with_metas = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_metas)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data, same set of cont. vars - no replot\n        attrs = [a for a in heart.domain.attributes if a.is_continuous]\n        domain = Domain(attrs)\n        heart_with_cont_features = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_cont_features)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data.X - replot\n        domain = Domain(heart.domain.attributes[:5])\n        heart_with_less_features = heart.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_less_features)\n        self.widget._recompute_som.assert_called_once()\n\n    def test_modified_info(self):\n        w = self.widget\n        self.assertFalse(w.Information.modified.is_shown())\n        self.send_signal(w.Inputs.data, self.iris)\n        self.assertFalse(w.Information.modified.is_shown())\n        restart_button = w.controlArea.findChild(QPushButton)\n\n        # modify grid\n        simulate.combobox_activate_index(w.controlArea.findChild(QComboBox), 1)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify set dimensions automatically\n        w.controlArea.findChild(QCheckBox).setCheckState(Qt.Unchecked)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify dimension spins\n        w.spin_x.setValue(7)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        w.spin_y.setValue(7)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify initialization\n        simulate.combobox_activate_index(w.controlArea.findChildren(QComboBox)[1], 1)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n    def test_make_domain_without_class_vars(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        widget.data.domain.class_vars = None\n        widget.update_output()\n        self.assertIsNotNone(self.get_output(widget.Outputs.annotated_data))\n\n\nclass TestComputeValues(unittest.TestCase):\n    def test_eq_hash(self):\n        def equ(obj2):\n            self.assertEqual(obj1, obj2)\n            self.assertEqual(hash(obj1), hash(obj2))\n\n        def neq(obj2):\n            self.assertNotEqual(obj1, obj2)\n            self.assertNotEqual(hash(obj1), hash(obj2))\n\n        som1 = Mock()\n        som2 = Mock()\n        domain1 = Table(\"iris\").domain\n        domain2 = Table(\"iris\")[:, :4].domain\n        assert domain1 != domain2\n        offsets1, scales1 = np.array([1, 2, 3]), np.array([4, 5, 6])\n        offsets2, scales2 = np.array([2, 3, 4]), np.array([5, 6, 7])\n\n        shared1 = SomSharedValueCompute(domain1, som1, offsets1, scales1)\n        shared2 = SomSharedValueCompute(domain2, som1, offsets1, scales1)\n\n        obj1 = shared1\n        equ(SomSharedValueCompute(domain1, som1, offsets1, scales1))\n        neq(shared2)\n        neq(SomSharedValueCompute(domain1, som2, offsets1, scales1))\n        neq(SomSharedValueCompute(domain1, som1, offsets2, scales1))\n        neq(SomSharedValueCompute(domain1, som1, offsets1, scales2))\n\n        obj1 = SomCellCompute(shared1, 8, False)\n        equ(SomCellCompute(shared1, 8, False))\n        neq(SomCellCompute(shared2, 8, False))\n        neq(SomCellCompute(shared1, 5, False))\n        neq(SomCellCompute(shared1, 8, True))\n\n        obj1 = SomCoordsCompute(shared1, 0)\n        equ(SomCoordsCompute(shared1, 0))\n        neq(SomCoordsCompute(shared2, 0))\n        neq(SomCoordsCompute(shared1, 1))\n\n        obj1 = SomErrorCompute(shared1)\n        equ(SomErrorCompute(shared1))\n        neq(SomErrorCompute(shared2))\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/unsupervised/tests/test_owsom.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "ea7350d46",
                "new_commit_hash": "142c9b0c9",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/unsupervised/tests/test_owsom.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/unsupervised/tests/test_owsom.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 723,
                            "length": 6
                        },
                        "new_range": {
                            "start": 723,
                            "length": 13
                        },
                        "section": "class TestOWSOM(WidgetTest):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        restart_button.click()",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertFalse(w.Information.modified.is_shown())",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def test_make_domain_without_class_vars(self):",
                                "type": "added"
                            },
                            {
                                "content": "        widget = self.widget",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.data, self.iris)",
                                "type": "added"
                            },
                            {
                                "content": "        widget.data.domain.class_vars = None",
                                "type": "added"
                            },
                            {
                                "content": "        widget.update_output()",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertIsNotNone(self.get_output(widget.Outputs.annotated_data))",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "class TestComputeValues(unittest.TestCase):",
                                "type": "context"
                            },
                            {
                                "content": "    def test_eq_hash(self):",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/tests/test_owsom.py",
                            "type": "class",
                            "name": "TestOWSOM",
                            "content": "class TestOWSOM(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWSOM)  #: OWSOM\n        self.iris = Table(\"iris\")\n\n    def tearDown(self):\n        self.widget.onDeleteWidget()\n        super().tearDown()\n\n    def test_requires_continuous(self):\n        widget = self.widget\n        heart = Table(\"heart_disease\")\n\n        self.send_signal(widget.Inputs.data, Table(\"zoo\"))\n        self.assertTrue(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, heart)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(\n            widget.cont_x.shape[1],\n            sum(attr.is_continuous for attr in heart.domain.attributes))\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_single_attribute(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Warning.single_attribute.is_shown())\n        iris = self.iris[:, 0]\n        self.send_signal(widget.Inputs.data, iris)\n        self.assertTrue(widget.Warning.single_attribute.is_shown())\n\n    def test_missing_all_data(self):\n        widget = self.widget\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        with self.iris.unlocked():\n            for i in range(150):\n                self.iris.X[i, i % 4] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n    def test_missing_some_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:50, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n        np.testing.assert_almost_equal(\n            widget.data.Y.flatten(), [1] * 50 + [2] * 50)\n        self.assertEqual(widget.cont_x.shape, (100, 4))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_missing_one_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[5, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_run_actual_optimization(self):\n        # ther tests that compute something use _patch_recompute_som\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        out = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n\n    @_patch_recompute_som\n    def test_single_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:-1] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n\n    @_patch_recompute_som\n    def test_sparse_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X = sp.csc_matrix(self.iris.X)\n\n        # Table.from_table can decide to return dense data\n        with patch.object(Table, \"from_table\", lambda _, x: x):\n            # update_output would fail because of above patch\n            widget.update_output = Mock()\n            self.send_signal(widget.Inputs.data, self.iris)\n        self.assertIs(widget.data, self.iris)\n        self.assertTrue(sp.isspmatrix_csr(widget.cont_x))\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    @_patch_recompute_som\n    def test_auto_compute_dimensions(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        spin_x = widget.opt_controls.spin_x\n        spin_y = widget.opt_controls.spin_y\n\n        spin_x.setValue(100)\n        spin_y.setValue(100)\n        widget.auto_dimension = True\n        widget.controls.auto_dimension.toggled.emit(True)\n        self.assertLess(spin_x.value(), 100)\n        self.assertLess(spin_y.value(), 100)\n\n        spin_x.setValue(9)\n        spin_y.setValue(7)\n        widget.auto_dimension = False\n        widget.controls.auto_dimension.toggled.emit(False)\n        self.assertEqual(spin_x.value(), 10)\n        self.assertEqual(spin_y.value(), 5)\n\n    @_patch_recompute_som\n    def test_redraw_grid(self):\n        widget = self.widget\n\n        widget.auto_dimension = False\n\n        widget.opt_controls.spin_x.setValue(8)\n        widget.opt_controls.spin_y.setValue(5)\n        widget.opt_controls.shape.setCurrentIndex(1)\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 38)\n        self.assertIsNone(widget.grid_cells[1, 7])\n        self.assertIsNone(widget.grid_cells[3, 7])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n    def test_attr_color_change(self):\n        widget = self.widget\n        widget._redraw = Mock()\n\n        heart = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, heart)\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        combo.setCurrentIndex(0)\n        combo.activated[int].emit(0)\n        self.assertFalse(widget.controls.pie_charts.isEnabled())\n        self.assertIsNone(widget.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        gender = heart.domain[\"gender\"]\n        ind_gen = combo.model().indexOf(gender)\n        combo.setCurrentIndex(ind_gen)\n        combo.activated[int].emit(ind_gen)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        np.testing.assert_equal(widget.colors.palette, gender.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        age = heart.domain[\"age\"]\n        ind_age = combo.model().indexOf(age)\n        combo.setCurrentIndex(ind_age)\n        combo.activated[int].emit(ind_age)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        self.assertIsNotNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n    def test_colored_circles_with_constant(self):\n        domain = self.iris.domain\n        self.widget.pie_charts = False\n\n        with self.iris.unlocked():\n            self.iris.X[:, 0] = 1\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        attr0 = domain.attributes[0]\n\n        combo = self.widget.controls.attr_color\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNotNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n        dom1 = Domain(domain.attributes[1:], domain.class_var,\n                      domain.attributes[:1])\n        iris = self.iris.transform(dom1).copy()\n        with iris.unlocked(iris.metas):\n            iris.metas[::2, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, iris)\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNotNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n        iris = self.iris.transform(dom1).copy()\n        with iris.unlocked(iris.metas):\n            iris.metas[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, iris)\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNone(self.widget.colors)\n        self.assertTrue(self.widget.Warning.no_defined_colors.is_shown())\n\n        simulate.combobox_activate_index(combo, 0)\n        self.assertIsNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n    @_patch_recompute_som\n    def test_cell_sizes(self):\n        widget = self.widget\n        widget._draw_same_color = widget._draw_pie_charts = \\\n            widget._draw_colored_circles = draw = Mock()\n\n        widget.size_by_instances = False\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.size_by_instances = True\n        widget.pie_charts = False\n        widget.controls.size_by_instances.toggled.emit(True)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8 * (30 / 100))\n        self.assertAlmostEqual(sizes[0, 1], 0.8 * (20 / 100))\n        self.assertAlmostEqual(sizes[1, 2], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n        widget.size_by_instances = False\n        widget.controls.size_by_instances.toggled.emit(False)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n    @_patch_recompute_som\n    def test_same_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_same_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n        widget.opt_controls.shape.setCurrentIndex(0)\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * (30 / 100)),\n                         (0.5, np.sqrt(3) / 2, 0.4 * (20 / 100)),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0.5, np.sqrt(3) / 2, 0.4 * 20 / 100),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_pie_charts(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.pie_charts = True\n        widget.controls.pie_charts.toggled.emit(True)\n\n        a, b, c = widget.elements.childItems()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0.5, np.sqrt(3) / 2, 0.8 * 20 / 100),\n                                 (1, np.sqrt(3), 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n            self.assertEqual(len(e.colors), len(widget.colors) + 1)\n        np.testing.assert_equal(a.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(b.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(c.dist, [0, 0.5, 0.5, 0])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0, 1, 0.8 * 20 / 100),\n                                 (1, 2, 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n\n        with self.iris.unlocked():\n            self.iris.Y[:15] = np.nan\n        self.send_signal(widget.Inputs.data, self.iris)\n        a = widget.elements.childItems()[0]\n        np.testing.assert_equal(a.dist, [0.5, 0, 0, 0.5])\n\n    @_patch_recompute_som\n    def test_get_color_column(self):\n        widget = self.widget\n\n        table = Table(\"heart_disease\")\n        domain = table.domain\n        new_domain = Domain(\n            domain.attributes[3:], domain.class_var, domain.attributes[:3])\n        new_table = table.transform(new_domain).copy()\n        with new_table.unlocked(new_table.metas):\n            new_table.metas = new_table.metas.astype(object)\n        self.send_signal(widget.Inputs.data, new_table)\n\n        # discrete attribute\n        widget.attr_color = domain[\"rest ECG\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column(\"rest ECG\").astype(int))\n\n        # discrete meta\n        widget.attr_color = domain[\"gender\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column(\"gender\").astype(int))\n\n        # numeric attribute\n        widget.thresholds = np.array([120, 150])\n        widget.attr_color = domain[\"max HR\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column(\"max HR\")):\n            if d < 120:\n                self.assertEqual(c, 0)\n            if 120 <= d < 150:\n                self.assertEqual(c, 1)\n            if d >= 150:\n                self.assertEqual(c, 2)\n\n        # numeric meta\n        widget.thresholds = np.array([50, 60])\n        widget.attr_color = domain[\"age\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column(\"age\")):\n            if d < 50:\n                self.assertEqual(c, 0)\n            if 50 <= d < 60:\n                self.assertEqual(c, 1)\n            if d >= 60:\n                self.assertEqual(c, 2)\n\n        # discrete meta with missing values\n        widget.attr_color = domain[\"gender\"]\n        with widget.data.unlocked():\n            col = widget.data.get_column(\"gender\", copy=True)\n            widget.data.metas[:5, 1] = np.nan  # gender\n        col[:5] = 2\n        np.testing.assert_equal(widget._get_color_column(), col)\n\n    @_patch_recompute_som\n    def test_colored_circles_with_missing_values(self):\n        with self.iris.unlocked():\n            self.iris.Y[:6] = np.nan\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertTrue(self.widget.Warning.missing_colors.is_shown())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_colors.is_shown())\n\n    def test_send_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n    @_patch_recompute_som\n    def test_on_selection_change(self):\n        def selm(*cells):\n            m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n            for x, y in cells:\n                m[x, y] = True\n            return m\n\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        widget.redraw_selection = Mock()\n        widget.update_output = Mock()\n\n        m = selm((0, 0)).astype(int)\n        widget.on_selection_change(selm((0, 0)))\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n        widget.redraw_selection.assert_called_once()\n        widget.update_output.assert_called_once()\n\n        m = selm((0, 1)).astype(int)\n        widget.on_selection_change(selm((0, 1)))\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 1\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionAddToGroup)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 0\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionRemove)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 2\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionNewGroup)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n    @_patch_recompute_som\n    def test_on_selection_change_on_empty(self):\n        \"\"\"Test clicks on empty scene, when no data\"\"\"\n        widget = self.widget\n        widget.on_selection_change([])\n\n    @_patch_recompute_som\n    def test_output_selection(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        out = self.get_output(widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n        self.assertTrue(\n            np.all(out.get_column(ANNOTATED_DATA_FEATURE_NAME) == 0))\n\n        self.widget.cells = np.array(\n            [[[0, 30], [30, 50]] + [[50, 50]] * 6,\n             [[50, 50], [50, 50], [50, 150]] + [[150, 150]] * 5]\n            + [[[150, 150]] * 8] * 6)\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        widget.on_selection_change(m)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:30])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column(ANNOTATED_DATA_FEATURE_NAME),\n            [1] * 30 + [0] * 120)\n\n        m[0, 0] = False\n        m[0, 1] = True\n        widget.on_selection_change(m, SomView.SelectionNewGroup)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:50])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column(ANNOTATED_DATA_FEATURE_NAME),\n            [0] * 30 + [1] * 20 + [2] * 100)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))\n\n    @_patch_recompute_som\n    def test_output_columns(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        m[1, 2] = True\n        widget.on_selection_change(m)\n\n        out = self.get_output(widget.Outputs.annotated_data)\n\n        np.testing.assert_equal(out.get_column(\"som_row\"),\n                                [1] * 30 + [2] * 20 + [3] * 100)\n        np.testing.assert_equal(out.get_column(\"som_col\"),\n                                [1] * 50 + [2] * 100)\n        cell_var = out.domain[\"som_cell\"]\n        np.testing.assert_equal([cell_var.repr_val(v)\n                                 for v in out.get_column(\"som_cell\")],\n                                [\"r1c1\"] * 30 + [\"r2c1\"] * 20 + [\"r3c2\"] * 100)\n        np.testing.assert_equal(out.get_column(\"som_error\"),\n                                np.arange(150) / 150)\n\n    def test_invalidated(self):\n        heart = Table(\"heart_disease\")\n        self.widget._recompute_som = Mock()\n\n        # New data - replot\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_called_once()\n\n        # Same data - no replot\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_not_called()\n\n        # Same data.X - no replot\n        domain = heart.domain\n        domain = Domain(domain.attributes, metas=domain.class_vars)\n        heart_with_metas = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_metas)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data, same set of cont. vars - no replot\n        attrs = [a for a in heart.domain.attributes if a.is_continuous]\n        domain = Domain(attrs)\n        heart_with_cont_features = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_cont_features)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data.X - replot\n        domain = Domain(heart.domain.attributes[:5])\n        heart_with_less_features = heart.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_less_features)\n        self.widget._recompute_som.assert_called_once()\n\n    def test_modified_info(self):\n        w = self.widget\n        self.assertFalse(w.Information.modified.is_shown())\n        self.send_signal(w.Inputs.data, self.iris)\n        self.assertFalse(w.Information.modified.is_shown())\n        restart_button = w.controlArea.findChild(QPushButton)\n\n        # modify grid\n        simulate.combobox_activate_index(w.controlArea.findChild(QComboBox), 1)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify set dimensions automatically\n        w.controlArea.findChild(QCheckBox).setCheckState(Qt.Unchecked)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify dimension spins\n        w.spin_x.setValue(7)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        w.spin_y.setValue(7)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify initialization\n        simulate.combobox_activate_index(w.controlArea.findChildren(QComboBox)[1], 1)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n    def test_make_domain_without_class_vars(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        widget.data.domain.class_vars = None\n        widget.update_output()\n        self.assertIsNotNone(self.get_output(widget.Outputs.annotated_data))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 45,
                            "end_lineno": 731,
                            "parent": null
                        }
                    ],
                    "added_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/tests/test_owsom.py",
                            "type": "method",
                            "name": "TestOWSOM.test_make_domain_without_class_vars",
                            "content": "class TestOWSOM(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWSOM)  #: OWSOM\n        self.iris = Table(\"iris\")\n\n    def tearDown(self):\n        self.widget.onDeleteWidget()\n        super().tearDown()\n\n    def test_requires_continuous(self):\n        widget = self.widget\n        heart = Table(\"heart_disease\")\n\n        self.send_signal(widget.Inputs.data, Table(\"zoo\"))\n        self.assertTrue(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, heart)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(\n            widget.cont_x.shape[1],\n            sum(attr.is_continuous for attr in heart.domain.attributes))\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_single_attribute(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Warning.single_attribute.is_shown())\n        iris = self.iris[:, 0]\n        self.send_signal(widget.Inputs.data, iris)\n        self.assertTrue(widget.Warning.single_attribute.is_shown())\n\n    def test_missing_all_data(self):\n        widget = self.widget\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        with self.iris.unlocked():\n            for i in range(150):\n                self.iris.X[i, i % 4] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n    def test_missing_some_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:50, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n        np.testing.assert_almost_equal(\n            widget.data.Y.flatten(), [1] * 50 + [2] * 50)\n        self.assertEqual(widget.cont_x.shape, (100, 4))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_missing_one_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[5, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_run_actual_optimization(self):\n        # ther tests that compute something use _patch_recompute_som\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        out = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n\n    @_patch_recompute_som\n    def test_single_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:-1] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n\n    @_patch_recompute_som\n    def test_sparse_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X = sp.csc_matrix(self.iris.X)\n\n        # Table.from_table can decide to return dense data\n        with patch.object(Table, \"from_table\", lambda _, x: x):\n            # update_output would fail because of above patch\n            widget.update_output = Mock()\n            self.send_signal(widget.Inputs.data, self.iris)\n        self.assertIs(widget.data, self.iris)\n        self.assertTrue(sp.isspmatrix_csr(widget.cont_x))\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    @_patch_recompute_som\n    def test_auto_compute_dimensions(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        spin_x = widget.opt_controls.spin_x\n        spin_y = widget.opt_controls.spin_y\n\n        spin_x.setValue(100)\n        spin_y.setValue(100)\n        widget.auto_dimension = True\n        widget.controls.auto_dimension.toggled.emit(True)\n        self.assertLess(spin_x.value(), 100)\n        self.assertLess(spin_y.value(), 100)\n\n        spin_x.setValue(9)\n        spin_y.setValue(7)\n        widget.auto_dimension = False\n        widget.controls.auto_dimension.toggled.emit(False)\n        self.assertEqual(spin_x.value(), 10)\n        self.assertEqual(spin_y.value(), 5)\n\n    @_patch_recompute_som\n    def test_redraw_grid(self):\n        widget = self.widget\n\n        widget.auto_dimension = False\n\n        widget.opt_controls.spin_x.setValue(8)\n        widget.opt_controls.spin_y.setValue(5)\n        widget.opt_controls.shape.setCurrentIndex(1)\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 38)\n        self.assertIsNone(widget.grid_cells[1, 7])\n        self.assertIsNone(widget.grid_cells[3, 7])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n    def test_attr_color_change(self):\n        widget = self.widget\n        widget._redraw = Mock()\n\n        heart = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, heart)\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        combo.setCurrentIndex(0)\n        combo.activated[int].emit(0)\n        self.assertFalse(widget.controls.pie_charts.isEnabled())\n        self.assertIsNone(widget.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        gender = heart.domain[\"gender\"]\n        ind_gen = combo.model().indexOf(gender)\n        combo.setCurrentIndex(ind_gen)\n        combo.activated[int].emit(ind_gen)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        np.testing.assert_equal(widget.colors.palette, gender.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        age = heart.domain[\"age\"]\n        ind_age = combo.model().indexOf(age)\n        combo.setCurrentIndex(ind_age)\n        combo.activated[int].emit(ind_age)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        self.assertIsNotNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n    def test_colored_circles_with_constant(self):\n        domain = self.iris.domain\n        self.widget.pie_charts = False\n\n        with self.iris.unlocked():\n            self.iris.X[:, 0] = 1\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        attr0 = domain.attributes[0]\n\n        combo = self.widget.controls.attr_color\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNotNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n        dom1 = Domain(domain.attributes[1:], domain.class_var,\n                      domain.attributes[:1])\n        iris = self.iris.transform(dom1).copy()\n        with iris.unlocked(iris.metas):\n            iris.metas[::2, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, iris)\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNotNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n        iris = self.iris.transform(dom1).copy()\n        with iris.unlocked(iris.metas):\n            iris.metas[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, iris)\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNone(self.widget.colors)\n        self.assertTrue(self.widget.Warning.no_defined_colors.is_shown())\n\n        simulate.combobox_activate_index(combo, 0)\n        self.assertIsNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n    @_patch_recompute_som\n    def test_cell_sizes(self):\n        widget = self.widget\n        widget._draw_same_color = widget._draw_pie_charts = \\\n            widget._draw_colored_circles = draw = Mock()\n\n        widget.size_by_instances = False\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.size_by_instances = True\n        widget.pie_charts = False\n        widget.controls.size_by_instances.toggled.emit(True)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8 * (30 / 100))\n        self.assertAlmostEqual(sizes[0, 1], 0.8 * (20 / 100))\n        self.assertAlmostEqual(sizes[1, 2], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n        widget.size_by_instances = False\n        widget.controls.size_by_instances.toggled.emit(False)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n    @_patch_recompute_som\n    def test_same_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_same_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n        widget.opt_controls.shape.setCurrentIndex(0)\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * (30 / 100)),\n                         (0.5, np.sqrt(3) / 2, 0.4 * (20 / 100)),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0.5, np.sqrt(3) / 2, 0.4 * 20 / 100),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_pie_charts(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.pie_charts = True\n        widget.controls.pie_charts.toggled.emit(True)\n\n        a, b, c = widget.elements.childItems()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0.5, np.sqrt(3) / 2, 0.8 * 20 / 100),\n                                 (1, np.sqrt(3), 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n            self.assertEqual(len(e.colors), len(widget.colors) + 1)\n        np.testing.assert_equal(a.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(b.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(c.dist, [0, 0.5, 0.5, 0])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0, 1, 0.8 * 20 / 100),\n                                 (1, 2, 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n\n        with self.iris.unlocked():\n            self.iris.Y[:15] = np.nan\n        self.send_signal(widget.Inputs.data, self.iris)\n        a = widget.elements.childItems()[0]\n        np.testing.assert_equal(a.dist, [0.5, 0, 0, 0.5])\n\n    @_patch_recompute_som\n    def test_get_color_column(self):\n        widget = self.widget\n\n        table = Table(\"heart_disease\")\n        domain = table.domain\n        new_domain = Domain(\n            domain.attributes[3:], domain.class_var, domain.attributes[:3])\n        new_table = table.transform(new_domain).copy()\n        with new_table.unlocked(new_table.metas):\n            new_table.metas = new_table.metas.astype(object)\n        self.send_signal(widget.Inputs.data, new_table)\n\n        # discrete attribute\n        widget.attr_color = domain[\"rest ECG\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column(\"rest ECG\").astype(int))\n\n        # discrete meta\n        widget.attr_color = domain[\"gender\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column(\"gender\").astype(int))\n\n        # numeric attribute\n        widget.thresholds = np.array([120, 150])\n        widget.attr_color = domain[\"max HR\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column(\"max HR\")):\n            if d < 120:\n                self.assertEqual(c, 0)\n            if 120 <= d < 150:\n                self.assertEqual(c, 1)\n            if d >= 150:\n                self.assertEqual(c, 2)\n\n        # numeric meta\n        widget.thresholds = np.array([50, 60])\n        widget.attr_color = domain[\"age\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column(\"age\")):\n            if d < 50:\n                self.assertEqual(c, 0)\n            if 50 <= d < 60:\n                self.assertEqual(c, 1)\n            if d >= 60:\n                self.assertEqual(c, 2)\n\n        # discrete meta with missing values\n        widget.attr_color = domain[\"gender\"]\n        with widget.data.unlocked():\n            col = widget.data.get_column(\"gender\", copy=True)\n            widget.data.metas[:5, 1] = np.nan  # gender\n        col[:5] = 2\n        np.testing.assert_equal(widget._get_color_column(), col)\n\n    @_patch_recompute_som\n    def test_colored_circles_with_missing_values(self):\n        with self.iris.unlocked():\n            self.iris.Y[:6] = np.nan\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertTrue(self.widget.Warning.missing_colors.is_shown())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_colors.is_shown())\n\n    def test_send_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n    @_patch_recompute_som\n    def test_on_selection_change(self):\n        def selm(*cells):\n            m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n            for x, y in cells:\n                m[x, y] = True\n            return m\n\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        widget.redraw_selection = Mock()\n        widget.update_output = Mock()\n\n        m = selm((0, 0)).astype(int)\n        widget.on_selection_change(selm((0, 0)))\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n        widget.redraw_selection.assert_called_once()\n        widget.update_output.assert_called_once()\n\n        m = selm((0, 1)).astype(int)\n        widget.on_selection_change(selm((0, 1)))\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 1\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionAddToGroup)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 0\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionRemove)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 2\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionNewGroup)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n    @_patch_recompute_som\n    def test_on_selection_change_on_empty(self):\n        \"\"\"Test clicks on empty scene, when no data\"\"\"\n        widget = self.widget\n        widget.on_selection_change([])\n\n    @_patch_recompute_som\n    def test_output_selection(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        out = self.get_output(widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n        self.assertTrue(\n            np.all(out.get_column(ANNOTATED_DATA_FEATURE_NAME) == 0))\n\n        self.widget.cells = np.array(\n            [[[0, 30], [30, 50]] + [[50, 50]] * 6,\n             [[50, 50], [50, 50], [50, 150]] + [[150, 150]] * 5]\n            + [[[150, 150]] * 8] * 6)\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        widget.on_selection_change(m)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:30])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column(ANNOTATED_DATA_FEATURE_NAME),\n            [1] * 30 + [0] * 120)\n\n        m[0, 0] = False\n        m[0, 1] = True\n        widget.on_selection_change(m, SomView.SelectionNewGroup)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:50])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column(ANNOTATED_DATA_FEATURE_NAME),\n            [0] * 30 + [1] * 20 + [2] * 100)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))\n\n    @_patch_recompute_som\n    def test_output_columns(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        m[1, 2] = True\n        widget.on_selection_change(m)\n\n        out = self.get_output(widget.Outputs.annotated_data)\n\n        np.testing.assert_equal(out.get_column(\"som_row\"),\n                                [1] * 30 + [2] * 20 + [3] * 100)\n        np.testing.assert_equal(out.get_column(\"som_col\"),\n                                [1] * 50 + [2] * 100)\n        cell_var = out.domain[\"som_cell\"]\n        np.testing.assert_equal([cell_var.repr_val(v)\n                                 for v in out.get_column(\"som_cell\")],\n                                [\"r1c1\"] * 30 + [\"r2c1\"] * 20 + [\"r3c2\"] * 100)\n        np.testing.assert_equal(out.get_column(\"som_error\"),\n                                np.arange(150) / 150)\n\n    def test_invalidated(self):\n        heart = Table(\"heart_disease\")\n        self.widget._recompute_som = Mock()\n\n        # New data - replot\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_called_once()\n\n        # Same data - no replot\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_not_called()\n\n        # Same data.X - no replot\n        domain = heart.domain\n        domain = Domain(domain.attributes, metas=domain.class_vars)\n        heart_with_metas = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_metas)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data, same set of cont. vars - no replot\n        attrs = [a for a in heart.domain.attributes if a.is_continuous]\n        domain = Domain(attrs)\n        heart_with_cont_features = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_cont_features)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data.X - replot\n        domain = Domain(heart.domain.attributes[:5])\n        heart_with_less_features = heart.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_less_features)\n        self.widget._recompute_som.assert_called_once()\n\n    def test_modified_info(self):\n        w = self.widget\n        self.assertFalse(w.Information.modified.is_shown())\n        self.send_signal(w.Inputs.data, self.iris)\n        self.assertFalse(w.Information.modified.is_shown())\n        restart_button = w.controlArea.findChild(QPushButton)\n\n        # modify grid\n        simulate.combobox_activate_index(w.controlArea.findChild(QComboBox), 1)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify set dimensions automatically\n        w.controlArea.findChild(QCheckBox).setCheckState(Qt.Unchecked)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify dimension spins\n        w.spin_x.setValue(7)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        w.spin_y.setValue(7)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify initialization\n        simulate.combobox_activate_index(w.controlArea.findChildren(QComboBox)[1], 1)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n    def test_make_domain_without_class_vars(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        widget.data.domain.class_vars = None\n        widget.update_output()\n        self.assertIsNotNone(self.get_output(widget.Outputs.annotated_data))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 726,
                            "end_lineno": 731,
                            "parent": {
                                "file_name": "Orange/widgets/unsupervised/tests/test_owsom.py",
                                "type": "class",
                                "name": "TestOWSOM",
                                "content": "class TestOWSOM(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWSOM)  #: OWSOM\n        self.iris = Table(\"iris\")\n\n    def tearDown(self):\n        self.widget.onDeleteWidget()\n        super().tearDown()\n\n    def test_requires_continuous(self):\n        widget = self.widget\n        heart = Table(\"heart_disease\")\n\n        self.send_signal(widget.Inputs.data, Table(\"zoo\"))\n        self.assertTrue(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, heart)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(\n            widget.cont_x.shape[1],\n            sum(attr.is_continuous for attr in heart.domain.attributes))\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_single_attribute(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Warning.single_attribute.is_shown())\n        iris = self.iris[:, 0]\n        self.send_signal(widget.Inputs.data, iris)\n        self.assertTrue(widget.Warning.single_attribute.is_shown())\n\n    def test_missing_all_data(self):\n        widget = self.widget\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        with self.iris.unlocked():\n            for i in range(150):\n                self.iris.X[i, i % 4] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n    def test_missing_some_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:50, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n        np.testing.assert_almost_equal(\n            widget.data.Y.flatten(), [1] * 50 + [2] * 50)\n        self.assertEqual(widget.cont_x.shape, (100, 4))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_missing_one_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[5, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_run_actual_optimization(self):\n        # ther tests that compute something use _patch_recompute_som\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        out = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n\n    @_patch_recompute_som\n    def test_single_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:-1] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n\n    @_patch_recompute_som\n    def test_sparse_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X = sp.csc_matrix(self.iris.X)\n\n        # Table.from_table can decide to return dense data\n        with patch.object(Table, \"from_table\", lambda _, x: x):\n            # update_output would fail because of above patch\n            widget.update_output = Mock()\n            self.send_signal(widget.Inputs.data, self.iris)\n        self.assertIs(widget.data, self.iris)\n        self.assertTrue(sp.isspmatrix_csr(widget.cont_x))\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    @_patch_recompute_som\n    def test_auto_compute_dimensions(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        spin_x = widget.opt_controls.spin_x\n        spin_y = widget.opt_controls.spin_y\n\n        spin_x.setValue(100)\n        spin_y.setValue(100)\n        widget.auto_dimension = True\n        widget.controls.auto_dimension.toggled.emit(True)\n        self.assertLess(spin_x.value(), 100)\n        self.assertLess(spin_y.value(), 100)\n\n        spin_x.setValue(9)\n        spin_y.setValue(7)\n        widget.auto_dimension = False\n        widget.controls.auto_dimension.toggled.emit(False)\n        self.assertEqual(spin_x.value(), 10)\n        self.assertEqual(spin_y.value(), 5)\n\n    @_patch_recompute_som\n    def test_redraw_grid(self):\n        widget = self.widget\n\n        widget.auto_dimension = False\n\n        widget.opt_controls.spin_x.setValue(8)\n        widget.opt_controls.spin_y.setValue(5)\n        widget.opt_controls.shape.setCurrentIndex(1)\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 38)\n        self.assertIsNone(widget.grid_cells[1, 7])\n        self.assertIsNone(widget.grid_cells[3, 7])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n    def test_attr_color_change(self):\n        widget = self.widget\n        widget._redraw = Mock()\n\n        heart = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, heart)\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        combo.setCurrentIndex(0)\n        combo.activated[int].emit(0)\n        self.assertFalse(widget.controls.pie_charts.isEnabled())\n        self.assertIsNone(widget.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        gender = heart.domain[\"gender\"]\n        ind_gen = combo.model().indexOf(gender)\n        combo.setCurrentIndex(ind_gen)\n        combo.activated[int].emit(ind_gen)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        np.testing.assert_equal(widget.colors.palette, gender.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        age = heart.domain[\"age\"]\n        ind_age = combo.model().indexOf(age)\n        combo.setCurrentIndex(ind_age)\n        combo.activated[int].emit(ind_age)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        self.assertIsNotNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n    def test_colored_circles_with_constant(self):\n        domain = self.iris.domain\n        self.widget.pie_charts = False\n\n        with self.iris.unlocked():\n            self.iris.X[:, 0] = 1\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        attr0 = domain.attributes[0]\n\n        combo = self.widget.controls.attr_color\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNotNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n        dom1 = Domain(domain.attributes[1:], domain.class_var,\n                      domain.attributes[:1])\n        iris = self.iris.transform(dom1).copy()\n        with iris.unlocked(iris.metas):\n            iris.metas[::2, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, iris)\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNotNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n        iris = self.iris.transform(dom1).copy()\n        with iris.unlocked(iris.metas):\n            iris.metas[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, iris)\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNone(self.widget.colors)\n        self.assertTrue(self.widget.Warning.no_defined_colors.is_shown())\n\n        simulate.combobox_activate_index(combo, 0)\n        self.assertIsNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n    @_patch_recompute_som\n    def test_cell_sizes(self):\n        widget = self.widget\n        widget._draw_same_color = widget._draw_pie_charts = \\\n            widget._draw_colored_circles = draw = Mock()\n\n        widget.size_by_instances = False\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.size_by_instances = True\n        widget.pie_charts = False\n        widget.controls.size_by_instances.toggled.emit(True)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8 * (30 / 100))\n        self.assertAlmostEqual(sizes[0, 1], 0.8 * (20 / 100))\n        self.assertAlmostEqual(sizes[1, 2], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n        widget.size_by_instances = False\n        widget.controls.size_by_instances.toggled.emit(False)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n    @_patch_recompute_som\n    def test_same_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_same_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n        widget.opt_controls.shape.setCurrentIndex(0)\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * (30 / 100)),\n                         (0.5, np.sqrt(3) / 2, 0.4 * (20 / 100)),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0.5, np.sqrt(3) / 2, 0.4 * 20 / 100),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_pie_charts(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.pie_charts = True\n        widget.controls.pie_charts.toggled.emit(True)\n\n        a, b, c = widget.elements.childItems()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0.5, np.sqrt(3) / 2, 0.8 * 20 / 100),\n                                 (1, np.sqrt(3), 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n            self.assertEqual(len(e.colors), len(widget.colors) + 1)\n        np.testing.assert_equal(a.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(b.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(c.dist, [0, 0.5, 0.5, 0])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0, 1, 0.8 * 20 / 100),\n                                 (1, 2, 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n\n        with self.iris.unlocked():\n            self.iris.Y[:15] = np.nan\n        self.send_signal(widget.Inputs.data, self.iris)\n        a = widget.elements.childItems()[0]\n        np.testing.assert_equal(a.dist, [0.5, 0, 0, 0.5])\n\n    @_patch_recompute_som\n    def test_get_color_column(self):\n        widget = self.widget\n\n        table = Table(\"heart_disease\")\n        domain = table.domain\n        new_domain = Domain(\n            domain.attributes[3:], domain.class_var, domain.attributes[:3])\n        new_table = table.transform(new_domain).copy()\n        with new_table.unlocked(new_table.metas):\n            new_table.metas = new_table.metas.astype(object)\n        self.send_signal(widget.Inputs.data, new_table)\n\n        # discrete attribute\n        widget.attr_color = domain[\"rest ECG\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column(\"rest ECG\").astype(int))\n\n        # discrete meta\n        widget.attr_color = domain[\"gender\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column(\"gender\").astype(int))\n\n        # numeric attribute\n        widget.thresholds = np.array([120, 150])\n        widget.attr_color = domain[\"max HR\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column(\"max HR\")):\n            if d < 120:\n                self.assertEqual(c, 0)\n            if 120 <= d < 150:\n                self.assertEqual(c, 1)\n            if d >= 150:\n                self.assertEqual(c, 2)\n\n        # numeric meta\n        widget.thresholds = np.array([50, 60])\n        widget.attr_color = domain[\"age\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column(\"age\")):\n            if d < 50:\n                self.assertEqual(c, 0)\n            if 50 <= d < 60:\n                self.assertEqual(c, 1)\n            if d >= 60:\n                self.assertEqual(c, 2)\n\n        # discrete meta with missing values\n        widget.attr_color = domain[\"gender\"]\n        with widget.data.unlocked():\n            col = widget.data.get_column(\"gender\", copy=True)\n            widget.data.metas[:5, 1] = np.nan  # gender\n        col[:5] = 2\n        np.testing.assert_equal(widget._get_color_column(), col)\n\n    @_patch_recompute_som\n    def test_colored_circles_with_missing_values(self):\n        with self.iris.unlocked():\n            self.iris.Y[:6] = np.nan\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertTrue(self.widget.Warning.missing_colors.is_shown())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_colors.is_shown())\n\n    def test_send_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n    @_patch_recompute_som\n    def test_on_selection_change(self):\n        def selm(*cells):\n            m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n            for x, y in cells:\n                m[x, y] = True\n            return m\n\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        widget.redraw_selection = Mock()\n        widget.update_output = Mock()\n\n        m = selm((0, 0)).astype(int)\n        widget.on_selection_change(selm((0, 0)))\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n        widget.redraw_selection.assert_called_once()\n        widget.update_output.assert_called_once()\n\n        m = selm((0, 1)).astype(int)\n        widget.on_selection_change(selm((0, 1)))\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 1\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionAddToGroup)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 0\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionRemove)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 2\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionNewGroup)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n    @_patch_recompute_som\n    def test_on_selection_change_on_empty(self):\n        \"\"\"Test clicks on empty scene, when no data\"\"\"\n        widget = self.widget\n        widget.on_selection_change([])\n\n    @_patch_recompute_som\n    def test_output_selection(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        out = self.get_output(widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n        self.assertTrue(\n            np.all(out.get_column(ANNOTATED_DATA_FEATURE_NAME) == 0))\n\n        self.widget.cells = np.array(\n            [[[0, 30], [30, 50]] + [[50, 50]] * 6,\n             [[50, 50], [50, 50], [50, 150]] + [[150, 150]] * 5]\n            + [[[150, 150]] * 8] * 6)\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        widget.on_selection_change(m)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:30])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column(ANNOTATED_DATA_FEATURE_NAME),\n            [1] * 30 + [0] * 120)\n\n        m[0, 0] = False\n        m[0, 1] = True\n        widget.on_selection_change(m, SomView.SelectionNewGroup)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:50])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column(ANNOTATED_DATA_FEATURE_NAME),\n            [0] * 30 + [1] * 20 + [2] * 100)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))\n\n    @_patch_recompute_som\n    def test_output_columns(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        m[1, 2] = True\n        widget.on_selection_change(m)\n\n        out = self.get_output(widget.Outputs.annotated_data)\n\n        np.testing.assert_equal(out.get_column(\"som_row\"),\n                                [1] * 30 + [2] * 20 + [3] * 100)\n        np.testing.assert_equal(out.get_column(\"som_col\"),\n                                [1] * 50 + [2] * 100)\n        cell_var = out.domain[\"som_cell\"]\n        np.testing.assert_equal([cell_var.repr_val(v)\n                                 for v in out.get_column(\"som_cell\")],\n                                [\"r1c1\"] * 30 + [\"r2c1\"] * 20 + [\"r3c2\"] * 100)\n        np.testing.assert_equal(out.get_column(\"som_error\"),\n                                np.arange(150) / 150)\n\n    def test_invalidated(self):\n        heart = Table(\"heart_disease\")\n        self.widget._recompute_som = Mock()\n\n        # New data - replot\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_called_once()\n\n        # Same data - no replot\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_not_called()\n\n        # Same data.X - no replot\n        domain = heart.domain\n        domain = Domain(domain.attributes, metas=domain.class_vars)\n        heart_with_metas = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_metas)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data, same set of cont. vars - no replot\n        attrs = [a for a in heart.domain.attributes if a.is_continuous]\n        domain = Domain(attrs)\n        heart_with_cont_features = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_cont_features)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data.X - replot\n        domain = Domain(heart.domain.attributes[:5])\n        heart_with_less_features = heart.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_less_features)\n        self.widget._recompute_som.assert_called_once()\n\n    def test_modified_info(self):\n        w = self.widget\n        self.assertFalse(w.Information.modified.is_shown())\n        self.send_signal(w.Inputs.data, self.iris)\n        self.assertFalse(w.Information.modified.is_shown())\n        restart_button = w.controlArea.findChild(QPushButton)\n\n        # modify grid\n        simulate.combobox_activate_index(w.controlArea.findChild(QComboBox), 1)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify set dimensions automatically\n        w.controlArea.findChild(QCheckBox).setCheckState(Qt.Unchecked)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify dimension spins\n        w.spin_x.setValue(7)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        w.spin_y.setValue(7)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify initialization\n        simulate.combobox_activate_index(w.controlArea.findChildren(QComboBox)[1], 1)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n    def test_make_domain_without_class_vars(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        widget.data.domain.class_vars = None\n        widget.update_output()\n        self.assertIsNotNone(self.get_output(widget.Outputs.annotated_data))",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 45,
                                "end_lineno": 731,
                                "parent": null
                            }
                        }
                    ],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "371a1793bec28936d4a5e4dd0be7de6fb270c8b1^",
    "new_commit_hash": "371a1793bec28936d4a5e4dd0be7de6fb270c8b1",
    "commit_message": "fix type error in owsom",
    "commit_date": "2024-03-19T11:40:28+01:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "371a1793bec28936d4a5e4dd0be7de6fb270c8b1",
    "test_file_codes": [
        "# pylint: disable=protected-access\n\nimport unittest\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\nimport scipy.sparse as sp\nfrom AnyQt.QtWidgets import QComboBox, QPushButton, QCheckBox\nfrom AnyQt.QtCore import Qt\n\nfrom Orange.data import Table, Domain\nfrom Orange.widgets.tests.base import WidgetTest\nfrom Orange.widgets.tests.utils import simulate\nfrom Orange.widgets.utils.annotated_data import ANNOTATED_DATA_FEATURE_NAME\nfrom Orange.widgets.unsupervised.owsom import OWSOM, SomView, SOM, \\\n    SomSharedValueCompute, SomCellCompute, SomCoordsCompute, SomErrorCompute\n\n\ndef _patch_recompute_som(meth):\n    def winners_from_weights(cont_x, *_1, **_2):\n        n = cont_x.shape[0]\n        w = np.zeros((n, 2), dtype=int)\n        w[n // 5:] = [0, 1]\n        w[n // 3:] = [1, 2]\n        return w, np.arange(n) / n\n\n    def recompute(self):\n        if not self.data:\n            return\n\n        self.som = Mock()\n        self.som.winners = winners_from_weights\n        self._assign_instances(None, None)\n        self._redraw()\n        self.update_output()\n\n    def patched(*args, **kwargs):\n        with patch.object(SOM, \"winner_from_weights\", new=winners_from_weights), \\\n                patch.object(OWSOM, \"_recompute_som\", new=recompute):\n            meth(*args, **kwargs)\n\n    return patched\n\n\nclass TestOWSOM(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWSOM)  #: OWSOM\n        self.iris = Table(\"iris\")\n\n    def tearDown(self):\n        self.widget.onDeleteWidget()\n        super().tearDown()\n\n    def test_requires_continuous(self):\n        widget = self.widget\n        heart = Table(\"heart_disease\")\n\n        self.send_signal(widget.Inputs.data, Table(\"zoo\"))\n        self.assertTrue(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, heart)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(\n            widget.cont_x.shape[1],\n            sum(attr.is_continuous for attr in heart.domain.attributes))\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_single_attribute(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Warning.single_attribute.is_shown())\n        iris = self.iris[:, 0]\n        self.send_signal(widget.Inputs.data, iris)\n        self.assertTrue(widget.Warning.single_attribute.is_shown())\n\n    def test_missing_all_data(self):\n        widget = self.widget\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        with self.iris.unlocked():\n            for i in range(150):\n                self.iris.X[i, i % 4] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n    def test_missing_some_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:50, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n        np.testing.assert_almost_equal(\n            widget.data.Y.flatten(), [1] * 50 + [2] * 50)\n        self.assertEqual(widget.cont_x.shape, (100, 4))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_missing_one_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[5, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_run_actual_optimization(self):\n        # ther tests that compute something use _patch_recompute_som\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        out = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n\n    @_patch_recompute_som\n    def test_single_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:-1] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.not_enough_data.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Error.not_enough_data.is_shown())\n\n    @_patch_recompute_som\n    def test_sparse_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X = sp.csc_matrix(self.iris.X)\n\n        # Table.from_table can decide to return dense data\n        with patch.object(Table, \"from_table\", lambda _, x: x):\n            # update_output would fail because of above patch\n            widget.update_output = Mock()\n            self.send_signal(widget.Inputs.data, self.iris)\n        self.assertIs(widget.data, self.iris)\n        self.assertTrue(sp.isspmatrix_csr(widget.cont_x))\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    @_patch_recompute_som\n    def test_auto_compute_dimensions(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        spin_x = widget.opt_controls.spin_x\n        spin_y = widget.opt_controls.spin_y\n\n        spin_x.setValue(100)\n        spin_y.setValue(100)\n        widget.auto_dimension = True\n        widget.controls.auto_dimension.toggled.emit(True)\n        self.assertLess(spin_x.value(), 100)\n        self.assertLess(spin_y.value(), 100)\n\n        spin_x.setValue(9)\n        spin_y.setValue(7)\n        widget.auto_dimension = False\n        widget.controls.auto_dimension.toggled.emit(False)\n        self.assertEqual(spin_x.value(), 10)\n        self.assertEqual(spin_y.value(), 5)\n\n    @_patch_recompute_som\n    def test_redraw_grid(self):\n        widget = self.widget\n\n        widget.auto_dimension = False\n\n        widget.opt_controls.spin_x.setValue(8)\n        widget.opt_controls.spin_y.setValue(5)\n        widget.opt_controls.shape.setCurrentIndex(1)\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 38)\n        self.assertIsNone(widget.grid_cells[1, 7])\n        self.assertIsNone(widget.grid_cells[3, 7])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n    def test_attr_color_change(self):\n        widget = self.widget\n        widget._redraw = Mock()\n\n        heart = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, heart)\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        combo.setCurrentIndex(0)\n        combo.activated[int].emit(0)\n        self.assertFalse(widget.controls.pie_charts.isEnabled())\n        self.assertIsNone(widget.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        gender = heart.domain[\"gender\"]\n        ind_gen = combo.model().indexOf(gender)\n        combo.setCurrentIndex(ind_gen)\n        combo.activated[int].emit(ind_gen)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        np.testing.assert_equal(widget.colors.palette, gender.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        age = heart.domain[\"age\"]\n        ind_age = combo.model().indexOf(age)\n        combo.setCurrentIndex(ind_age)\n        combo.activated[int].emit(ind_age)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        self.assertIsNotNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n    def test_colored_circles_with_constant(self):\n        domain = self.iris.domain\n        self.widget.pie_charts = False\n\n        with self.iris.unlocked():\n            self.iris.X[:, 0] = 1\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        attr0 = domain.attributes[0]\n\n        combo = self.widget.controls.attr_color\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNotNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n        dom1 = Domain(domain.attributes[1:], domain.class_var,\n                      domain.attributes[:1])\n        iris = self.iris.transform(dom1).copy()\n        with iris.unlocked(iris.metas):\n            iris.metas[::2, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, iris)\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNotNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n        iris = self.iris.transform(dom1).copy()\n        with iris.unlocked(iris.metas):\n            iris.metas[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, iris)\n        simulate.combobox_activate_index(combo, combo.model().indexOf(attr0))\n        self.assertIsNone(self.widget.colors)\n        self.assertTrue(self.widget.Warning.no_defined_colors.is_shown())\n\n        simulate.combobox_activate_index(combo, 0)\n        self.assertIsNone(self.widget.colors)\n        self.assertFalse(self.widget.Warning.no_defined_colors.is_shown())\n\n    @_patch_recompute_som\n    def test_cell_sizes(self):\n        widget = self.widget\n        widget._draw_same_color = widget._draw_pie_charts = \\\n            widget._draw_colored_circles = draw = Mock()\n\n        widget.size_by_instances = False\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.size_by_instances = True\n        widget.pie_charts = False\n        widget.controls.size_by_instances.toggled.emit(True)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8 * (30 / 100))\n        self.assertAlmostEqual(sizes[0, 1], 0.8 * (20 / 100))\n        self.assertAlmostEqual(sizes[1, 2], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n        widget.size_by_instances = False\n        widget.controls.size_by_instances.toggled.emit(False)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n    @_patch_recompute_som\n    def test_same_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_same_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n        widget.opt_controls.shape.setCurrentIndex(0)\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * (30 / 100)),\n                         (0.5, np.sqrt(3) / 2, 0.4 * (20 / 100)),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0.5, np.sqrt(3) / 2, 0.4 * 20 / 100),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_pie_charts(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.pie_charts = True\n        widget.controls.pie_charts.toggled.emit(True)\n\n        a, b, c = widget.elements.childItems()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0.5, np.sqrt(3) / 2, 0.8 * 20 / 100),\n                                 (1, np.sqrt(3), 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n            self.assertEqual(len(e.colors), len(widget.colors) + 1)\n        np.testing.assert_equal(a.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(b.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(c.dist, [0, 0.5, 0.5, 0])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0, 1, 0.8 * 20 / 100),\n                                 (1, 2, 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n\n        with self.iris.unlocked():\n            self.iris.Y[:15] = np.nan\n        self.send_signal(widget.Inputs.data, self.iris)\n        a = widget.elements.childItems()[0]\n        np.testing.assert_equal(a.dist, [0.5, 0, 0, 0.5])\n\n    @_patch_recompute_som\n    def test_get_color_column(self):\n        widget = self.widget\n\n        table = Table(\"heart_disease\")\n        domain = table.domain\n        new_domain = Domain(\n            domain.attributes[3:], domain.class_var, domain.attributes[:3])\n        new_table = table.transform(new_domain).copy()\n        with new_table.unlocked(new_table.metas):\n            new_table.metas = new_table.metas.astype(object)\n        self.send_signal(widget.Inputs.data, new_table)\n\n        # discrete attribute\n        widget.attr_color = domain[\"rest ECG\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column(\"rest ECG\").astype(int))\n\n        # discrete meta\n        widget.attr_color = domain[\"gender\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column(\"gender\").astype(int))\n\n        # numeric attribute\n        widget.thresholds = np.array([120, 150])\n        widget.attr_color = domain[\"max HR\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column(\"max HR\")):\n            if d < 120:\n                self.assertEqual(c, 0)\n            if 120 <= d < 150:\n                self.assertEqual(c, 1)\n            if d >= 150:\n                self.assertEqual(c, 2)\n\n        # numeric meta\n        widget.thresholds = np.array([50, 60])\n        widget.attr_color = domain[\"age\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column(\"age\")):\n            if d < 50:\n                self.assertEqual(c, 0)\n            if 50 <= d < 60:\n                self.assertEqual(c, 1)\n            if d >= 60:\n                self.assertEqual(c, 2)\n\n        # discrete meta with missing values\n        widget.attr_color = domain[\"gender\"]\n        with widget.data.unlocked():\n            col = widget.data.get_column(\"gender\", copy=True)\n            widget.data.metas[:5, 1] = np.nan  # gender\n        col[:5] = 2\n        np.testing.assert_equal(widget._get_color_column(), col)\n\n    @_patch_recompute_som\n    def test_colored_circles_with_missing_values(self):\n        with self.iris.unlocked():\n            self.iris.Y[:6] = np.nan\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertTrue(self.widget.Warning.missing_colors.is_shown())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_colors.is_shown())\n\n    def test_send_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n    @_patch_recompute_som\n    def test_on_selection_change(self):\n        def selm(*cells):\n            m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n            for x, y in cells:\n                m[x, y] = True\n            return m\n\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        widget.redraw_selection = Mock()\n        widget.update_output = Mock()\n\n        m = selm((0, 0)).astype(int)\n        widget.on_selection_change(selm((0, 0)))\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n        widget.redraw_selection.assert_called_once()\n        widget.update_output.assert_called_once()\n\n        m = selm((0, 1)).astype(int)\n        widget.on_selection_change(selm((0, 1)))\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 1\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionAddToGroup)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 0\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionRemove)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n        m[0, 0] = 2\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionNewGroup)\n        np.testing.assert_equal(widget.selection, m)\n        self.assertIsInstance(widget.selection, list)\n\n    @_patch_recompute_som\n    def test_on_selection_change_on_empty(self):\n        \"\"\"Test clicks on empty scene, when no data\"\"\"\n        widget = self.widget\n        widget.on_selection_change([])\n\n    @_patch_recompute_som\n    def test_output_selection(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        out = self.get_output(widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n        self.assertTrue(\n            np.all(out.get_column(ANNOTATED_DATA_FEATURE_NAME) == 0))\n\n        self.widget.cells = np.array(\n            [[[0, 30], [30, 50]] + [[50, 50]] * 6,\n             [[50, 50], [50, 50], [50, 150]] + [[150, 150]] * 5]\n            + [[[150, 150]] * 8] * 6)\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        widget.on_selection_change(m)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:30])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column(ANNOTATED_DATA_FEATURE_NAME),\n            [1] * 30 + [0] * 120)\n\n        m[0, 0] = False\n        m[0, 1] = True\n        widget.on_selection_change(m, SomView.SelectionNewGroup)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:50])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column(ANNOTATED_DATA_FEATURE_NAME),\n            [0] * 30 + [1] * 20 + [2] * 100)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))\n\n    @_patch_recompute_som\n    def test_output_columns(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        m[1, 2] = True\n        widget.on_selection_change(m)\n\n        out = self.get_output(widget.Outputs.annotated_data)\n\n        np.testing.assert_equal(out.get_column(\"som_row\"),\n                                [1] * 30 + [2] * 20 + [3] * 100)\n        np.testing.assert_equal(out.get_column(\"som_col\"),\n                                [1] * 50 + [2] * 100)\n        cell_var = out.domain[\"som_cell\"]\n        np.testing.assert_equal([cell_var.repr_val(v)\n                                 for v in out.get_column(\"som_cell\")],\n                                [\"r1c1\"] * 30 + [\"r2c1\"] * 20 + [\"r3c2\"] * 100)\n        np.testing.assert_equal(out.get_column(\"som_error\"),\n                                np.arange(150) / 150)\n\n    def test_invalidated(self):\n        heart = Table(\"heart_disease\")\n        self.widget._recompute_som = Mock()\n\n        # New data - replot\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_called_once()\n\n        # Same data - no replot\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_not_called()\n\n        # Same data.X - no replot\n        domain = heart.domain\n        domain = Domain(domain.attributes, metas=domain.class_vars)\n        heart_with_metas = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_metas)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data, same set of cont. vars - no replot\n        attrs = [a for a in heart.domain.attributes if a.is_continuous]\n        domain = Domain(attrs)\n        heart_with_cont_features = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_cont_features)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data.X - replot\n        domain = Domain(heart.domain.attributes[:5])\n        heart_with_less_features = heart.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_less_features)\n        self.widget._recompute_som.assert_called_once()\n\n    def test_modified_info(self):\n        w = self.widget\n        self.assertFalse(w.Information.modified.is_shown())\n        self.send_signal(w.Inputs.data, self.iris)\n        self.assertFalse(w.Information.modified.is_shown())\n        restart_button = w.controlArea.findChild(QPushButton)\n\n        # modify grid\n        simulate.combobox_activate_index(w.controlArea.findChild(QComboBox), 1)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify set dimensions automatically\n        w.controlArea.findChild(QCheckBox).setCheckState(Qt.Unchecked)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify dimension spins\n        w.spin_x.setValue(7)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        w.spin_y.setValue(7)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n        # modify initialization\n        simulate.combobox_activate_index(w.controlArea.findChildren(QComboBox)[1], 1)\n        self.assertTrue(w.Information.modified.is_shown())\n        restart_button.click()\n        self.assertFalse(w.Information.modified.is_shown())\n\n    def test_make_domain_without_class_vars(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        widget.data.domain.class_vars = None\n        widget.update_output()\n        self.assertIsNotNone(self.get_output(widget.Outputs.annotated_data))\n\n\nclass TestComputeValues(unittest.TestCase):\n    def test_eq_hash(self):\n        def equ(obj2):\n            self.assertEqual(obj1, obj2)\n            self.assertEqual(hash(obj1), hash(obj2))\n\n        def neq(obj2):\n            self.assertNotEqual(obj1, obj2)\n            self.assertNotEqual(hash(obj1), hash(obj2))\n\n        som1 = Mock()\n        som2 = Mock()\n        domain1 = Table(\"iris\").domain\n        domain2 = Table(\"iris\")[:, :4].domain\n        assert domain1 != domain2\n        offsets1, scales1 = np.array([1, 2, 3]), np.array([4, 5, 6])\n        offsets2, scales2 = np.array([2, 3, 4]), np.array([5, 6, 7])\n\n        shared1 = SomSharedValueCompute(domain1, som1, offsets1, scales1)\n        shared2 = SomSharedValueCompute(domain2, som1, offsets1, scales1)\n\n        obj1 = shared1\n        equ(SomSharedValueCompute(domain1, som1, offsets1, scales1))\n        neq(shared2)\n        neq(SomSharedValueCompute(domain1, som2, offsets1, scales1))\n        neq(SomSharedValueCompute(domain1, som1, offsets2, scales1))\n        neq(SomSharedValueCompute(domain1, som1, offsets1, scales2))\n\n        obj1 = SomCellCompute(shared1, 8, False)\n        equ(SomCellCompute(shared1, 8, False))\n        neq(SomCellCompute(shared2, 8, False))\n        neq(SomCellCompute(shared1, 5, False))\n        neq(SomCellCompute(shared1, 8, True))\n\n        obj1 = SomCoordsCompute(shared1, 0)\n        equ(SomCoordsCompute(shared1, 0))\n        neq(SomCoordsCompute(shared2, 0))\n        neq(SomCoordsCompute(shared1, 1))\n\n        obj1 = SomErrorCompute(shared1)\n        equ(SomErrorCompute(shared1))\n        neq(SomErrorCompute(shared2))\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
    ],
    "test_file_names": [
        "test_1.py"
    ],
    "setup_res_code": 0,
    "setup_res_stdout": "Starting Orange installation attempts...\nAttempting installation with Python 3.7...\nCompiling Orange/classification/_tree_scorers.pyx because it changed.\nCompiling Orange/data/_contingency.pyx because it changed.\nCompiling Orange/data/_io.pyx because it changed.\nCompiling Orange/data/_valuecount.pyx because it changed.\nCompiling Orange/data/_variable.pyx because it changed.\nCompiling Orange/distance/_distance.pyx because it changed.\nCompiling Orange/preprocess/_discretize.pyx because it changed.\nCompiling Orange/preprocess/_relieff.pyx because it changed.\nCompiling Orange/projection/_som.pyx because it changed.\n[1/9] Cythonizing Orange/classification/_tree_scorers.pyx\n[2/9] Cythonizing Orange/data/_contingency.pyx\n[3/9] Cythonizing Orange/data/_io.pyx\n[4/9] Cythonizing Orange/data/_valuecount.pyx\n[5/9] Cythonizing Orange/data/_variable.pyx\n[6/9] Cythonizing Orange/distance/_distance.pyx\n[7/9] Cythonizing Orange/preprocess/_discretize.pyx\n[8/9] Cythonizing Orange/preprocess/_relieff.pyx\n[9/9] Cythonizing Orange/projection/_som.pyx\nrunning build_ext\nbuilding 'Orange.classification._simple_tree' extension\ncreating build\ncreating build/temp.linux-x86_64-3.7\ncreating build/temp.linux-x86_64-3.7/Orange\ncreating build/temp.linux-x86_64-3.7/Orange/classification\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/classification/_simple_tree.c -o build/temp.linux-x86_64-3.7/Orange/classification/_simple_tree.o\ncreating build/lib.linux-x86_64-3.7\ncreating build/lib.linux-x86_64-3.7/Orange\ncreating build/lib.linux-x86_64-3.7/Orange/classification\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/classification/_simple_tree.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.widgets.utils._grid_density' extension\ncreating build/temp.linux-x86_64-3.7/Orange/widgets\ncreating build/temp.linux-x86_64-3.7/Orange/widgets/utils\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/widgets/utils/_grid_density.cpp -o build/temp.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.o\ncreating build/lib.linux-x86_64-3.7/Orange/widgets\ncreating build/lib.linux-x86_64-3.7/Orange/widgets/utils\nc++ -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.classification._tree_scorers' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/classification/_tree_scorers.c -o build/temp.linux-x86_64-3.7/Orange/classification/_tree_scorers.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/classification/_tree_scorers.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._contingency' extension\ncreating build/temp.linux-x86_64-3.7/Orange/data\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_contingency.c -o build/temp.linux-x86_64-3.7/Orange/data/_contingency.o\ncreating build/lib.linux-x86_64-3.7/Orange/data\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_contingency.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._io' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_io.c -o build/temp.linux-x86_64-3.7/Orange/data/_io.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_io.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_io.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._valuecount' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_valuecount.c -o build/temp.linux-x86_64-3.7/Orange/data/_valuecount.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_valuecount.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._variable' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_variable.c -o build/temp.linux-x86_64-3.7/Orange/data/_variable.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_variable.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.distance._distance' extension\ncreating build/temp.linux-x86_64-3.7/Orange/distance\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/distance/_distance.c -o build/temp.linux-x86_64-3.7/Orange/distance/_distance.o\ncreating build/lib.linux-x86_64-3.7/Orange/distance\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/distance/_distance.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._discretize' extension\ncreating build/temp.linux-x86_64-3.7/Orange/preprocess\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/preprocess/_discretize.c -o build/temp.linux-x86_64-3.7/Orange/preprocess/_discretize.o\ncreating build/lib.linux-x86_64-3.7/Orange/preprocess\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/preprocess/_discretize.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._relieff' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/preprocess/_relieff.cpp -o build/temp.linux-x86_64-3.7/Orange/preprocess/_relieff.o\nc++ -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/preprocess/_relieff.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.projection._som' extension\ncreating build/temp.linux-x86_64-3.7/Orange/projection\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/projection/_som.c -o build/temp.linux-x86_64-3.7/Orange/projection/_som.o\ncreating build/lib.linux-x86_64-3.7/Orange/projection\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/projection/_som.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so -> Orange/widgets/utils\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_io.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so -> Orange/projection\nrunning develop\nrunning egg_info\ncreating Orange3.egg-info\nwriting Orange3.egg-info/PKG-INFO\nwriting dependency_links to Orange3.egg-info/dependency_links.txt\nwriting entry points to Orange3.egg-info/entry_points.txt\nwriting requirements to Orange3.egg-info/requires.txt\nwriting top-level names to Orange3.egg-info/top_level.txt\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nreading manifest file 'Orange3.egg-info/SOURCES.txt'\nreading manifest template 'MANIFEST.in'\nadding license file 'LICENSE'\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nrunning build_ext\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so -> Orange/widgets/utils\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_io.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so -> Orange/projection\nCreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.37.0.dev0+371a179 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1\nProcessing dependencies for Orange3==3.37.0.dev0+371a179\nSearching for xlsxwriter\nReading https://pypi.org/simple/xlsxwriter/\nDownloading https://files.pythonhosted.org/packages/a7/ea/53d1fe468e63e092cf16e2c18d16f50c29851242f9dd12d6a66e0d7f0d02/XlsxWriter-3.2.0-py3-none-any.whl#sha256=ecfd5405b3e0e228219bcaf24c2ca0915e012ca9464a14048021d21a995d490e\nBest match: XlsxWriter 3.2.0\nProcessing XlsxWriter-3.2.0-py3-none-any.whl\nInstalling XlsxWriter-3.2.0-py3-none-any.whl to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages\nAdding XlsxWriter 3.2.0 to easy-install.pth file\nInstalling vba_extract.py script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/XlsxWriter-3.2.0-py3.7.egg\nSearching for xlrd>=1.2.0\nReading https://pypi.org/simple/xlrd/\nDownloading https://files.pythonhosted.org/packages/a6/0c/c2a72d51fe56e08a08acc85d13013558a2d793028ae7385448a6ccdfae64/xlrd-2.0.1-py2.py3-none-any.whl#sha256=6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd\nBest match: xlrd 2.0.1\nProcessing xlrd-2.0.1-py2.py3-none-any.whl\nInstalling xlrd-2.0.1-py2.py3-none-any.whl to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages\nAdding xlrd 2.0.1 to easy-install.pth file\nInstalling runxlrd.py script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/xlrd-2.0.1-py3.7.egg\nSearching for xgboost>=1.7.4\nReading https://pypi.org/simple/xgboost/\nDownloading https://files.pythonhosted.org/packages/48/bc/05d7db90d421c5e3d681a12fd1eb087e37bf2e9bbe2b105422d6319ecc92/xgboost-2.1.3-py3-none-manylinux2014_x86_64.whl#sha256=32a43526208fe676527c698cb852e0e9515e6d7294143780e476d335290a131b\nBest match: xgboost 2.1.3\nProcessing xgboost-2.1.3-py3-none-manylinux2014_x86_64.whl\nInstalling xgboost-2.1.3-py3-none-manylinux2014_x86_64.whl to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages\nAdding xgboost 2.1.3 to easy-install.pth file\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/xgboost-2.1.3-py3.7-linux-x86_64.egg\nSearching for serverfiles\nReading https://pypi.org/simple/serverfiles/\nDownloading https://files.pythonhosted.org/packages/33/36/794fe1c4ed29b0f56a26d72e956baf0285d8fe9e29faf36797a28a8ad45b/serverfiles-0.3.1.tar.gz#sha256=5e10fc32e758791e3735bc08bce2ed4702a83b1e45ab96c5d59cc8aeecfbebe1\nBest match: serverfiles 0.3.1\nProcessing serverfiles-0.3.1.tar.gz\nWriting /tmp/easy_install-oowdqd6x/serverfiles-0.3.1/setup.cfg\nRunning serverfiles-0.3.1/setup.py -q bdist_egg --dist-dir /tmp/easy_install-oowdqd6x/serverfiles-0.3.1/egg-dist-tmp-zl_3e83d\ncreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/serverfiles-0.3.1-py3.7.egg\nExtracting serverfiles-0.3.1-py3.7.egg to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages\nAdding serverfiles 0.3.1 to easy-install.pth file\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/serverfiles-0.3.1-py3.7.egg\nSearching for scipy>=1.9\nReading https://pypi.org/simple/scipy/\nDownloading https://files.pythonhosted.org/packages/d9/7b/2b8ac283cf32465ed08bc20a83d559fe7b174a484781702ba8accea001d6/scipy-1.15.0.tar.gz#sha256=300742e2cc94e36a2880ebe464a1c8b4352a7b0f3e36ec3d2ac006cdbe0219ac\nBest match: scipy 1.15.0\nProcessing scipy-1.15.0.tar.gz\nVerifying Orange installation...\n❌ Orange verification failed\nPython 3.7 installation failed, trying Python 3.8...\nAttempting installation with Python 3.8...\nrunning build_ext\nbuilding 'Orange.classification._simple_tree' extension\ncreating build/temp.linux-x86_64-cpython-38/Orange/classification\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/classification/_simple_tree.c -o build/temp.linux-x86_64-cpython-38/Orange/classification/_simple_tree.o\ncreating build/lib.linux-x86_64-cpython-38/Orange/classification\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/classification/_simple_tree.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/classification/_simple_tree.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.widgets.utils._grid_density' extension\ncreating build/temp.linux-x86_64-cpython-38/Orange/widgets/utils\nc++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/widgets/utils/_grid_density.cpp -o build/temp.linux-x86_64-cpython-38/Orange/widgets/utils/_grid_density.o\ncreating build/lib.linux-x86_64-cpython-38/Orange/widgets/utils\nc++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -pthread -shared build/temp.linux-x86_64-cpython-38/Orange/widgets/utils/_grid_density.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/widgets/utils/_grid_density.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.classification._tree_scorers' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/classification/_tree_scorers.c -o build/temp.linux-x86_64-cpython-38/Orange/classification/_tree_scorers.o\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/classification/_tree_scorers.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/classification/_tree_scorers.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.data._contingency' extension\ncreating build/temp.linux-x86_64-cpython-38/Orange/data\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/data/_contingency.c -o build/temp.linux-x86_64-cpython-38/Orange/data/_contingency.o\ncreating build/lib.linux-x86_64-cpython-38/Orange/data\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/data/_contingency.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/data/_contingency.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.data._io' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/data/_io.c -o build/temp.linux-x86_64-cpython-38/Orange/data/_io.o\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/data/_io.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/data/_io.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.data._valuecount' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/data/_valuecount.c -o build/temp.linux-x86_64-cpython-38/Orange/data/_valuecount.o\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/data/_valuecount.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/data/_valuecount.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.data._variable' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/data/_variable.c -o build/temp.linux-x86_64-cpython-38/Orange/data/_variable.o\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/data/_variable.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/data/_variable.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.distance._distance' extension\ncreating build/temp.linux-x86_64-cpython-38/Orange/distance\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/distance/_distance.c -o build/temp.linux-x86_64-cpython-38/Orange/distance/_distance.o\ncreating build/lib.linux-x86_64-cpython-38/Orange/distance\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/distance/_distance.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/distance/_distance.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._discretize' extension\ncreating build/temp.linux-x86_64-cpython-38/Orange/preprocess\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/preprocess/_discretize.c -o build/temp.linux-x86_64-cpython-38/Orange/preprocess/_discretize.o\ncreating build/lib.linux-x86_64-cpython-38/Orange/preprocess\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/preprocess/_discretize.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/preprocess/_discretize.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._relieff' extension\nc++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/preprocess/_relieff.cpp -o build/temp.linux-x86_64-cpython-38/Orange/preprocess/_relieff.o\nc++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -pthread -shared build/temp.linux-x86_64-cpython-38/Orange/preprocess/_relieff.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/preprocess/_relieff.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.projection._som' extension\ncreating build/temp.linux-x86_64-cpython-38/Orange/projection\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/projection/_som.c -o build/temp.linux-x86_64-cpython-38/Orange/projection/_som.o\ncreating build/lib.linux-x86_64-cpython-38/Orange/projection\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/projection/_som.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/projection/_som.cpython-38-x86_64-linux-gnu.so\ncopying build/lib.linux-x86_64-cpython-38/Orange/classification/_simple_tree.cpython-38-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-cpython-38/Orange/widgets/utils/_grid_density.cpython-38-x86_64-linux-gnu.so -> Orange/widgets/utils\ncopying build/lib.linux-x86_64-cpython-38/Orange/classification/_tree_scorers.cpython-38-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_contingency.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_io.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_valuecount.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_variable.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/distance/_distance.cpython-38-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-cpython-38/Orange/preprocess/_discretize.cpython-38-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-cpython-38/Orange/preprocess/_relieff.cpython-38-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-cpython-38/Orange/projection/_som.cpython-38-x86_64-linux-gnu.so -> Orange/projection\nrunning develop\nrunning egg_info\nwriting Orange3.egg-info/PKG-INFO\nwriting dependency_links to Orange3.egg-info/dependency_links.txt\nwriting entry points to Orange3.egg-info/entry_points.txt\nwriting requirements to Orange3.egg-info/requires.txt\nwriting top-level names to Orange3.egg-info/top_level.txt\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\nreading manifest file 'Orange3.egg-info/SOURCES.txt'\nreading manifest template 'MANIFEST.in'\nadding license file 'LICENSE'\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nrunning build_ext\ncopying build/lib.linux-x86_64-cpython-38/Orange/classification/_simple_tree.cpython-38-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-cpython-38/Orange/widgets/utils/_grid_density.cpython-38-x86_64-linux-gnu.so -> Orange/widgets/utils\ncopying build/lib.linux-x86_64-cpython-38/Orange/classification/_tree_scorers.cpython-38-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_contingency.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_io.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_valuecount.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_variable.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/distance/_distance.cpython-38-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-cpython-38/Orange/preprocess/_discretize.cpython-38-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-cpython-38/Orange/preprocess/_relieff.cpython-38-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-cpython-38/Orange/projection/_som.cpython-38-x86_64-linux-gnu.so -> Orange/projection\nCreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.37.0.dev0+371a179 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1\nProcessing dependencies for Orange3==3.37.0.dev0+371a179\nSearching for XlsxWriter==3.2.0\nBest match: XlsxWriter 3.2.0\nAdding XlsxWriter 3.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for xlrd==2.0.1\nBest match: xlrd 2.0.1\nAdding xlrd 2.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for xgboost==2.1.3\nBest match: xgboost 2.1.3\nAdding xgboost 2.1.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for serverfiles==0.3.1\nBest match: serverfiles 0.3.1\nAdding serverfiles 0.3.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for scipy==1.10.1\nBest match: scipy 1.10.1\nAdding scipy 1.10.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for scikit-learn==1.3.2\nBest match: scikit-learn 1.3.2\nAdding scikit-learn 1.3.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for requests==2.32.3\nBest match: requests 2.32.3\nAdding requests 2.32.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for qtconsole==5.6.1\nBest match: qtconsole 5.6.1\nAdding qtconsole 5.6.1 to easy-install.pth file\nInstalling jupyter-qtconsole script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for PyYAML==6.0.2\nBest match: PyYAML 6.0.2\nAdding PyYAML 6.0.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for python-louvain==0.16\nBest match: python-louvain 0.16\nAdding python-louvain 0.16 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for pyqtgraph==0.13.3\nBest match: pyqtgraph 0.13.3\nAdding pyqtgraph 0.13.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for pygments==2.19.1\nBest match: pygments 2.19.1\nAdding pygments 2.19.1 to easy-install.pth file\nInstalling pygmentize script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for pip==24.3.1\nBest match: pip 24.3.1\nAdding pip 24.3.1 to easy-install.pth file\nInstalling pip script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for pandas==2.0.3\nBest match: pandas 2.0.3\nAdding pandas 2.0.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for packaging==24.2\nBest match: packaging 24.2\nAdding packaging 24.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for orange-widget-base==4.24.0\nBest match: orange-widget-base 4.24.0\nAdding orange-widget-base 4.24.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for orange-canvas-core==0.2.2\nBest match: orange-canvas-core 0.2.2\nAdding orange-canvas-core 0.2.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for openpyxl==3.1.5\nBest match: openpyxl 3.1.5\nAdding openpyxl 3.1.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for openTSNE==1.0.1\nBest match: openTSNE 1.0.1\nAdding openTSNE 1.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for numpy==1.24.4\nBest match: numpy 1.24.4\nAdding numpy 1.24.4 to easy-install.pth file\nInstalling f2py script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\nInstalling f2py3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\nInstalling f2py3.8 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for networkx==3.1\nBest match: networkx 3.1\nAdding networkx 3.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for matplotlib==3.7.5\nBest match: matplotlib 3.7.5\nAdding matplotlib 3.7.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for keyrings.alt==5.0.2\nBest match: keyrings.alt 5.0.2\nAdding keyrings.alt 5.0.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for keyring==25.5.0\nBest match: keyring 25.5.0\nAdding keyring 25.5.0 to easy-install.pth file\nInstalling keyring script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for joblib==1.4.2\nBest match: joblib 1.4.2\nAdding joblib 1.4.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for httpx==0.28.1\nBest match: httpx 0.28.1\nAdding httpx 0.28.1 to easy-install.pth file\nInstalling httpx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for chardet==5.2.0\nBest match: chardet 5.2.0\nAdding chardet 5.2.0 to easy-install.pth file\nInstalling chardetect script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for catboost==1.2.7\nBest match: catboost 1.2.7\nAdding catboost 1.2.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for Bottleneck==1.4.0\nBest match: Bottleneck 1.4.0\nAdding Bottleneck 1.4.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for baycomp==1.0.3\nBest match: baycomp 1.0.3\nAdding baycomp 1.0.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for AnyQt==0.2.0\nBest match: AnyQt 0.2.0\nAdding AnyQt 0.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for nvidia-nccl-cu12==2.24.3\nBest match: nvidia-nccl-cu12 2.24.3\nAdding nvidia-nccl-cu12 2.24.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for threadpoolctl==3.5.0\nBest match: threadpoolctl 3.5.0\nAdding threadpoolctl 3.5.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for certifi==2024.12.14\nBest match: certifi 2024.12.14\nAdding certifi 2024.12.14 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for urllib3==2.2.3\nBest match: urllib3 2.2.3\nAdding urllib3 2.2.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for idna==3.10\nBest match: idna 3.10\nAdding idna 3.10 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for charset-normalizer==3.4.1\nBest match: charset-normalizer 3.4.1\nAdding charset-normalizer 3.4.1 to easy-install.pth file\nInstalling normalizer script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for QtPy==2.4.2\nBest match: QtPy 2.4.2\nAdding QtPy 2.4.2 to easy-install.pth file\nInstalling qtpy script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for ipykernel==6.29.5\nBest match: ipykernel 6.29.5\nAdding ipykernel 6.29.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for jupyter-client==8.6.3\nBest match: jupyter-client 8.6.3\nAdding jupyter-client 8.6.3 to easy-install.pth file\nInstalling jupyter-kernel script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\nInstalling jupyter-kernelspec script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\nInstalling jupyter-run script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for jupyter-core==5.7.2\nBest match: jupyter-core 5.7.2\nAdding jupyter-core 5.7.2 to easy-install.pth file\nInstalling jupyter script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\nInstalling jupyter-migrate script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\nInstalling jupyter-troubleshoot script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for traitlets==5.14.3\nBest match: traitlets 5.14.3\nAdding traitlets 5.14.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for tzdata==2024.2\nBest match: tzdata 2024.2\nAdding tzdata 2024.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for pytz==2024.2\nBest match: pytz 2024.2\nAdding pytz 2024.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for python-dateutil==2.9.0.post0\nBest match: python-dateutil 2.9.0.post0\nAdding python-dateutil 2.9.0.post0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for importlib-resources==6.4.5\nBest match: importlib-resources 6.4.5\nAdding importlib-resources 6.4.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for typing-extensions==4.12.2\nBest match: typing-extensions 4.12.2\nAdding typing-extensions 4.12.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/setuptools/_vendor\nSearching for importlib-metadata==8.5.0\nBest match: importlib-metadata 8.5.0\nAdding importlib-metadata 8.5.0 to easy-install.pth file\ndetected new path './setuptools/_vendor'\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for qasync==0.27.1\nBest match: qasync 0.27.1\nAdding qasync 0.27.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for dictdiffer==0.9.0\nBest match: dictdiffer 0.9.0\nAdding dictdiffer 0.9.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for requests-cache==1.2.1\nBest match: requests-cache 1.2.1\nAdding requests-cache 1.2.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for commonmark==0.9.1\nBest match: commonmark 0.9.1\nAdding commonmark 0.9.1 to easy-install.pth file\nInstalling cmark script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for docutils==0.20.1\nBest match: docutils 0.20.1\nAdding docutils 0.20.1 to easy-install.pth file\nInstalling docutils script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for et-xmlfile==2.0.0\nBest match: et-xmlfile 2.0.0\nAdding et-xmlfile 2.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for pyparsing==3.1.4\nBest match: pyparsing 3.1.4\nAdding pyparsing 3.1.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for pillow==10.4.0\nBest match: pillow 10.4.0\nAdding pillow 10.4.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for kiwisolver==1.4.7\nBest match: kiwisolver 1.4.7\nAdding kiwisolver 1.4.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for fonttools==4.55.3\nBest match: fonttools 4.55.3\nAdding fonttools 4.55.3 to easy-install.pth file\nInstalling fonttools script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for cycler==0.12.1\nBest match: cycler 0.12.1\nAdding cycler 0.12.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for contourpy==1.1.1\nBest match: contourpy 1.1.1\nAdding contourpy 1.1.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for jaraco.context==6.0.1\nBest match: jaraco.context 6.0.1\nAdding jaraco.context 6.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for jaraco.classes==3.4.0\nBest match: jaraco.classes 3.4.0\nAdding jaraco.classes 3.4.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for jeepney==0.8.0\nBest match: jeepney 0.8.0\nAdding jeepney 0.8.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for SecretStorage==3.3.3\nBest match: SecretStorage 3.3.3\nAdding SecretStorage 3.3.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for jaraco.functools==4.1.0\nBest match: jaraco.functools 4.1.0\nAdding jaraco.functools 4.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for httpcore==1.0.7\nBest match: httpcore 1.0.7\nAdding httpcore 1.0.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for anyio==4.5.2\nBest match: anyio 4.5.2\nAdding anyio 4.5.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for six==1.17.0\nBest match: six 1.17.0\nAdding six 1.17.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for plotly==5.24.1\nBest match: plotly 5.24.1\nAdding plotly 5.24.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for graphviz==0.20.3\nBest match: graphviz 0.20.3\nAdding graphviz 0.20.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for tornado==6.4.2\nBest match: tornado 6.4.2\nAdding tornado 6.4.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for pyzmq==26.2.0\nBest match: pyzmq 26.2.0\nAdding pyzmq 26.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for psutil==6.1.1\nBest match: psutil 6.1.1\nAdding psutil 6.1.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for nest-asyncio==1.6.0\nBest match: nest-asyncio 1.6.0\nAdding nest-asyncio 1.6.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for matplotlib-inline==0.1.7\nBest match: matplotlib-inline 0.1.7\nAdding matplotlib-inline 0.1.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for ipython==8.12.3\nBest match: ipython 8.12.3\nAdding ipython 8.12.3 to easy-install.pth file\nInstalling ipython script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\nInstalling ipython3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for debugpy==1.8.11\nBest match: debugpy 1.8.11\nAdding debugpy 1.8.11 to easy-install.pth file\nInstalling debugpy script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for comm==0.2.2\nBest match: comm 0.2.2\nAdding comm 0.2.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for platformdirs==4.3.6\nBest match: platformdirs 4.3.6\nAdding platformdirs 4.3.6 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for zipp==3.20.2\nBest match: zipp 3.20.2\nAdding zipp 3.20.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for url-normalize==1.4.3\nBest match: url-normalize 1.4.3\nAdding url-normalize 1.4.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for cattrs==24.1.2\nBest match: cattrs 24.1.2\nAdding cattrs 24.1.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for attrs==24.3.0\nBest match: attrs 24.3.0\nAdding attrs 24.3.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for backports.tarfile==1.2.0\nBest match: backports.tarfile 1.2.0\nbackports.tarfile 1.2.0 is already the active version in easy-install.pth\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/setuptools/_vendor\nSearching for more-itertools==10.5.0\nBest match: more-itertools 10.5.0\nAdding more-itertools 10.5.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for cryptography==44.0.0\nBest match: cryptography 44.0.0\nAdding cryptography 44.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for h11==0.14.0\nBest match: h11 0.14.0\nAdding h11 0.14.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for exceptiongroup==1.2.2\nBest match: exceptiongroup 1.2.2\nAdding exceptiongroup 1.2.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for sniffio==1.3.1\nBest match: sniffio 1.3.1\nAdding sniffio 1.3.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for tenacity==9.0.0\nBest match: tenacity 9.0.0\nAdding tenacity 9.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for pexpect==4.9.0\nBest match: pexpect 4.9.0\nAdding pexpect 4.9.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for stack-data==0.6.3\nBest match: stack-data 0.6.3\nAdding stack-data 0.6.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for prompt-toolkit==3.0.48\nBest match: prompt-toolkit 3.0.48\nAdding prompt-toolkit 3.0.48 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for pickleshare==0.7.5\nBest match: pickleshare 0.7.5\nAdding pickleshare 0.7.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for jedi==0.19.2\nBest match: jedi 0.19.2\nAdding jedi 0.19.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for decorator==5.1.1\nBest match: decorator 5.1.1\nAdding decorator 5.1.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for backcall==0.2.0\nBest match: backcall 0.2.0\nAdding backcall 0.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for cffi==1.17.1\nBest match: cffi 1.17.1\nAdding cffi 1.17.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for ptyprocess==0.7.0\nBest match: ptyprocess 0.7.0\nAdding ptyprocess 0.7.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for pure-eval==0.2.3\nBest match: pure-eval 0.2.3\nAdding pure-eval 0.2.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for asttokens==3.0.0\nBest match: asttokens 3.0.0\nAdding asttokens 3.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for executing==2.1.0\nBest match: executing 2.1.0\nAdding executing 2.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for wcwidth==0.2.13\nBest match: wcwidth 0.2.13\nAdding wcwidth 0.2.13 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for parso==0.8.4\nBest match: parso 0.8.4\nAdding parso 0.8.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nSearching for pycparser==2.22\nBest match: pycparser 2.22\nAdding pycparser 2.22 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages\nFinished processing dependencies for Orange3==3.37.0.dev0+371a179\nVerifying Orange installation...\n✅ Orange installation successful!\nSuccessfully installed orange using Python 3.8\n",
    "setup_res_stderr": "Using CPython 3.7.9\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 18 packages in 185ms\nPrepared 18 packages in 17ms\nInstalled 18 packages in 122ms\n + cython==0.29.37\n + exceptiongroup==1.2.2\n + importlib-metadata==6.7.0\n + iniconfig==2.0.0\n + numpy==1.17.5\n + packaging==24.0\n + pluggy==1.2.0\n + pyqt5==5.15.10\n + pyqt5-qt5==5.15.16\n + pyqt5-sip==12.13.0\n + pyqtwebengine==5.15.6\n + pyqtwebengine-qt5==5.15.16\n + pytest==7.4.4\n + setuptools==59.8.0\n + tomli==2.0.1\n + typing-extensions==4.7.1\n + wheel==0.42.0\n + zipp==3.15.0\n  × No solution found when resolving dependencies:\n  ╰─▶ Because the current Python version (3.7.9) does not satisfy Python>=3.8 and all of:\n          pandas>=1.4.0,<=1.4.4\n          pandas>=1.5.1,<=1.5.3\n          pandas>=2.0.1,<=2.0.3\n      depend on Python>=3.8, we can conclude that all of:\n          pandas>=1.4.0,<=1.4.4\n          pandas>=1.5.1,<=1.5.3\n          pandas>=2.0.1,<=2.0.3\n       cannot be used.\n      And because only the following versions of pandas are available:\n          pandas<=1.4.0\n          pandas==1.4.1\n          pandas==1.4.2\n          pandas==1.4.3\n          pandas==1.4.4\n          pandas==1.5.0\n          pandas==1.5.1\n          pandas==1.5.2\n          pandas==1.5.3\n          pandas==2.0.0\n          pandas==2.0.1\n          pandas==2.0.2\n          pandas==2.0.3\n          pandas==2.1.0\n          pandas==2.1.1\n          pandas==2.1.2\n          pandas==2.1.3\n          pandas==2.1.4\n          pandas==2.2.0\n          pandas==2.2.1\n          pandas==2.2.2\n          pandas==2.2.3\n      we can conclude that all of:\n          pandas>=1.4.0,<1.5.0\n          pandas>1.5.0,<2.0.0\n          pandas>2.0.0,<2.1.0\n       cannot be used. (1)\n\n      Because the current Python version (3.7.9) does not satisfy Python>=3.9 and pandas>=2.1.0 depends on Python>=3.9, we can\n      conclude that pandas>=2.1.0 cannot be used.\n      And because we know from (1) that all of:\n          pandas>=1.4.0,<1.5.0\n          pandas>1.5.0,<2.0.0\n          pandas>2.0.0,<2.1.0\n       cannot be used, we can conclude that all of:\n          pandas>=1.4.0,<1.5.0\n          pandas>1.5.0,<2.0.0\n          pandas>2.0.0\n       cannot be used.\n      And because you require one of:\n          pandas>=1.4.0,<1.5.0\n          pandas>1.5.0,<2.0.0\n          pandas>2.0.0\n      we can conclude that your requirements are unsatisfiable.\n\n      hint: Pre-releases are available for `pandas` in the requested range (e.g., 2.2.0rc0), but pre-releases weren't enabled\n      (try: `--prerelease=allow`)\n  × No solution found when resolving dependencies:\n  ╰─▶ Because the current Python version (3.7.9) does not satisfy Python>=3.8 and pyqtgraph>=0.13.1,<=0.13.3 depends on\n      Python>=3.8, we can conclude that pyqtgraph>=0.13.1,<=0.13.3 cannot be used.\n      And because only the following versions of pyqtgraph are available:\n          pyqtgraph<=0.13.1\n          pyqtgraph==0.13.2\n          pyqtgraph==0.13.3\n          pyqtgraph==0.13.4\n          pyqtgraph==0.13.5\n          pyqtgraph==0.13.6\n          pyqtgraph==0.13.7\n      we can conclude that pyqtgraph>=0.13.1,<0.13.4 cannot be used. (1)\n\n      Because the current Python version (3.7.9) does not satisfy Python>=3.9 and pyqtgraph>=0.13.4 depends on Python>=3.9, we\n      can conclude that pyqtgraph>=0.13.4 cannot be used.\n      And because we know from (1) that pyqtgraph>=0.13.1,<0.13.4 cannot be used, we can conclude that pyqtgraph>=0.13.1 cannot\n      be used.\n      And because you require pyqtgraph>=0.13.1, we can conclude that your requirements are unsatisfiable.\nResolved 2 packages in 5ms\nInstalled 2 packages in 10ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nResolved 5 packages in 23ms\nInstalled 4 packages in 122ms\n + joblib==1.3.2\n + scikit-learn==1.0.2\n + scipy==1.7.3\n + threadpoolctl==3.1.0\nOrange/classification/_simple_tree.c: In function ‘gain_ratio_c’:\nOrange/classification/_simple_tree.c:63:9: warning: implicit declaration of function ‘qsort_r’; did you mean ‘qsort’? [-Wimplicit-function-declaration]\n   63 |         qsort_r(base, nel, size, compar, thunk)\n      |         ^~~~~~~\nOrange/classification/_simple_tree.c:147:9: note: in expansion of macro ‘QSORT_R_FUNC’\n  147 |         QSORT_R_FUNC(examples, size, sizeof(struct Example), (void*) &compar_attr, compar_examples);\n      |         ^~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/classification/_tree_scorers.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_2find_threshold_entropy’:\nOrange/classification/_tree_scorers.c:3426:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 3426 |   __pyx_t_7 = ((__pyx_v_N <= __pyx_v_min_leaf) != 0);\n      |                           ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_6find_threshold_MSE’:\nOrange/classification/_tree_scorers.c:4872:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 4872 |   __pyx_t_1 = ((__pyx_v_N <= __pyx_v_min_leaf) != 0);\n      |                           ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_8find_binarization_MSE’:\nOrange/classification/_tree_scorers.c:5618:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5618 |           for (__pyx_t_19 = 0; __pyx_t_19 < __pyx_t_18; __pyx_t_19+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:5739:39: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5739 |           __pyx_t_20 = ((__pyx_v_left >= __pyx_v_min_leaf) != 0);\n      |                                       ^~\nOrange/classification/_tree_scorers.c:5745:53: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5745 |           __pyx_t_20 = (((__pyx_v_N - __pyx_v_left) >= __pyx_v_min_leaf) != 0);\n      |                                                     ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_14compute_predictions_csr’:\nOrange/classification/_tree_scorers.c:7206:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7206 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_16compute_predictions_csc’:\nOrange/classification/_tree_scorers.c:7863:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7863 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:7923:150: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n 7923 |               __pyx_t_16 = (((*((__pyx_t_5numpy_int32_t const  *) ( /* dim=0 */ (__pyx_v_indices.data + __pyx_t_15 * __pyx_v_indices.strides[0]) ))) != __pyx_v_i) != 0);\n      |                                                                                                                                                      ^~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_contingency.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/data/_io.c: In function ‘__pyx_pf_6Orange_4data_3_io_sparse_read_float.constprop’:\nOrange/data/_io.c:4120:43: warning: ‘__pyx_v_decs’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4120 |           __pyx_v_value = ((__pyx_v_value * __pyx_v_decs) + (__pyx_v_c & 0xf));\n      |                            ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/data/_io.c:4318:24: warning: ‘__pyx_v_value’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4318 |           __pyx_t_10 = PyFloat_FromDouble(__pyx_v_value); if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 223, __pyx_L5_error)\n      |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:683:40: warning: ‘__pyx_v_attr_index’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n  683 |   #define PyInt_FromLong               PyLong_FromLong\n      |                                        ^~~~~~~~~~~~~~~\nOrange/data/_io.c:1590:7: note: ‘__pyx_v_attr_index’ was declared here\n 1590 |   int __pyx_v_attr_index;\n      |       ^~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:3183:25: warning: ‘__pyx_v_atomp’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3183 |           __pyx_v_atomp = (__pyx_v_atomp + 1);\n      |           ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_valuecount.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/distance/_distance.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_discretize.c:774:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_relieff.cpp:790:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/projection/_som.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_5update_hex’:\nOrange/projection/_som.c:3944:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3944 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3943:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3943 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pf_6Orange_10projection_4_som_8update_sparse.constprop’:\nOrange/projection/_som.c:5094:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5094 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5093:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5093 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_1get_winners’:\nOrange/projection/_som.c:3244:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3244 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_24, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2892:7: note: ‘__pyx_v_win_y’ was declared here\n 2892 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3233:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3233 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_24, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2891:7: note: ‘__pyx_v_win_x’ was declared here\n 2891 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pf_6Orange_10projection_4_som_10update_sparse_hex.constprop’:\nOrange/projection/_som.c:5842:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5842 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5841:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5841 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_7get_winners_sparse’:\nOrange/projection/_som.c:4871:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4871 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_28, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4389:7: note: ‘__pyx_v_win_y’ was declared here\n 4389 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4860:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4860 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_28, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4388:7: note: ‘__pyx_v_win_x’ was declared here\n 4388 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_3update’:\nOrange/projection/_som.c:3438:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3438 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/setuptools/command/easy_install.py:159: EasyInstallDeprecationWarning: easy_install command is deprecated. Use build and pip and other standards-based tools.\n  EasyInstallDeprecationWarning,\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/setuptools/command/install.py:37: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.\n  setuptools.SetuptoolsDeprecationWarning,\nwarning: no files found matching 'README*' under directory 'Orange'\nwarning: no files found matching 'LICENSE' under directory 'Orange'\nwarning: no files found matching '*.basket' under directory 'Orange/datasets'\nwarning: no files found matching '*.svg' under directory 'Orange/canvas'\nwarning: no files found matching '*.js' under directory 'Orange/widgets'\nwarning: no files found matching '*.css' under directory 'Orange/widgets'\nwarning: no files found matching '*.html' under directory 'Orange/widgets'\nwarning: no files found matching '*.svg' under directory 'distribute'\nwarning: no files found matching 'COPYING'\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/pkg_resources/__init__.py:119: PkgResourcesDeprecationWarning:  is an invalid version and will not be supported in a future release\n  PkgResourcesDeprecationWarning,\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/setuptools/command/install.py:37: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.\n  setuptools.SetuptoolsDeprecationWarning,\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.7/site-packages/pkg_resources/__init__.py:119: PkgResourcesDeprecationWarning:  is an invalid version and will not be supported in a future release\n  PkgResourcesDeprecationWarning,\nerror: Couldn't find a setup script in /tmp/easy_install-7lk85mnw/scipy-1.15.0.tar.gz\nUsing CPython 3.8.20\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 15 packages in 107ms\nPrepared 15 packages in 1ms\nInstalled 15 packages in 73ms\n + cython==0.29.37\n + exceptiongroup==1.2.2\n + iniconfig==2.0.0\n + numpy==1.24.4\n + packaging==24.2\n + pluggy==1.5.0\n + pyqt5==5.15.11\n + pyqt5-qt5==5.15.16\n + pyqt5-sip==12.15.0\n + pyqtwebengine==5.15.7\n + pyqtwebengine-qt5==5.15.16\n + pytest==8.3.4\n + setuptools==75.3.0\n + tomli==2.2.1\n + wheel==0.45.1\nResolved 64 packages in 164ms\nInstalled 61 packages in 823ms\n + anyio==4.5.2\n + backports-tarfile==1.2.0\n + baycomp==1.0.3\n + bottleneck==1.4.0\n + catboost==1.2.7\n + certifi==2024.12.14\n + cffi==1.17.1\n + chardet==5.2.0\n + charset-normalizer==3.4.1\n + contourpy==1.1.1\n + cryptography==44.0.0\n + cycler==0.12.1\n + et-xmlfile==2.0.0\n + fonttools==4.55.3\n + graphviz==0.20.3\n + h11==0.14.0\n + httpcore==1.0.7\n + httpx==0.28.1\n + idna==3.10\n + importlib-metadata==8.5.0\n + importlib-resources==6.4.5\n + jaraco-classes==3.4.0\n + jaraco-context==6.0.1\n + jaraco-functools==4.1.0\n + jeepney==0.8.0\n + joblib==1.4.2\n + keyring==25.5.0\n + keyrings-alt==5.0.2\n + kiwisolver==1.4.7\n + matplotlib==3.7.5\n + more-itertools==10.5.0\n + networkx==3.1\n + nvidia-nccl-cu12==2.24.3\n + openpyxl==3.1.5\n + opentsne==1.0.1\n + pandas==2.0.3\n + pillow==10.4.0\n + pip==24.3.1\n + plotly==5.24.1\n + pycparser==2.22\n + pyparsing==3.1.4\n + python-dateutil==2.9.0.post0\n + python-louvain==0.16\n + pytz==2024.2\n + pyyaml==6.0.2\n + requests==2.32.3\n + scikit-learn==1.3.2\n + scipy==1.10.1\n + secretstorage==3.3.3\n + serverfiles==0.3.1\n + six==1.17.0\n + sniffio==1.3.1\n + tenacity==9.0.0\n + threadpoolctl==3.5.0\n + typing-extensions==4.12.2\n + tzdata==2024.2\n + urllib3==2.2.3\n + xgboost==2.1.3\n + xlrd==2.0.1\n + xlsxwriter==3.2.0\n + zipp==3.20.2\nResolved 63 packages in 123ms\nInstalled 41 packages in 177ms\n + anyqt==0.2.0\n + asttokens==3.0.0\n + attrs==24.3.0\n + backcall==0.2.0\n + cattrs==24.1.2\n + comm==0.2.2\n + commonmark==0.9.1\n + debugpy==1.8.11\n + decorator==5.1.1\n + dictdiffer==0.9.0\n + docutils==0.20.1\n + executing==2.1.0\n + ipykernel==6.29.5\n + ipython==8.12.3\n + jedi==0.19.2\n + jupyter-client==8.6.3\n + jupyter-core==5.7.2\n + matplotlib-inline==0.1.7\n + nest-asyncio==1.6.0\n + orange-canvas-core==0.2.2\n + orange-widget-base==4.24.0\n + parso==0.8.4\n + pexpect==4.9.0\n + pickleshare==0.7.5\n + platformdirs==4.3.6\n + prompt-toolkit==3.0.48\n + psutil==6.1.1\n + ptyprocess==0.7.0\n + pure-eval==0.2.3\n + pygments==2.19.1\n + pyqtgraph==0.13.3\n + pyzmq==26.2.0\n + qasync==0.27.1\n + qtconsole==5.6.1\n + qtpy==2.4.2\n + requests-cache==1.2.1\n + stack-data==0.6.3\n + tornado==6.4.2\n + traitlets==5.14.3\n + url-normalize==1.4.3\n + wcwidth==0.2.13\nResolved 2 packages in 2ms\nInstalled 2 packages in 4ms\n + psycopg2==2.9.10\n + pymssql==2.3.2\nOrange/classification/_simple_tree.c: In function ‘gain_ratio_c’:\nOrange/classification/_simple_tree.c:63:9: warning: implicit declaration of function ‘qsort_r’; did you mean ‘qsort’? [-Wimplicit-function-declaration]\n   63 |         qsort_r(base, nel, size, compar, thunk)\n      |         ^~~~~~~\nOrange/classification/_simple_tree.c:147:9: note: in expansion of macro ‘QSORT_R_FUNC’\n  147 |         QSORT_R_FUNC(examples, size, sizeof(struct Example), (void*) &compar_attr, compar_examples);\n      |         ^~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/classification/_tree_scorers.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_2find_threshold_entropy’:\nOrange/classification/_tree_scorers.c:3426:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 3426 |   __pyx_t_7 = ((__pyx_v_N <= __pyx_v_min_leaf) != 0);\n      |                           ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_6find_threshold_MSE’:\nOrange/classification/_tree_scorers.c:4872:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 4872 |   __pyx_t_1 = ((__pyx_v_N <= __pyx_v_min_leaf) != 0);\n      |                           ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_8find_binarization_MSE’:\nOrange/classification/_tree_scorers.c:5618:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5618 |           for (__pyx_t_19 = 0; __pyx_t_19 < __pyx_t_18; __pyx_t_19+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:5739:39: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5739 |           __pyx_t_20 = ((__pyx_v_left >= __pyx_v_min_leaf) != 0);\n      |                                       ^~\nOrange/classification/_tree_scorers.c:5745:53: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5745 |           __pyx_t_20 = (((__pyx_v_N - __pyx_v_left) >= __pyx_v_min_leaf) != 0);\n      |                                                     ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_14compute_predictions_csr’:\nOrange/classification/_tree_scorers.c:7206:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7206 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_16compute_predictions_csc’:\nOrange/classification/_tree_scorers.c:7863:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7863 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:7923:150: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n 7923 |               __pyx_t_16 = (((*((__pyx_t_5numpy_int32_t const  *) ( /* dim=0 */ (__pyx_v_indices.data + __pyx_t_15 * __pyx_v_indices.strides[0]) ))) != __pyx_v_i) != 0);\n      |                                                                                                                                                      ^~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/data/_contingency.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/data/_io.c: In function ‘__pyx_pf_6Orange_4data_3_io_sparse_read_float.constprop’:\nOrange/data/_io.c:4120:43: warning: ‘__pyx_v_decs’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4120 |           __pyx_v_value = ((__pyx_v_value * __pyx_v_decs) + (__pyx_v_c & 0xf));\n      |                            ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/data/_io.c:4318:24: warning: ‘__pyx_v_value’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4318 |           __pyx_t_10 = PyFloat_FromDouble(__pyx_v_value); if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 223, __pyx_L5_error)\n      |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:683:40: warning: ‘__pyx_v_attr_index’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n  683 |   #define PyInt_FromLong               PyLong_FromLong\n      |                                        ^~~~~~~~~~~~~~~\nOrange/data/_io.c:1590:7: note: ‘__pyx_v_attr_index’ was declared here\n 1590 |   int __pyx_v_attr_index;\n      |       ^~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:3183:25: warning: ‘__pyx_v_atomp’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3183 |           __pyx_v_atomp = (__pyx_v_atomp + 1);\n      |           ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/data/_valuecount.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/distance/_distance.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/preprocess/_discretize.c:774:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/preprocess/_relieff.cpp:790:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/projection/_som.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_5update_hex’:\nOrange/projection/_som.c:3944:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3944 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3943:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3943 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pf_6Orange_10projection_4_som_8update_sparse.constprop’:\nOrange/projection/_som.c:5094:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5094 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5093:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5093 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_3update’:\nOrange/projection/_som.c:3438:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3438 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_1get_winners’:\nOrange/projection/_som.c:3244:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3244 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_24, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2892:7: note: ‘__pyx_v_win_y’ was declared here\n 2892 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3233:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3233 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_24, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2891:7: note: ‘__pyx_v_win_x’ was declared here\n 2891 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pf_6Orange_10projection_4_som_10update_sparse_hex.constprop’:\nOrange/projection/_som.c:5842:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5842 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5841:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5841 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_7get_winners_sparse’:\nOrange/projection/_som.c:4871:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4871 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_28, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4389:7: note: ‘__pyx_v_win_y’ was declared here\n 4389 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4860:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4860 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_28, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4388:7: note: ‘__pyx_v_win_x’ was declared here\n 4388 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/setuptools/_distutils/dist.py:261: UserWarning: Unknown distribution option: 'test_suite'\n  warnings.warn(msg)\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/setuptools/_distutils/dist.py:261: UserWarning: Unknown distribution option: 'test_suite'\n  warnings.warn(msg)\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/setuptools/command/develop.py:41: EasyInstallDeprecationWarning: easy_install command is deprecated.\n!!\n\n        ********************************************************************************\n        Please avoid running ``setup.py`` and ``easy_install``.\n        Instead, use pypa/build, pypa/installer or other\n        standards-based tools.\n\n        See https://github.com/pypa/setuptools/issues/917 for details.\n        ********************************************************************************\n\n!!\n  easy_install.initialize_options(self)\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1/.venv/lib/python3.8/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.\n!!\n\n        ********************************************************************************\n        Please avoid running ``setup.py`` directly.\n        Instead, use pypa/build, pypa/installer or other\n        standards-based tools.\n\n        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html for details.\n        ********************************************************************************\n\n!!\n  self.initialize_options()\nwarning: no files found matching 'README*' under directory 'Orange'\nwarning: no files found matching 'LICENSE' under directory 'Orange'\nwarning: no files found matching '*.basket' under directory 'Orange/datasets'\nwarning: no files found matching '*.svg' under directory 'Orange/canvas'\nwarning: no files found matching '*.js' under directory 'Orange/widgets'\nwarning: no files found matching '*.css' under directory 'Orange/widgets'\nwarning: no files found matching '*.html' under directory 'Orange/widgets'\nwarning: no files found matching '*.svg' under directory 'distribute'\nwarning: no files found matching 'COPYING'\n",
    "new_commit_res_code": 0,
    "new_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.8.20, pytest-8.3.4, pluggy-1.5.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1\nconfigfile: pyproject.toml\nplugins: anyio-4.5.2, typeguard-4.3.0\ncollected 35 items\n\nr2e_tests/test_1.py sss................................                  [100%]\n\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWSOM::test_attr_color_change\nPASSED r2e_tests/test_1.py::TestOWSOM::test_auto_compute_dimensions\nPASSED r2e_tests/test_1.py::TestOWSOM::test_cell_sizes\nPASSED r2e_tests/test_1.py::TestOWSOM::test_colored_circles_with_constant\nPASSED r2e_tests/test_1.py::TestOWSOM::test_colored_circles_with_missing_values\nPASSED r2e_tests/test_1.py::TestOWSOM::test_diff_color_diff_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_diff_color_same_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_get_color_column\nPASSED r2e_tests/test_1.py::TestOWSOM::test_image_export\nPASSED r2e_tests/test_1.py::TestOWSOM::test_invalidated\nPASSED r2e_tests/test_1.py::TestOWSOM::test_make_domain_without_class_vars\nPASSED r2e_tests/test_1.py::TestOWSOM::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_missing_all_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_missing_one_row_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_missing_some_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_modified_info\nPASSED r2e_tests/test_1.py::TestOWSOM::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWSOM::test_on_selection_change\nPASSED r2e_tests/test_1.py::TestOWSOM::test_on_selection_change_on_empty\nPASSED r2e_tests/test_1.py::TestOWSOM::test_output_columns\nPASSED r2e_tests/test_1.py::TestOWSOM::test_output_selection\nPASSED r2e_tests/test_1.py::TestOWSOM::test_pie_charts\nPASSED r2e_tests/test_1.py::TestOWSOM::test_redraw_grid\nPASSED r2e_tests/test_1.py::TestOWSOM::test_requires_continuous\nPASSED r2e_tests/test_1.py::TestOWSOM::test_run_actual_optimization\nPASSED r2e_tests/test_1.py::TestOWSOM::test_same_color_diff_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_same_color_same_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_send_report\nPASSED r2e_tests/test_1.py::TestOWSOM::test_single_attribute\nPASSED r2e_tests/test_1.py::TestOWSOM::test_single_row_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_sparse_data\nPASSED r2e_tests/test_1.py::TestComputeValues::test_eq_hash\nSKIPPED [1] .venv/lib/python3.8/site-packages/orangewidget/tests/base.py:244: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.8/site-packages/orangewidget/tests/base.py:238: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.8/site-packages/orangewidget/tests/base.py:250: msg base class not tested as .widget was not set\n======================== 32 passed, 3 skipped in 7.96s =========================\nWidgets left: 0    Max widgets: 2605 \n\n",
    "new_commit_res_stderr": "",
    "old_commit_res_code": 1,
    "old_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.8.20, pytest-8.3.4, pluggy-1.5.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_371a1793bec28936d4a5e4dd0be7de6fb270c8b1\nconfigfile: pyproject.toml\nplugins: anyio-4.5.2, typeguard-4.3.0\ncollected 35 items\n\nr2e_tests/test_1.py sss..........F.....................                  [100%]\n\n=================================== FAILURES ===================================\n________________ TestOWSOM.test_make_domain_without_class_vars _________________\n\nself = <r2e_tests.test_1.TestOWSOM testMethod=test_make_domain_without_class_vars>\n\n    def test_make_domain_without_class_vars(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        widget.data.domain.class_vars = None\n>       widget.update_output()\n\nr2e_tests/test_1.py:730: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nOrange/widgets/unsupervised/owsom.py:1037: in update_output\n    sel_domain = make_domain((\"No\", \"Yes\"), 0, 0)\nOrange/widgets/unsupervised/owsom.py:1024: in make_domain\n    return add_columns(self.data.domain, (), class_vars, metas)\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\ndomain = [sepal length, sepal width, petal length, petal width]\nattributes = (ContinuousVariable(name='sepal length', number_of_decimals=1), ContinuousVariable(name='sepal width', number_of_decim...nuousVariable(name='petal length', number_of_decimals=1), ContinuousVariable(name='petal width', number_of_decimals=1))\nclass_vars = DiscreteVariable(name='Selected', values=('No', 'Yes'), compute_value=<Orange.widgets.unsupervised.owsom.GetGroups object at 0x73d5b6335790>)\nmetas = (DiscreteVariable(name='som_cell', values=('r1c1', 'r1c2', 'r1c3', 'r1c4', 'r1c5', 'r1c6', 'r1c7', 'r1c8', 'r2c1', 'r2...or', number_of_decimals=3, compute_value=<Orange.widgets.unsupervised.owsom.SomErrorCompute object at 0x73d5b6335a30>))\n\n    def add_columns(domain, attributes=(), class_vars=(), metas=()):\n        \"\"\"Construct a new domain with new columns added to the specified place\n    \n        Parameters\n        ----------\n        domain : Domain\n            source domain\n        attributes\n            list of variables to append to attributes from source domain\n        class_vars\n            list of variables to append to class_vars from source domain\n        metas\n            list of variables to append to metas from source domain\n    \n        Returns\n        -------\n        Domain\n        \"\"\"\n        attributes = domain.attributes + tuple(attributes)\n>       class_vars = domain.class_vars + tuple(class_vars)\nE       TypeError: 'DiscreteVariable' object is not iterable\n\nOrange/widgets/utils/annotated_data.py:33: TypeError\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWSOM::test_attr_color_change\nPASSED r2e_tests/test_1.py::TestOWSOM::test_auto_compute_dimensions\nPASSED r2e_tests/test_1.py::TestOWSOM::test_cell_sizes\nPASSED r2e_tests/test_1.py::TestOWSOM::test_colored_circles_with_constant\nPASSED r2e_tests/test_1.py::TestOWSOM::test_colored_circles_with_missing_values\nPASSED r2e_tests/test_1.py::TestOWSOM::test_diff_color_diff_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_diff_color_same_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_get_color_column\nPASSED r2e_tests/test_1.py::TestOWSOM::test_image_export\nPASSED r2e_tests/test_1.py::TestOWSOM::test_invalidated\nPASSED r2e_tests/test_1.py::TestOWSOM::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_missing_all_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_missing_one_row_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_missing_some_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_modified_info\nPASSED r2e_tests/test_1.py::TestOWSOM::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWSOM::test_on_selection_change\nPASSED r2e_tests/test_1.py::TestOWSOM::test_on_selection_change_on_empty\nPASSED r2e_tests/test_1.py::TestOWSOM::test_output_columns\nPASSED r2e_tests/test_1.py::TestOWSOM::test_output_selection\nPASSED r2e_tests/test_1.py::TestOWSOM::test_pie_charts\nPASSED r2e_tests/test_1.py::TestOWSOM::test_redraw_grid\nPASSED r2e_tests/test_1.py::TestOWSOM::test_requires_continuous\nPASSED r2e_tests/test_1.py::TestOWSOM::test_run_actual_optimization\nPASSED r2e_tests/test_1.py::TestOWSOM::test_same_color_diff_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_same_color_same_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_send_report\nPASSED r2e_tests/test_1.py::TestOWSOM::test_single_attribute\nPASSED r2e_tests/test_1.py::TestOWSOM::test_single_row_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_sparse_data\nPASSED r2e_tests/test_1.py::TestComputeValues::test_eq_hash\nSKIPPED [1] .venv/lib/python3.8/site-packages/orangewidget/tests/base.py:244: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.8/site-packages/orangewidget/tests/base.py:238: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.8/site-packages/orangewidget/tests/base.py:250: msg base class not tested as .widget was not set\nFAILED r2e_tests/test_1.py::TestOWSOM::test_make_domain_without_class_vars - ...\n=================== 1 failed, 31 passed, 3 skipped in 5.13s ====================\nWidgets left: 0    Max widgets: 2605 \n\n",
    "old_commit_res_stderr": ""
}
Orange/widgets/unsupervised/owsom.py Orange/widgets/unsupervised/tests/test_owsom.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** TypeError When `class_vars` Is None in OWSOM Widget

**Description:**
When the `class_vars` attribute of the data domain is set to `None`, updating the widget's output raises a `TypeError`. This occurs because the code attempts to iterate over a `DiscreteVariable` object, which is not iterable.

**Example Code:**
```python
widget.data.domain.class_vars = None
widget.update_output()
```

**Expected Behavior:**
The widget should handle the case where `class_vars` is `None` without raising an error, allowing the output to be updated correctly.

**Actual Behavior:**
A `TypeError` is raised with the message:
```
TypeError: 'DiscreteVariable' object is not iterable
```
[/ISSUE]
--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.

--- BEGIN FILE ---
```
### Orange/widgets/unsupervised/owsom.py
from collections import defaultdict, namedtuple
from contextlib import contextmanager
from typing import Optional, Union
from xml.sax.saxutils import escape

import numpy as np

from AnyQt.QtCore import Qt, QRectF, pyqtSignal as Signal, QObject, QThread, \
    pyqtSlot as Slot
from AnyQt.QtGui import QTransform, QPen, QBrush, QColor, QPainter, \
    QPainterPath, QKeyEvent
from AnyQt.QtWidgets import \
    QGraphicsView, QGraphicsScene, \
    QGraphicsItem, QGraphicsRectItem, QGraphicsItemGroup, QSizePolicy, \
    QGraphicsPathItem

from Orange.widgets.utils.signals import lazy_table_transform

from Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable
from Orange.data.util import array_equal, SharedComputeValue, get_unique_names
from Orange.preprocess import decimal_binnings, time_binnings
from Orange.projection.som import SOM

from Orange.widgets import gui
from Orange.widgets.utils.localization import pl
from Orange.widgets.widget import OWWidget, Msg, Input, Output
from Orange.widgets.settings import \
    DomainContextHandler, ContextSetting, Setting
from Orange.widgets.utils.itemmodels import DomainModel
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.annotated_data import \
    add_columns, group_values, \
    ANNOTATED_DATA_SIGNAL_NAME, ANNOTATED_DATA_FEATURE_NAME
from Orange.widgets.utils.colorpalettes import \
    BinnedContinuousPalette, LimitedDiscretePalette, DiscretePalette
from Orange.widgets.visualize.utils import CanvasRectangle, CanvasText
from Orange.widgets.visualize.utils.plotutils import wrap_legend_items


sqrt3_2 = np.sqrt(3) / 2


class SomView(QGraphicsView):
    SelectionSet, SelectionNewGroup, SelectionAddToGroup, SelectionRemove \
        = range(4)
    selection_changed = Signal(np.ndarray, int)
    selection_moved = Signal(QKeyEvent)
    selection_mark_changed = Signal(np.ndarray)

    def __init__(self, scene):
        super().__init__(scene)
        self.__button_down_pos = None
        self.size_x = self.size_y = 1
        self.hexagonal = True

    def set_dimensions(self, size_x, size_y, hexagonal):
        self.size_x = size_x
        self.size_y = size_y
        self.hexagonal = hexagonal

    def _get_marked_cells(self, event):
        x0, y0 = self.__button_down_pos.x(), self.__button_down_pos.y()
        pos = self.mapToScene(event.pos())
        x1, y1 = pos.x(), pos.y()
        x0, x1 = sorted((x0, x1))
        y0, y1 = sorted((y0, y1))
        selection = np.zeros((self.size_x, self.size_y), dtype=bool)
        if self.hexagonal:
            y0 = max(0, int(y0 / sqrt3_2 + 0.5))
            y1 = min(self.size_y, int(np.ceil(y1 / sqrt3_2 + 0.5)))
            for y in range(y0, y1):
                if x1 < 0:
                    continue
                x0_ = max(0, int(x0 + 0.5 - (y % 2) / 2))
                x1_ = min(self.size_x - y % 2,
                          int(np.ceil(x1 + 0.5 - (y % 2) / 2)))
                selection[x0_:x1_, y] = True
        elif not(x1 < -0.5 or x0 > self.size_x - 0.5
                 or y1 < -0.5 or y0 > self.size_y - 0.5):

            def roundclip(z, zmax):
                return int(np.clip(np.round(z), 0, zmax - 1))\

            x0 = roundclip(x0, self.size_x)
            y0 = roundclip(y0, self.size_y)
            x1 = roundclip(x1, self.size_x)
            y1 = roundclip(y1, self.size_y)
            selection[x0:x1 + 1, y0:y1 + 1] = True

        return selection

    def mousePressEvent(self, event):
        if event.button() != Qt.LeftButton:
            return

        self.__button_down_pos = self.mapToScene(event.pos())
        event.accept()

    def mouseMoveEvent(self, event):
        if event.buttons() != Qt.LeftButton:
            return

        self.selection_mark_changed.emit(self._get_marked_cells(event))
        event.accept()

    def mouseReleaseEvent(self, event):
        if event.button() != Qt.LeftButton:
            return

        if event.modifiers() & Qt.ShiftModifier:
            if event.modifiers() & Qt.ControlModifier:
                action = self.SelectionAddToGroup
            else:
                action = self.SelectionNewGroup
        elif event.modifiers() & Qt.AltModifier:
            action = self.SelectionRemove
        else:
            action = self.SelectionSet
        selection = self._get_marked_cells(event)
        self.selection_changed.emit(selection, action)
        event.accept()

    def keyPressEvent(self, event: QKeyEvent):
        if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right):
            self.selection_moved.emit(event)
        else:
            super().keyPressEvent(event)


class PieChart(QGraphicsItem):
    def __init__(self, dist, r, colors):
        super().__init__()
        self.dist = dist
        self.r = r
        self.colors = colors

    def boundingRect(self):
        return QRectF(- self.r, - self.r, 2 * self.r, 2 * self.r)

    def paint(self, painter, _option, _index):
        painter.save()
        start_angle = 0
        pen = QPen(QBrush(Qt.black), 2)
        pen.setCosmetic(True)
        painter.setPen(pen)
        painter.drawEllipse(self.boundingRect())
        for angle, color in zip(self.dist * 16 * 360, self.colors):
            if angle == 0:
                continue
            painter.setBrush(QBrush(color))
            painter.setPen(Qt.NoPen)
            painter.drawPie(self.boundingRect(), int(start_angle), int(angle))
            start_angle += angle
        painter.restore()


class ColoredCircle(QGraphicsItem):
    def __init__(self, r, color, proportion):
        super().__init__()
        self.r = r
        self.color = color
        self.proportion = proportion

    def boundingRect(self):
        return QRectF(-self.r, -self.r, 2 * self.r, 2 * self.r)

    def paint(self, painter, _option, _index):
        painter.save()
        pen = QPen(QBrush(self.color), 2)
        pen.setCosmetic(True)
        painter.setPen(pen)
        painter.setBrush(QBrush(self.color.lighter(int(200 - 80 * self.proportion))))
        painter.drawEllipse(self.boundingRect())
        painter.restore()


@contextmanager
def disconnected_spin(spin):
    spin.blockSignals(True)
    try:
        yield
    finally:
        spin.blockSignals(False)


N_ITERATIONS = 200


class SomSharedValueCompute:
    def __init__(self, domain: Domain, model: SOM,
                 offsets: Union[np.ndarray, None],
                 scales: Union[np.ndarray, None]):
        # offsets and scales are made immutable so that they are hashable
        def immutable(a):
            if a is not None:
                a = a.copy()
                a.flags.writeable = False
            return a

        self.domain = domain
        self.model = model
        self.offsets = immutable(offsets)
        self.scales = immutable(scales)
        self.__hash = None

    def __getstate__(self):
        state = self.__dict__.copy()
        state["__hash"] = None
        return state

    def __call__(self, data):
        x = data.transform(self.domain).X
        cont_x, mask, _, _ = SOM.prepare_data(x, self.offsets, self.scales)
        winners = np.full((len(data), 2), np.nan)
        distances = np.full(len(data), np.nan)
        winners[mask], distances[mask] = self.model.winners(cont_x)
        winners += 1
        return winners, distances

    # `offsets` and `scales` are ndarray's (and `model` contains ndarray's)
    # Unless we __eq__ by identity, we can't properly define hash.
    def __eq__(self, other):
        return type(self) is type(other) and \
               self.domain == other.domain \
               and self.model == other.model \
               and np.array_equal(self.offsets, other.offsets) \
               and np.array_equal(self.scales, other.scales)

    def __hash__(self):
        if self.__hash is None:
            self.__hash = hash((
                self.domain, self.model,
                tuple(self.offsets), tuple(self.scales)))
        return self.__hash


class SomCellCompute(SharedComputeValue):
    def __init__(self, compute_shared, dim_x, hexagonal):
        super().__init__(compute_shared)
        self.dim_x = dim_x
        self.hexagonal = hexagonal

    def __eq__(self, other):
        return super().__eq__(other) \
            and self.dim_x == other.dim_x \
            and self.hexagonal == other.hexagonal

    def __hash__(self):
        return hash((super().__hash__(), self.dim_x, self.hexagonal))

    def compute(self, _, shared_data):
        coords = shared_data[0]
        # coords are 1-based, subtract 1
        col = (coords[:, 0] - 1) + (coords[:, 1] - 1) * self.dim_x
        if self.hexagonal:
            # 1 cell less after every two rows
            col -= col // (self.dim_x * 2 - 1)
        return col


class SomCoordsCompute(SharedComputeValue):
    def __init__(self, shared, column):
        super().__init__(shared)
        self.column = column

    def compute(self, _, shared_data):
        return shared_data[0][:, self.column]

    def __eq__(self, other):
        return self.column == other.column and super().__eq__(other)

    def __hash__(self):
        return hash((super().__hash__(), self.column))


class SomErrorCompute(SharedComputeValue):
    InheritEq = True

    def compute(self, _, shared_data):
        return shared_data[1]


class GetGroups:
    # This assigns instances to selection groups; two instances that are not
    # same cannot be considered equal, period.
    InheritEq = True

    def __init__(self, id_to_group, default, offset):
        self.id_to_group = id_to_group
        self.default = default + offset
        self.offset = offset

    def __call__(self, data, *args, **kwargs):
        return np.array([self.id_to_group.get(id, self.default) - self.offset
                        for id in data.ids])


class OWSOM(OWWidget):
    name = "Self-Organizing Map"
    description = "Computation of self-organizing map."
    icon = "icons/SOM.svg"
    keywords = "self-organizing map, 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"  # QGraphicsView

    _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 Information(OWWidget.Information):
        modified = Msg(
            'The parameter settings have been changed. Press "Start" to '
            "rerun with the new settings."
        )

    class Warning(OWWidget.Warning):
        ignoring_disc_variables = Msg("SOM ignores categorical variables.")
        missing_colors = \
            Msg("Some data instances have undefined value of '{}'.")
        no_defined_colors = \
            Msg("'{}' has no defined values.")
        missing_values = Msg("{}")
        single_attribute = Msg("Data contains a single numeric column.")

    class Error(OWWidget.Error):
        no_numeric_variables = Msg("Data contains no numeric columns.")
        not_enough_data = Msg("SOM needs at least two data rows without missing values.")

    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.scales = self.offsets = None
        self.som = self.cells = self.member_data = None
        self.selection = None

        # self.colors holds a palette or None when we need to draw same-colored
        # circles. This happens by user's choice or when the color attribute
        # is numeric and has no defined values, so we can't construct bins
        self.colors: Optional[DiscretePalette] = None
        self.thresholds = self.bin_labels = None

        box = gui.vBox(self.controlArea, box="SOM")
        shape = gui.comboBox(
            box, self, "", items=("Hexagonal grid", "Square grid"))
        shape.setCurrentIndex(1 - self.hexagonal)
        shape.currentIndexChanged.connect(self.on_parameter_change)

        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,
            callback=self.on_parameter_change,
        )
        self.spin_x = gui.spin(**spinargs)
        with disconnected_spin(self.spin_x):
            self.spin_x.setValue(self.size_x)
        gui.widgetLabel(box3, "×")
        self.spin_y = gui.spin(**spinargs)
        with disconnected_spin(self.spin_y):
            self.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"),
            callback=self.on_parameter_change,
        )

        start = gui.button(
            box, self, "Restart", callback=self.restart_som_pressed,
            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))

        self.opt_controls = self.OptControls(
            shape, auto_dim, self.spin_x, self.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

    @staticmethod
    def _cont_domain(data):
        attrs = data.domain.attributes
        cont_attrs = [var for var in attrs if var.is_continuous]
        if not cont_attrs:
            return None
        return Domain(cont_attrs)

    @Inputs.data
    def set_data(self, data):
        def set_warnings():
            missing = len(data) - len(self.data)
            if missing:
                self.Warning.missing_values(
                    f'{missing} data {pl(missing, "instance")} with undefined value(s) {pl(missing, "is|are")} not shown.')

        cont_x = self.cont_x.copy() if self.cont_x is not None else None
        self.data = self.cont_x = None
        self.offsets = self.scales = None
        new_cont_x = None
        self.closeContext()
        self.clear_messages()

        if data is not None:
            cont_domain = self._cont_domain(data)
            if cont_domain is None:
                self.Error.no_numeric_variables()
            else:
                cont_attrs = cont_domain.attributes
                if len(cont_attrs) < len(data.domain.attributes):
                    self.Warning.ignoring_disc_variables()
                if len(cont_attrs) == 1:
                    self.Warning.single_attribute()

                new_cont_x, mask, self.offsets, self.scales \
                    = SOM.prepare_data(data.transform(cont_domain).X)
                rows = np.sum(mask)
                if rows == len(mask):
                    self.data = data
                elif rows > 1:
                    self.data = data[mask]
                else:
                    self.Error.not_enough_data()

        invalidated = cont_x is None or new_cont_x is None \
            or not array_equal(cont_x, new_cont_x)
        if invalidated:
            self.stop_optimization_and_wait()
            self.clear()

        if self.data is not None:
            self.cont_x = new_cont_x
            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()
        if invalidated:
            with disconnected_spin(self.spin_x), disconnected_spin(self.spin_y):
                self.recompute_dimensions()
            self.start_som()
        else:
            self._redraw()

    def clear(self):
        self.som = 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)

    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)
        self.on_parameter_change()

    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 on_parameter_change(self):
        self.Information.modified()

    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)

        selection_np = np.array(self.selection)
        if action == SomView.SelectionSet:
            selection_np[:] = 0
            selection_np[selection] = 1
        elif action == SomView.SelectionAddToGroup:
            selection_np[selection] = max(1, np.max(selection_np))
        elif action == SomView.SelectionNewGroup:
            selection_np[selection] = 1 + np.max(selection_np)
        elif action & SomView.SelectionRemove:
            selection_np[selection] = 0
        self.selection = selection_np.tolist()
        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
            x, y = x[0], y[0]
            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.tolist())

    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):
        self.Information.modified.clear()
        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.colors 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):
        # if self.colors is None, we use _draw_same_color and don't call
        # this function
        assert self.colors is not None

        color_column = self.data.get_column(self.attr_color)
        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)
            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()
        values = list(values) + ["(N/A)"]
        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}&nbsp;%)
                </td>
            </tr>
            """ for color, val, n in zip(colors, values, distribution) if n) \
            + "</table>"

    def _draw_pie_charts(self, sizes):
        assert self.colors is not None  # if it were, we'd call _draw_same_color

        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):
        assert self.colors is not None  # if it were, we'd call _draw_same_color

        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

        self.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.setInvalidated(True)

        self._optimizer = Optimizer(self.cont_x, self.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.setInvalidated(False)
        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(
                # -1.5: 1 is necessary, 0.5 is to add some border
                -0.5, -1.5 / np.sqrt(3), self.size_x,
                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale - 1 / np.sqrt(3))
        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.som is None:
            self.Outputs.selected_data.send(None)
            self.Outputs.annotated_data.send(None)
            return

        ngroups = int(self.selection is not None) and np.max(self.selection)
        indices = np.zeros(len(self.data), dtype=int)
        id_to_group = {}
        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)
                    group = self.selection[x][y]
                    indices[rows] = group
                    if group > 0:
                        for id_ in self.data.ids[rows]:
                            id_to_group[id_] = group

        cont_domain = self._cont_domain(self.data)
        shared_compute = SomSharedValueCompute(
            cont_domain, self.som, self.offsets, self.scales)
        cell = DiscreteVariable(
            "som_cell",
            values=tuple(
                f"r{row + 1}c{col + 1}"
                for row in range(self.size_y)
                for col in range(self.size_x - (self.hexagonal and row % 2))),
            compute_value=SomCellCompute(shared_compute,
                                         self.size_x, self.hexagonal))
        coordx = ContinuousVariable(
            "som_row",
            number_of_decimals=0,
            compute_value=SomCoordsCompute(shared_compute, 1))
        coordy = ContinuousVariable(
            "som_col",
            number_of_decimals=0,
            compute_value=SomCoordsCompute(shared_compute, 0))
        error = ContinuousVariable(
            "som_error",
            compute_value=SomErrorCompute(shared_compute)
        )
        som_attrs = (cell, coordx, coordy, error)

        grp_values, _ = group_values(
                indices, include_unselected=False, values=None)

        def make_domain(values, default_grp, offset):
            grp_var = DiscreteVariable(
                get_unique_names(self.data.domain, ANNOTATED_DATA_FEATURE_NAME),
                values,
                compute_value=GetGroups(id_to_group, default_grp, offset))

            if not self.data.domain.class_vars:
                class_vars, metas = grp_var, som_attrs
            else:
                class_vars, metas = (), (grp_var,) + som_attrs
            return add_columns(self.data.domain, (), class_vars, metas)

        if np.any(indices):
            sel_domain = make_domain(grp_values, np.nan, 1)
            mask = np.flatnonzero(indices)
            self.Outputs.selected_data.send(
                lazy_table_transform(sel_domain, self.data[mask]))
        else:
            self.Outputs.selected_data.send(None)

        if ngroups > 1:
            sel_domain = make_domain(grp_values + ["Unselected", ], ngroups, 1)
        else:
            sel_domain = make_domain(("No", "Yes"), 0, 0)
        self.Outputs.annotated_data.send(
            lazy_table_transform(sel_domain, self.data))

    def set_color_bins(self):
        self.Warning.no_defined_colors.clear()

        if self.attr_color is None:
            self.thresholds = self.bin_labels = self.colors = None
            return

        if self.attr_color.is_discrete:
            self.thresholds = self.bin_labels = None
            self.colors = self.attr_color.palette
            return

        col = self.data.get_column(self.attr_color)
        col = col[np.isfinite(col)]
        if not col.size:
            self.Warning.no_defined_colors(self.attr_color)
            self.thresholds = self.bin_labels = self.colors = None
            return

        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])
        if not self.bin_labels[0] and binning.labels:
            # Nan's are already filtered out, but it doesn't hurt much
            # to use nanmax/nanmin
            if np.nanmin(col) == np.nanmax(col):
                # Handle a degenerate case with a single value
                # Use the second threshold (because value must be smaller),
                # but the first threshold as label (because that's the
                # actual value in the data.
                self.thresholds = binning.thresholds[1:]
                self.bin_labels = (binning.labels[:1],
                                   binning.short_labels[: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.colors 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 or ([], [])
        if len(labels) <= 1:
            return 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.colors:
            self.report_caption(
                f"Self-organizing map colored by '{self.attr_color.name}'")

    @classmethod
    def migrate_settings(cls, settings, _):
        # previously selection was saved as np.ndarray which is not supported
        # by widget-base, change selection to list
        selection = settings.get('selection')
        if selection is not None and isinstance(selection, np.ndarray):
            settings['selection'] = selection.tolist()


def _draw_hexagon():
    path = QPainterPath()
    s = 0.5 / (np.sqrt(3) / 2)
    path.moveTo(-0.5, -s / 2)
    path.lineTo(-0.5, s / 2)
    path.lineTo(0, s)
    path.lineTo(0.5, s / 2)
    path.lineTo(0.5, -s / 2)
    path.lineTo(0, -s)
    path.lineTo(-0.5, -s / 2)
    return path


_hexagon_path = _draw_hexagon()


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWSOM).run(Table("iris"))

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
[ISSUE]
**Title:** TypeError When `class_vars` Is None in OWSOM Widget

**Description:**
When the `class_vars` attribute of the data domain is set to `None`, updating the widget's output raises a `TypeError`. This occurs because the code attempts to iterate over a `DiscreteVariable` object, which is not iterable.

**Example Code:**
```python
widget.data.domain.class_vars = None
widget.update_output()
```

**Expected Behavior:**
The widget should handle the case where `class_vars` is `None` without raising an error, allowing the output to be updated correctly.

**Actual Behavior:**
A `TypeError` is raised with the message:
```
TypeError: 'DiscreteVariable' object is not iterable
```
[/ISSUE]
{
    "TestOWSOM.test_attr_color_change": "PASSED",
    "TestOWSOM.test_auto_compute_dimensions": "PASSED",
    "TestOWSOM.test_cell_sizes": "PASSED",
    "TestOWSOM.test_colored_circles_with_constant": "PASSED",
    "TestOWSOM.test_colored_circles_with_missing_values": "PASSED",
    "TestOWSOM.test_diff_color_diff_size": "PASSED",
    "TestOWSOM.test_diff_color_same_size": "PASSED",
    "TestOWSOM.test_get_color_column": "PASSED",
    "TestOWSOM.test_image_export": "PASSED",
    "TestOWSOM.test_invalidated": "PASSED",
    "TestOWSOM.test_make_domain_without_class_vars": "PASSED",
    "TestOWSOM.test_minimum_size": "PASSED",
    "TestOWSOM.test_missing_all_data": "PASSED",
    "TestOWSOM.test_missing_one_row_data": "PASSED",
    "TestOWSOM.test_missing_some_data": "PASSED",
    "TestOWSOM.test_modified_info": "PASSED",
    "TestOWSOM.test_msg_base_class": "PASSED",
    "TestOWSOM.test_on_selection_change": "PASSED",
    "TestOWSOM.test_on_selection_change_on_empty": "PASSED",
    "TestOWSOM.test_output_columns": "PASSED",
    "TestOWSOM.test_output_selection": "PASSED",
    "TestOWSOM.test_pie_charts": "PASSED",
    "TestOWSOM.test_redraw_grid": "PASSED",
    "TestOWSOM.test_requires_continuous": "PASSED",
    "TestOWSOM.test_run_actual_optimization": "PASSED",
    "TestOWSOM.test_same_color_diff_size": "PASSED",
    "TestOWSOM.test_same_color_same_size": "PASSED",
    "TestOWSOM.test_send_report": "PASSED",
    "TestOWSOM.test_single_attribute": "PASSED",
    "TestOWSOM.test_single_row_data": "PASSED",
    "TestOWSOM.test_sparse_data": "PASSED",
    "TestComputeValues.test_eq_hash": "PASSED"
}
diff --git a/Orange/widgets/unsupervised/owsom.py b/Orange/widgets/unsupervised/owsom.py
index 84c11e311..6d5a11aad 100644
--- a/Orange/widgets/unsupervised/owsom.py
+++ b/Orange/widgets/unsupervised/owsom.py
@@ -1018,7 +1018,8 @@ class OWSOM(OWWidget):
                 compute_value=GetGroups(id_to_group, default_grp, offset))
 
             if not self.data.domain.class_vars:
-                class_vars, metas = grp_var, som_attrs
+                self.data.domain.class_vars = ()
+                class_vars, metas = (grp_var,), som_attrs
             else:
                 class_vars, metas = (), (grp_var,) + som_attrs
             return add_columns(self.data.domain, (), class_vars, metas)

{"Orange/widgets/unsupervised/owsom.py": "from collections import defaultdict, namedtuple\nfrom contextlib import contextmanager\nfrom typing import Optional, Union\nfrom xml.sax.saxutils import escape\n\nimport numpy as np\n\nfrom AnyQt.QtCore import Qt, QRectF, pyqtSignal as Signal, QObject, QThread, \\\n    pyqtSlot as Slot\nfrom AnyQt.QtGui import QTransform, QPen, QBrush, QColor, QPainter, \\\n    QPainterPath, QKeyEvent\nfrom AnyQt.QtWidgets import \\\n    QGraphicsView, QGraphicsScene, \\\n    QGraphicsItem, QGraphicsRectItem, QGraphicsItemGroup, QSizePolicy, \\\n    QGraphicsPathItem\n\nfrom Orange.widgets.utils.signals import lazy_table_transform\n\nfrom Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable\nfrom Orange.data.util import array_equal, SharedComputeValue, get_unique_names\nfrom Orange.preprocess import decimal_binnings, time_binnings\nfrom Orange.projection.som import SOM\n\nfrom Orange.widgets import gui\nfrom Orange.widgets.utils.localization import pl\nfrom Orange.widgets.widget import OWWidget, Msg, Input, Output\nfrom Orange.widgets.settings import \\\n    DomainContextHandler, ContextSetting, Setting\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.annotated_data import \\\n    add_columns, group_values, \\\n    ANNOTATED_DATA_SIGNAL_NAME, ANNOTATED_DATA_FEATURE_NAME\nfrom Orange.widgets.utils.colorpalettes import \\\n    BinnedContinuousPalette, LimitedDiscretePalette, DiscretePalette\nfrom Orange.widgets.visualize.utils import CanvasRectangle, CanvasText\nfrom Orange.widgets.visualize.utils.plotutils import wrap_legend_items\n\n\nsqrt3_2 = np.sqrt(3) / 2\n\n\nclass SomView(QGraphicsView):\n    SelectionSet, SelectionNewGroup, SelectionAddToGroup, SelectionRemove \\\n        = range(4)\n    selection_changed = Signal(np.ndarray, int)\n    selection_moved = Signal(QKeyEvent)\n    selection_mark_changed = Signal(np.ndarray)\n\n    def __init__(self, scene):\n        super().__init__(scene)\n        self.__button_down_pos = None\n        self.size_x = self.size_y = 1\n        self.hexagonal = True\n\n    def set_dimensions(self, size_x, size_y, hexagonal):\n        self.size_x = size_x\n        self.size_y = size_y\n        self.hexagonal = hexagonal\n\n    def _get_marked_cells(self, event):\n        x0, y0 = self.__button_down_pos.x(), self.__button_down_pos.y()\n        pos = self.mapToScene(event.pos())\n        x1, y1 = pos.x(), pos.y()\n        x0, x1 = sorted((x0, x1))\n        y0, y1 = sorted((y0, y1))\n        selection = np.zeros((self.size_x, self.size_y), dtype=bool)\n        if self.hexagonal:\n            y0 = max(0, int(y0 / sqrt3_2 + 0.5))\n            y1 = min(self.size_y, int(np.ceil(y1 / sqrt3_2 + 0.5)))\n            for y in range(y0, y1):\n                if x1 < 0:\n                    continue\n                x0_ = max(0, int(x0 + 0.5 - (y % 2) / 2))\n                x1_ = min(self.size_x - y % 2,\n                          int(np.ceil(x1 + 0.5 - (y % 2) / 2)))\n                selection[x0_:x1_, y] = True\n        elif not(x1 < -0.5 or x0 > self.size_x - 0.5\n                 or y1 < -0.5 or y0 > self.size_y - 0.5):\n\n            def roundclip(z, zmax):\n                return int(np.clip(np.round(z), 0, zmax - 1))\\\n\n            x0 = roundclip(x0, self.size_x)\n            y0 = roundclip(y0, self.size_y)\n            x1 = roundclip(x1, self.size_x)\n            y1 = roundclip(y1, self.size_y)\n            selection[x0:x1 + 1, y0:y1 + 1] = True\n\n        return selection\n\n    def mousePressEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n\n        self.__button_down_pos = self.mapToScene(event.pos())\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        if event.buttons() != Qt.LeftButton:\n            return\n\n        self.selection_mark_changed.emit(self._get_marked_cells(event))\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n\n        if event.modifiers() & Qt.ShiftModifier:\n            if event.modifiers() & Qt.ControlModifier:\n                action = self.SelectionAddToGroup\n            else:\n                action = self.SelectionNewGroup\n        elif event.modifiers() & Qt.AltModifier:\n            action = self.SelectionRemove\n        else:\n            action = self.SelectionSet\n        selection = self._get_marked_cells(event)\n        self.selection_changed.emit(selection, action)\n        event.accept()\n\n    def keyPressEvent(self, event: QKeyEvent):\n        if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right):\n            self.selection_moved.emit(event)\n        else:\n            super().keyPressEvent(event)\n\n\nclass PieChart(QGraphicsItem):\n    def __init__(self, dist, r, colors):\n        super().__init__()\n        self.dist = dist\n        self.r = r\n        self.colors = colors\n\n    def boundingRect(self):\n        return QRectF(- self.r, - self.r, 2 * self.r, 2 * self.r)\n\n    def paint(self, painter, _option, _index):\n        painter.save()\n        start_angle = 0\n        pen = QPen(QBrush(Qt.black), 2)\n        pen.setCosmetic(True)\n        painter.setPen(pen)\n        painter.drawEllipse(self.boundingRect())\n        for angle, color in zip(self.dist * 16 * 360, self.colors):\n            if angle == 0:\n                continue\n            painter.setBrush(QBrush(color))\n            painter.setPen(Qt.NoPen)\n            painter.drawPie(self.boundingRect(), int(start_angle), int(angle))\n            start_angle += angle\n        painter.restore()\n\n\nclass ColoredCircle(QGraphicsItem):\n    def __init__(self, r, color, proportion):\n        super().__init__()\n        self.r = r\n        self.color = color\n        self.proportion = proportion\n\n    def boundingRect(self):\n        return QRectF(-self.r, -self.r, 2 * self.r, 2 * self.r)\n\n    def paint(self, painter, _option, _index):\n        painter.save()\n        pen = QPen(QBrush(self.color), 2)\n        pen.setCosmetic(True)\n        painter.setPen(pen)\n        painter.setBrush(QBrush(self.color.lighter(int(200 - 80 * self.proportion))))\n        painter.drawEllipse(self.boundingRect())\n        painter.restore()\n\n\n@contextmanager\ndef disconnected_spin(spin):\n    spin.blockSignals(True)\n    try:\n        yield\n    finally:\n        spin.blockSignals(False)\n\n\nN_ITERATIONS = 200\n\n\nclass SomSharedValueCompute:\n    def __init__(self, domain: Domain, model: SOM,\n                 offsets: Union[np.ndarray, None],\n                 scales: Union[np.ndarray, None]):\n        # offsets and scales are made immutable so that they are hashable\n        def immutable(a):\n            if a is not None:\n                a = a.copy()\n                a.flags.writeable = False\n            return a\n\n        self.domain = domain\n        self.model = model\n        self.offsets = immutable(offsets)\n        self.scales = immutable(scales)\n        self.__hash = None\n\n    def __getstate__(self):\n        state = self.__dict__.copy()\n        state[\"__hash\"] = None\n        return state\n\n    def __call__(self, data):\n        x = data.transform(self.domain).X\n        cont_x, mask, _, _ = SOM.prepare_data(x, self.offsets, self.scales)\n        winners = np.full((len(data), 2), np.nan)\n        distances = np.full(len(data), np.nan)\n        winners[mask], distances[mask] = self.model.winners(cont_x)\n        winners += 1\n        return winners, distances\n\n    # `offsets` and `scales` are ndarray's (and `model` contains ndarray's)\n    # Unless we __eq__ by identity, we can't properly define hash.\n    def __eq__(self, other):\n        return type(self) is type(other) and \\\n               self.domain == other.domain \\\n               and self.model == other.model \\\n               and np.array_equal(self.offsets, other.offsets) \\\n               and np.array_equal(self.scales, other.scales)\n\n    def __hash__(self):\n        if self.__hash is None:\n            self.__hash = hash((\n                self.domain, self.model,\n                tuple(self.offsets), tuple(self.scales)))\n        return self.__hash\n\n\nclass SomCellCompute(SharedComputeValue):\n    def __init__(self, compute_shared, dim_x, hexagonal):\n        super().__init__(compute_shared)\n        self.dim_x = dim_x\n        self.hexagonal = hexagonal\n\n    def __eq__(self, other):\n        return super().__eq__(other) \\\n            and self.dim_x == other.dim_x \\\n            and self.hexagonal == other.hexagonal\n\n    def __hash__(self):\n        return hash((super().__hash__(), self.dim_x, self.hexagonal))\n\n    def compute(self, _, shared_data):\n        coords = shared_data[0]\n        # coords are 1-based, subtract 1\n        col = (coords[:, 0] - 1) + (coords[:, 1] - 1) * self.dim_x\n        if self.hexagonal:\n            # 1 cell less after every two rows\n            col -= col // (self.dim_x * 2 - 1)\n        return col\n\n\nclass SomCoordsCompute(SharedComputeValue):\n    def __init__(self, shared, column):\n        super().__init__(shared)\n        self.column = column\n\n    def compute(self, _, shared_data):\n        return shared_data[0][:, self.column]\n\n    def __eq__(self, other):\n        return self.column == other.column and super().__eq__(other)\n\n    def __hash__(self):\n        return hash((super().__hash__(), self.column))\n\n\nclass SomErrorCompute(SharedComputeValue):\n    InheritEq = True\n\n    def compute(self, _, shared_data):\n        return shared_data[1]\n\n\nclass GetGroups:\n    # This assigns instances to selection groups; two instances that are not\n    # same cannot be considered equal, period.\n    InheritEq = True\n\n    def __init__(self, id_to_group, default, offset):\n        self.id_to_group = id_to_group\n        self.default = default + offset\n        self.offset = offset\n\n    def __call__(self, data, *args, **kwargs):\n        return np.array([self.id_to_group.get(id, self.default) - self.offset\n                        for id in data.ids])\n\n\nclass OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = \"self-organizing map, som\"\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"  # QGraphicsView\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Information(OWWidget.Information):\n        modified = Msg(\n            'The parameter settings have been changed. Press \"Start\" to '\n            \"rerun with the new settings.\"\n        )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        no_defined_colors = \\\n            Msg(\"'{}' has no defined values.\")\n        missing_values = Msg(\"{}\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        not_enough_data = Msg(\"SOM needs at least two data rows without missing values.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.scales = self.offsets = None\n        self.som = self.cells = self.member_data = None\n        self.selection = None\n\n        # self.colors holds a palette or None when we need to draw same-colored\n        # circles. This happens by user's choice or when the color attribute\n        # is numeric and has no defined values, so we can't construct bins\n        self.colors: Optional[DiscretePalette] = None\n        self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n        shape.currentIndexChanged.connect(self.on_parameter_change)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\",\n            widget=box3,\n            master=self,\n            minv=5,\n            maxv=100,\n            step=5,\n            alignment=Qt.AlignRight,\n            callback=self.on_parameter_change,\n        )\n        self.spin_x = gui.spin(**spinargs)\n        with disconnected_spin(self.spin_x):\n            self.spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"\u00d7\")\n        self.spin_y = gui.spin(**spinargs)\n        with disconnected_spin(self.spin_y):\n            self.spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box,\n            self,\n            \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\", \"Replicable random\"),\n            callback=self.on_parameter_change,\n        )\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, self.spin_x, self.spin_y, initialization, start\n        )\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @staticmethod\n    def _cont_domain(data):\n        attrs = data.domain.attributes\n        cont_attrs = [var for var in attrs if var.is_continuous]\n        if not cont_attrs:\n            return None\n        return Domain(cont_attrs)\n\n    @Inputs.data\n    def set_data(self, data):\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing:\n                self.Warning.missing_values(\n                    f'{missing} data {pl(missing, \"instance\")} with undefined value(s) {pl(missing, \"is|are\")} not shown.')\n\n        cont_x = self.cont_x.copy() if self.cont_x is not None else None\n        self.data = self.cont_x = None\n        self.offsets = self.scales = None\n        new_cont_x = None\n        self.closeContext()\n        self.clear_messages()\n\n        if data is not None:\n            cont_domain = self._cont_domain(data)\n            if cont_domain is None:\n                self.Error.no_numeric_variables()\n            else:\n                cont_attrs = cont_domain.attributes\n                if len(cont_attrs) < len(data.domain.attributes):\n                    self.Warning.ignoring_disc_variables()\n                if len(cont_attrs) == 1:\n                    self.Warning.single_attribute()\n\n                new_cont_x, mask, self.offsets, self.scales \\\n                    = SOM.prepare_data(data.transform(cont_domain).X)\n                rows = np.sum(mask)\n                if rows == len(mask):\n                    self.data = data\n                elif rows > 1:\n                    self.data = data[mask]\n                else:\n                    self.Error.not_enough_data()\n\n        invalidated = cont_x is None or new_cont_x is None \\\n            or not array_equal(cont_x, new_cont_x)\n        if invalidated:\n            self.stop_optimization_and_wait()\n            self.clear()\n\n        if self.data is not None:\n            self.cont_x = new_cont_x\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        if invalidated:\n            with disconnected_spin(self.spin_x), disconnected_spin(self.spin_y):\n                self.recompute_dimensions()\n            self.start_som()\n        else:\n            self._redraw()\n\n    def clear(self):\n        self.som = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n        self.on_parameter_change()\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def on_parameter_change(self):\n        self.Information.modified()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n\n        selection_np = np.array(self.selection)\n        if action == SomView.SelectionSet:\n            selection_np[:] = 0\n            selection_np[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            selection_np[selection] = max(1, np.max(selection_np))\n        elif action == SomView.SelectionNewGroup:\n            selection_np[selection] = 1 + np.max(selection_np)\n        elif action & SomView.SelectionRemove:\n            selection_np[selection] = 0\n        self.selection = selection_np.tolist()\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            x, y = x[0], y[0]\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x][y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection.tolist())\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x][y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        self.Information.modified.clear()\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.colors is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        # if self.colors is None, we use _draw_same_color and don't call\n        # this function\n        assert self.colors is not None\n\n        color_column = self.data.get_column(self.attr_color)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        values = list(values) + [\"(N/A)\"]\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>\u25a0</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        assert self.colors is not None  # if it were, we'd call _draw_same_color\n\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        assert self.colors is not None  # if it were, we'd call _draw_same_color\n\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        self.som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n        self.setInvalidated(True)\n\n        self._optimizer = Optimizer(self.cont_x, self.som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self.setInvalidated(False)\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments, _ = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                # -1.5: 1 is necessary, 0.5 is to add some border\n                -0.5, -1.5 / np.sqrt(3), self.size_x,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale - 1 / np.sqrt(3))\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.som is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        ngroups = int(self.selection is not None) and np.max(self.selection)\n        indices = np.zeros(len(self.data), dtype=int)\n        id_to_group = {}\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    group = self.selection[x][y]\n                    indices[rows] = group\n                    if group > 0:\n                        for id_ in self.data.ids[rows]:\n                            id_to_group[id_] = group\n\n        cont_domain = self._cont_domain(self.data)\n        shared_compute = SomSharedValueCompute(\n            cont_domain, self.som, self.offsets, self.scales)\n        cell = DiscreteVariable(\n            \"som_cell\",\n            values=tuple(\n                f\"r{row + 1}c{col + 1}\"\n                for row in range(self.size_y)\n                for col in range(self.size_x - (self.hexagonal and row % 2))),\n            compute_value=SomCellCompute(shared_compute,\n                                         self.size_x, self.hexagonal))\n        coordx = ContinuousVariable(\n            \"som_row\",\n            number_of_decimals=0,\n            compute_value=SomCoordsCompute(shared_compute, 1))\n        coordy = ContinuousVariable(\n            \"som_col\",\n            number_of_decimals=0,\n            compute_value=SomCoordsCompute(shared_compute, 0))\n        error = ContinuousVariable(\n            \"som_error\",\n            compute_value=SomErrorCompute(shared_compute)\n        )\n        som_attrs = (cell, coordx, coordy, error)\n\n        grp_values, _ = group_values(\n                indices, include_unselected=False, values=None)\n\n        def make_domain(values, default_grp, offset):\n            grp_var = DiscreteVariable(\n                get_unique_names(self.data.domain, ANNOTATED_DATA_FEATURE_NAME),\n                values,\n                compute_value=GetGroups(id_to_group, default_grp, offset))\n\n            if not self.data.domain.class_vars:\n                class_vars, metas = grp_var, som_attrs\n            else:\n                class_vars, metas = (), (grp_var,) + som_attrs\n            return add_columns(self.data.domain, (), class_vars, metas)\n\n        if np.any(indices):\n            sel_domain = make_domain(grp_values, np.nan, 1)\n            mask = np.flatnonzero(indices)\n            self.Outputs.selected_data.send(\n                lazy_table_transform(sel_domain, self.data[mask]))\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if ngroups > 1:\n            sel_domain = make_domain(grp_values + [\"Unselected\", ], ngroups, 1)\n        else:\n            sel_domain = make_domain((\"No\", \"Yes\"), 0, 0)\n        self.Outputs.annotated_data.send(\n            lazy_table_transform(sel_domain, self.data))\n\n    def set_color_bins(self):\n        self.Warning.no_defined_colors.clear()\n\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n            return\n\n        if self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n            return\n\n        col = self.data.get_column(self.attr_color)\n        col = col[np.isfinite(col)]\n        if not col.size:\n            self.Warning.no_defined_colors(self.attr_color)\n            self.thresholds = self.bin_labels = self.colors = None\n            return\n\n        if self.attr_color.is_time:\n            binning = time_binnings(col, min_bins=4)[-1]\n        else:\n            binning = decimal_binnings(col, min_bins=4)[-1]\n        self.thresholds = binning.thresholds[1:-1]\n        self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n        if not self.bin_labels[0] and binning.labels:\n            # Nan's are already filtered out, but it doesn't hurt much\n            # to use nanmax/nanmin\n            if np.nanmin(col) == np.nanmax(col):\n                # Handle a degenerate case with a single value\n                # Use the second threshold (because value must be smaller),\n                # but the first threshold as label (because that's the\n                # actual value in the data.\n                self.thresholds = binning.thresholds[1:]\n                self.bin_labels = (binning.labels[:1],\n                                   binning.short_labels[:1])\n        palette = BinnedContinuousPalette.from_palette(\n            self.attr_color.palette, binning.thresholds)\n        self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.colors is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels or ([], [])\n        if len(labels) <= 1:\n            return labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"\u2265 {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.colors:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")\n\n    @classmethod\n    def migrate_settings(cls, settings, _):\n        # previously selection was saved as np.ndarray which is not supported\n        # by widget-base, change selection to list\n        selection = settings.get('selection')\n        if selection is not None and isinstance(selection, np.ndarray):\n            settings['selection'] = selection.tolist()\n\n\ndef _draw_hexagon():\n    path = QPainterPath()\n    s = 0.5 / (np.sqrt(3) / 2)\n    path.moveTo(-0.5, -s / 2)\n    path.lineTo(-0.5, s / 2)\n    path.lineTo(0, s)\n    path.lineTo(0.5, s / 2)\n    path.lineTo(0.5, -s / 2)\n    path.lineTo(0, -s)\n    path.lineTo(-0.5, -s / 2)\n    return path\n\n\n_hexagon_path = _draw_hexagon()\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWSOM).run(Table(\"iris\"))\n"}
A user will ask you to solve a task. You should first draft your thinking process (inner monologue). Then, generate the solution.

Your response format must follow the template below:
<think>
Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate a correct solution.
</think>
<solution>
Final solution presented to the user.
</solution>
system
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** TypeError When `class_vars` Is None in OWSOM Widget

**Description:**
When the `class_vars` attribute of the data domain is set to `None`, updating the widget's output raises a `TypeError`. This occurs because the code attempts to iterate over a `DiscreteVariable` object, which is not iterable.

**Example Code:**
```python
widget.data.domain.class_vars = None
widget.update_output()
```

**Expected Behavior:**
The widget should handle the case where `class_vars` is `None` without raising an error, allowing the output to be updated correctly.

**Actual Behavior:**
A `TypeError` is raised with the message:
```
TypeError: 'DiscreteVariable' object is not iterable
```
[/ISSUE]
--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.

--- BEGIN FILE ---
```
### Orange/widgets/unsupervised/owsom.py
from collections import defaultdict, namedtuple
from contextlib import contextmanager
from typing import Optional, Union
from xml.sax.saxutils import escape

import numpy as np

from AnyQt.QtCore import Qt, QRectF, pyqtSignal as Signal, QObject, QThread, \
    pyqtSlot as Slot
from AnyQt.QtGui import QTransform, QPen, QBrush, QColor, QPainter, \
    QPainterPath, QKeyEvent
from AnyQt.QtWidgets import \
    QGraphicsView, QGraphicsScene, \
    QGraphicsItem, QGraphicsRectItem, QGraphicsItemGroup, QSizePolicy, \
    QGraphicsPathItem

from Orange.widgets.utils.signals import lazy_table_transform

from Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable
from Orange.data.util import array_equal, SharedComputeValue, get_unique_names
from Orange.preprocess import decimal_binnings, time_binnings
from Orange.projection.som import SOM

from Orange.widgets import gui
from Orange.widgets.utils.localization import pl
from Orange.widgets.widget import OWWidget, Msg, Input, Output
from Orange.widgets.settings import \
    DomainContextHandler, ContextSetting, Setting
from Orange.widgets.utils.itemmodels import DomainModel
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.annotated_data import \
    add_columns, group_values, \
    ANNOTATED_DATA_SIGNAL_NAME, ANNOTATED_DATA_FEATURE_NAME
from Orange.widgets.utils.colorpalettes import \
    BinnedContinuousPalette, LimitedDiscretePalette, DiscretePalette
from Orange.widgets.visualize.utils import CanvasRectangle, CanvasText
from Orange.widgets.visualize.utils.plotutils import wrap_legend_items


sqrt3_2 = np.sqrt(3) / 2


class SomView(QGraphicsView):
    SelectionSet, SelectionNewGroup, SelectionAddToGroup, SelectionRemove \
        = range(4)
    selection_changed = Signal(np.ndarray, int)
    selection_moved = Signal(QKeyEvent)
    selection_mark_changed = Signal(np.ndarray)

    def __init__(self, scene):
        super().__init__(scene)
        self.__button_down_pos = None
        self.size_x = self.size_y = 1
        self.hexagonal = True

    def set_dimensions(self, size_x, size_y, hexagonal):
        self.size_x = size_x
        self.size_y = size_y
        self.hexagonal = hexagonal

    def _get_marked_cells(self, event):
        x0, y0 = self.__button_down_pos.x(), self.__button_down_pos.y()
        pos = self.mapToScene(event.pos())
        x1, y1 = pos.x(), pos.y()
        x0, x1 = sorted((x0, x1))
        y0, y1 = sorted((y0, y1))
        selection = np.zeros((self.size_x, self.size_y), dtype=bool)
        if self.hexagonal:
            y0 = max(0, int(y0 / sqrt3_2 + 0.5))
            y1 = min(self.size_y, int(np.ceil(y1 / sqrt3_2 + 0.5)))
            for y in range(y0, y1):
                if x1 < 0:
                    continue
                x0_ = max(0, int(x0 + 0.5 - (y % 2) / 2))
                x1_ = min(self.size_x - y % 2,
                          int(np.ceil(x1 + 0.5 - (y % 2) / 2)))
                selection[x0_:x1_, y] = True
        elif not(x1 < -0.5 or x0 > self.size_x - 0.5
                 or y1 < -0.5 or y0 > self.size_y - 0.5):

            def roundclip(z, zmax):
                return int(np.clip(np.round(z), 0, zmax - 1))\

            x0 = roundclip(x0, self.size_x)
            y0 = roundclip(y0, self.size_y)
            x1 = roundclip(x1, self.size_x)
            y1 = roundclip(y1, self.size_y)
            selection[x0:x1 + 1, y0:y1 + 1] = True

        return selection

    def mousePressEvent(self, event):
        if event.button() != Qt.LeftButton:
            return

        self.__button_down_pos = self.mapToScene(event.pos())
        event.accept()

    def mouseMoveEvent(self, event):
        if event.buttons() != Qt.LeftButton:
            return

        self.selection_mark_changed.emit(self._get_marked_cells(event))
        event.accept()

    def mouseReleaseEvent(self, event):
        if event.button() != Qt.LeftButton:
            return

        if event.modifiers() & Qt.ShiftModifier:
            if event.modifiers() & Qt.ControlModifier:
                action = self.SelectionAddToGroup
            else:
                action = self.SelectionNewGroup
        elif event.modifiers() & Qt.AltModifier:
            action = self.SelectionRemove
        else:
            action = self.SelectionSet
        selection = self._get_marked_cells(event)
        self.selection_changed.emit(selection, action)
        event.accept()

    def keyPressEvent(self, event: QKeyEvent):
        if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right):
            self.selection_moved.emit(event)
        else:
            super().keyPressEvent(event)


class PieChart(QGraphicsItem):
    def __init__(self, dist, r, colors):
        super().__init__()
        self.dist = dist
        self.r = r
        self.colors = colors

    def boundingRect(self):
        return QRectF(- self.r, - self.r, 2 * self.r, 2 * self.r)

    def paint(self, painter, _option, _index):
        painter.save()
        start_angle = 0
        pen = QPen(QBrush(Qt.black), 2)
        pen.setCosmetic(True)
        painter.setPen(pen)
        painter.drawEllipse(self.boundingRect())
        for angle, color in zip(self.dist * 16 * 360, self.colors):
            if angle == 0:
                continue
            painter.setBrush(QBrush(color))
            painter.setPen(Qt.NoPen)
            painter.drawPie(self.boundingRect(), int(start_angle), int(angle))
            start_angle += angle
        painter.restore()


class ColoredCircle(QGraphicsItem):
    def __init__(self, r, color, proportion):
        super().__init__()
        self.r = r
        self.color = color
        self.proportion = proportion

    def boundingRect(self):
        return QRectF(-self.r, -self.r, 2 * self.r, 2 * self.r)

    def paint(self, painter, _option, _index):
        painter.save()
        pen = QPen(QBrush(self.color), 2)
        pen.setCosmetic(True)
        painter.setPen(pen)
        painter.setBrush(QBrush(self.color.lighter(int(200 - 80 * self.proportion))))
        painter.drawEllipse(self.boundingRect())
        painter.restore()


@contextmanager
def disconnected_spin(spin):
    spin.blockSignals(True)
    try:
        yield
    finally:
        spin.blockSignals(False)


N_ITERATIONS = 200


class SomSharedValueCompute:
    def __init__(self, domain: Domain, model: SOM,
                 offsets: Union[np.ndarray, None],
                 scales: Union[np.ndarray, None]):
        # offsets and scales are made immutable so that they are hashable
        def immutable(a):
            if a is not None:
                a = a.copy()
                a.flags.writeable = False
            return a

        self.domain = domain
        self.model = model
        self.offsets = immutable(offsets)
        self.scales = immutable(scales)
        self.__hash = None

    def __getstate__(self):
        state = self.__dict__.copy()
        state["__hash"] = None
        return state

    def __call__(self, data):
        x = data.transform(self.domain).X
        cont_x, mask, _, _ = SOM.prepare_data(x, self.offsets, self.scales)
        winners = np.full((len(data), 2), np.nan)
        distances = np.full(len(data), np.nan)
        winners[mask], distances[mask] = self.model.winners(cont_x)
        winners += 1
        return winners, distances

    # `offsets` and `scales` are ndarray's (and `model` contains ndarray's)
    # Unless we __eq__ by identity, we can't properly define hash.
    def __eq__(self, other):
        return type(self) is type(other) and \
               self.domain == other.domain \
               and self.model == other.model \
               and np.array_equal(self.offsets, other.offsets) \
               and np.array_equal(self.scales, other.scales)

    def __hash__(self):
        if self.__hash is None:
            self.__hash = hash((
                self.domain, self.model,
                tuple(self.offsets), tuple(self.scales)))
        return self.__hash


class SomCellCompute(SharedComputeValue):
    def __init__(self, compute_shared, dim_x, hexagonal):
        super().__init__(compute_shared)
        self.dim_x = dim_x
        self.hexagonal = hexagonal

    def __eq__(self, other):
        return super().__eq__(other) \
            and self.dim_x == other.dim_x \
            and self.hexagonal == other.hexagonal

    def __hash__(self):
        return hash((super().__hash__(), self.dim_x, self.hexagonal))

    def compute(self, _, shared_data):
        coords = shared_data[0]
        # coords are 1-based, subtract 1
        col = (coords[:, 0] - 1) + (coords[:, 1] - 1) * self.dim_x
        if self.hexagonal:
            # 1 cell less after every two rows
            col -= col // (self.dim_x * 2 - 1)
        return col


class SomCoordsCompute(SharedComputeValue):
    def __init__(self, shared, column):
        super().__init__(shared)
        self.column = column

    def compute(self, _, shared_data):
        return shared_data[0][:, self.column]

    def __eq__(self, other):
        return self.column == other.column and super().__eq__(other)

    def __hash__(self):
        return hash((super().__hash__(), self.column))


class SomErrorCompute(SharedComputeValue):
    InheritEq = True

    def compute(self, _, shared_data):
        return shared_data[1]


class GetGroups:
    # This assigns instances to selection groups; two instances that are not
    # same cannot be considered equal, period.
    InheritEq = True

    def __init__(self, id_to_group, default, offset):
        self.id_to_group = id_to_group
        self.default = default + offset
        self.offset = offset

    def __call__(self, data, *args, **kwargs):
        return np.array([self.id_to_group.get(id, self.default) - self.offset
                        for id in data.ids])


class OWSOM(OWWidget):
    name = "Self-Organizing Map"
    description = "Computation of self-organizing map."
    icon = "icons/SOM.svg"
    keywords = "self-organizing map, 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"  # QGraphicsView

    _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 Information(OWWidget.Information):
        modified = Msg(
            'The parameter settings have been changed. Press "Start" to '
            "rerun with the new settings."
        )

    class Warning(OWWidget.Warning):
        ignoring_disc_variables = Msg("SOM ignores categorical variables.")
        missing_colors = \
            Msg("Some data instances have undefined value of '{}'.")
        no_defined_colors = \
            Msg("'{}' has no defined values.")
        missing_values = Msg("{}")
        single_attribute = Msg("Data contains a single numeric column.")

    class Error(OWWidget.Error):
        no_numeric_variables = Msg("Data contains no numeric columns.")
        not_enough_data = Msg("SOM needs at least two data rows without missing values.")

    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.scales = self.offsets = None
        self.som = self.cells = self.member_data = None
        self.selection = None

        # self.colors holds a palette or None when we need to draw same-colored
        # circles. This happens by user's choice or when the color attribute
        # is numeric and has no defined values, so we can't construct bins
        self.colors: Optional[DiscretePalette] = None
        self.thresholds = self.bin_labels = None

        box = gui.vBox(self.controlArea, box="SOM")
        shape = gui.comboBox(
            box, self, "", items=("Hexagonal grid", "Square grid"))
        shape.setCurrentIndex(1 - self.hexagonal)
        shape.currentIndexChanged.connect(self.on_parameter_change)

        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,
            callback=self.on_parameter_change,
        )
        self.spin_x = gui.spin(**spinargs)
        with disconnected_spin(self.spin_x):
            self.spin_x.setValue(self.size_x)
        gui.widgetLabel(box3, "×")
        self.spin_y = gui.spin(**spinargs)
        with disconnected_spin(self.spin_y):
            self.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"),
            callback=self.on_parameter_change,
        )

        start = gui.button(
            box, self, "Restart", callback=self.restart_som_pressed,
            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))

        self.opt_controls = self.OptControls(
            shape, auto_dim, self.spin_x, self.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

    @staticmethod
    def _cont_domain(data):
        attrs = data.domain.attributes
        cont_attrs = [var for var in attrs if var.is_continuous]
        if not cont_attrs:
            return None
        return Domain(cont_attrs)

    @Inputs.data
    def set_data(self, data):
        def set_warnings():
            missing = len(data) - len(self.data)
            if missing:
                self.Warning.missing_values(
                    f'{missing} data {pl(missing, "instance")} with undefined value(s) {pl(missing, "is|are")} not shown.')

        cont_x = self.cont_x.copy() if self.cont_x is not None else None
        self.data = self.cont_x = None
        self.offsets = self.scales = None
        new_cont_x = None
        self.closeContext()
        self.clear_messages()

        if data is not None:
            cont_domain = self._cont_domain(data)
            if cont_domain is None:
                self.Error.no_numeric_variables()
            else:
                cont_attrs = cont_domain.attributes
                if len(cont_attrs) < len(data.domain.attributes):
                    self.Warning.ignoring_disc_variables()
                if len(cont_attrs) == 1:
                    self.Warning.single_attribute()

                new_cont_x, mask, self.offsets, self.scales \
                    = SOM.prepare_data(data.transform(cont_domain).X)
                rows = np.sum(mask)
                if rows == len(mask):
                    self.data = data
                elif rows > 1:
                    self.data = data[mask]
                else:
                    self.Error.not_enough_data()

        invalidated = cont_x is None or new_cont_x is None \
            or not array_equal(cont_x, new_cont_x)
        if invalidated:
            self.stop_optimization_and_wait()
            self.clear()

        if self.data is not None:
            self.cont_x = new_cont_x
            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()
        if invalidated:
            with disconnected_spin(self.spin_x), disconnected_spin(self.spin_y):
                self.recompute_dimensions()
            self.start_som()
        else:
            self._redraw()

    def clear(self):
        self.som = 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)

    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)
        self.on_parameter_change()

    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 on_parameter_change(self):
        self.Information.modified()

    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)

        selection_np = np.array(self.selection)
        if action == SomView.SelectionSet:
            selection_np[:] = 0
            selection_np[selection] = 1
        elif action == SomView.SelectionAddToGroup:
            selection_np[selection] = max(1, np.max(selection_np))
        elif action == SomView.SelectionNewGroup:
            selection_np[selection] = 1 + np.max(selection_np)
        elif action & SomView.SelectionRemove:
            selection_np[selection] = 0
        self.selection = selection_np.tolist()
        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
            x, y = x[0], y[0]
            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.tolist())

    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):
        self.Information.modified.clear()
        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.colors 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):
        # if self.colors is None, we use _draw_same_color and don't call
        # this function
        assert self.colors is not None

        color_column = self.data.get_column(self.attr_color)
        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)
            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()
        values = list(values) + ["(N/A)"]
        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}&nbsp;%)
                </td>
            </tr>
            """ for color, val, n in zip(colors, values, distribution) if n) \
            + "</table>"

    def _draw_pie_charts(self, sizes):
        assert self.colors is not None  # if it were, we'd call _draw_same_color

        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):
        assert self.colors is not None  # if it were, we'd call _draw_same_color

        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

        self.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.setInvalidated(True)

        self._optimizer = Optimizer(self.cont_x, self.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.setInvalidated(False)
        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(
                # -1.5: 1 is necessary, 0.5 is to add some border
                -0.5, -1.5 / np.sqrt(3), self.size_x,
                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale - 1 / np.sqrt(3))
        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.som is None:
            self.Outputs.selected_data.send(None)
            self.Outputs.annotated_data.send(None)
            return

        ngroups = int(self.selection is not None) and np.max(self.selection)
        indices = np.zeros(len(self.data), dtype=int)
        id_to_group = {}
        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)
                    group = self.selection[x][y]
                    indices[rows] = group
                    if group > 0:
                        for id_ in self.data.ids[rows]:
                            id_to_group[id_] = group

        cont_domain = self._cont_domain(self.data)
        shared_compute = SomSharedValueCompute(
            cont_domain, self.som, self.offsets, self.scales)
        cell = DiscreteVariable(
            "som_cell",
            values=tuple(
                f"r{row + 1}c{col + 1}"
                for row in range(self.size_y)
                for col in range(self.size_x - (self.hexagonal and row % 2))),
            compute_value=SomCellCompute(shared_compute,
                                         self.size_x, self.hexagonal))
        coordx = ContinuousVariable(
            "som_row",
            number_of_decimals=0,
            compute_value=SomCoordsCompute(shared_compute, 1))
        coordy = ContinuousVariable(
            "som_col",
            number_of_decimals=0,
            compute_value=SomCoordsCompute(shared_compute, 0))
        error = ContinuousVariable(
            "som_error",
            compute_value=SomErrorCompute(shared_compute)
        )
        som_attrs = (cell, coordx, coordy, error)

        grp_values, _ = group_values(
                indices, include_unselected=False, values=None)

        def make_domain(values, default_grp, offset):
            grp_var = DiscreteVariable(
                get_unique_names(self.data.domain, ANNOTATED_DATA_FEATURE_NAME),
                values,
                compute_value=GetGroups(id_to_group, default_grp, offset))

            if not self.data.domain.class_vars:
                class_vars, metas = grp_var, som_attrs
            else:
                class_vars, metas = (), (grp_var,) + som_attrs
            return add_columns(self.data.domain, (), class_vars, metas)

        if np.any(indices):
            sel_domain = make_domain(grp_values, np.nan, 1)
            mask = np.flatnonzero(indices)
            self.Outputs.selected_data.send(
                lazy_table_transform(sel_domain, self.data[mask]))
        else:
            self.Outputs.selected_data.send(None)

        if ngroups > 1:
            sel_domain = make_domain(grp_values + ["Unselected", ], ngroups, 1)
        else:
            sel_domain = make_domain(("No", "Yes"), 0, 0)
        self.Outputs.annotated_data.send(
            lazy_table_transform(sel_domain, self.data))

    def set_color_bins(self):
        self.Warning.no_defined_colors.clear()

        if self.attr_color is None:
            self.thresholds = self.bin_labels = self.colors = None
            return

        if self.attr_color.is_discrete:
            self.thresholds = self.bin_labels = None
            self.colors = self.attr_color.palette
            return

        col = self.data.get_column(self.attr_color)
        col = col[np.isfinite(col)]
        if not col.size:
            self.Warning.no_defined_colors(self.attr_color)
            self.thresholds = self.bin_labels = self.colors = None
            return

        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])
        if not self.bin_labels[0] and binning.labels:
            # Nan's are already filtered out, but it doesn't hurt much
            # to use nanmax/nanmin
            if np.nanmin(col) == np.nanmax(col):
                # Handle a degenerate case with a single value
                # Use the second threshold (because value must be smaller),
                # but the first threshold as label (because that's the
                # actual value in the data.
                self.thresholds = binning.thresholds[1:]
                self.bin_labels = (binning.labels[:1],
                                   binning.short_labels[: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.colors 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 or ([], [])
        if len(labels) <= 1:
            return 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.colors:
            self.report_caption(
                f"Self-organizing map colored by '{self.attr_color.name}'")

    @classmethod
    def migrate_settings(cls, settings, _):
        # previously selection was saved as np.ndarray which is not supported
        # by widget-base, change selection to list
        selection = settings.get('selection')
        if selection is not None and isinstance(selection, np.ndarray):
            settings['selection'] = selection.tolist()


def _draw_hexagon():
    path = QPainterPath()
    s = 0.5 / (np.sqrt(3) / 2)
    path.moveTo(-0.5, -s / 2)
    path.lineTo(-0.5, s / 2)
    path.lineTo(0, s)
    path.lineTo(0.5, s / 2)
    path.lineTo(0.5, -s / 2)
    path.lineTo(0, -s)
    path.lineTo(-0.5, -s / 2)
    return path


_hexagon_path = _draw_hexagon()


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWSOM).run(Table("iris"))

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
user