Beispiel #1
0
class RingTable(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        layout = QGridLayout(self)

        self.table = QTableWidget()

        self.table.setColumnCount(1)
        self.table.setHorizontalHeaderLabels(['name'])

        self.add_rings()

        for i in range(0, 1):
            self.table.resizeColumnToContents(i)

        self.table.horizontalHeader().setSectionResizeMode(
            0, QHeaderView.Stretch)

        self.table.setSortingEnabled(True)
        self.table.setSelectionBehavior(QTableWidget.SelectRows)
        self.table.setEditTriggers(QTableWidget.NoEditTriggers)

        self.filterEdit = QLineEdit()
        self.filterEdit.textChanged.connect(self.apply_filter)
        self.filterEdit.setClearButtonEnabled(True)

        self.filter_columns = QComboBox()
        self.filter_columns.addItem("name")
        self.filter_columns.currentTextChanged.connect(
            self.change_filter_method)

        self.name_regex_option = QComboBox()
        self.name_regex_option.addItem("case-insensitive")
        self.name_regex_option.addItem("case-sensitive")
        self.name_regex_option.currentTextChanged.connect(self.apply_filter)
        self.name_regex_option.setVisible(
            self.filter_columns.currentText() == "name")

        layout.addWidget(self.table, 0, 0, 1, 4)
        layout.addWidget(QLabel("filter based on"), 1, 0)
        layout.addWidget(self.filter_columns, 1, 1)
        layout.addWidget(self.name_regex_option, 1, 2)
        layout.addWidget(self.filterEdit, 1, 3)

        self.change_filter_method("name")

    def change_filter_method(self, text):
        if text == "name":
            self.filterEdit.setToolTip("name regex")
            self.name_regex_option.setVisible(True)

        self.apply_filter()

    def apply_filter(self, *args):
        text = self.filterEdit.text()
        if text:
            if self.filter_columns.currentText() == "name":
                m = QRegularExpression(text)
                if m.isValid():
                    if self.name_regex_option.currentText(
                    ) == "case-insensitive":
                        m.setPatternOptions(
                            QRegularExpression.CaseInsensitiveOption)

                    m.optimize()
                    filter = lambda row_num: m.match(
                        self.table.item(row_num, 0).text()).hasMatch()
                else:
                    return

        else:
            filter = lambda row: True

        for i in range(0, self.table.rowCount()):
            self.table.setRowHidden(i, not filter(i))

    def add_rings(self):
        from AaronTools.ring import Ring

        names = []
        for lib in [Ring.AARON_LIBS, Ring.BUILTIN]:
            if not os.path.exists(lib):
                continue
            for ring in os.listdir(lib):
                name, ext = os.path.splitext(ring)
                if not any(".%s" % x == ext for x in read_types):
                    continue

                if name in names:
                    continue

                names.append(name)

                row = self.table.rowCount()
                self.table.insertRow(row)
                self.table.setItem(row, 0, QTableWidgetItem(name))

        self.ring_list = names
Beispiel #2
0
class LigandTable(QWidget):
    def __init__(
        self,
        parent=None,
        singleSelect=False,
        maxDenticity=None,
        include_substituents=False,
    ):
        super().__init__(parent)

        self._include_substituents = include_substituents

        layout = QGridLayout(self)

        self.table = QTableWidget()

        self.table.setColumnCount(3)
        self.table.setHorizontalHeaderLabels(
            ['name', 'denticity', 'coordinating elements'])

        self.add_ligands(maxDenticity)

        for i in range(0, 3):
            self.table.resizeColumnToContents(i)

        self.table.horizontalHeader().setStretchLastSection(False)
        self.table.horizontalHeader().setSectionResizeMode(
            0, QHeaderView.Fixed)
        self.table.horizontalHeader().setSectionResizeMode(
            1, QHeaderView.Fixed)
        self.table.horizontalHeader().setSectionResizeMode(
            2, QHeaderView.Stretch)

        self.table.setSortingEnabled(True)
        self.table.setSelectionBehavior(QTableWidget.SelectRows)
        if singleSelect:
            self.table.setSelectionMode(QTableWidget.SingleSelection)
        self.table.setEditTriggers(QTableWidget.NoEditTriggers)

        self.filterEdit = QLineEdit()
        self.filterEdit.textChanged.connect(self.apply_filter)
        self.filterEdit.setClearButtonEnabled(True)

        self.filter_columns = QComboBox()
        self.filter_columns.addItem("name")
        self.filter_columns.addItem("denticity")
        self.filter_columns.addItem("coordinating elements")
        self.filter_columns.currentTextChanged.connect(
            self.change_filter_method)

        self.name_regex_option = QComboBox()
        self.name_regex_option.addItem("case-insensitive")
        self.name_regex_option.addItem("case-sensitive")
        self.name_regex_option.currentTextChanged.connect(self.apply_filter)
        self.name_regex_option.setVisible(
            self.filter_columns.currentText() == "name")

        self.coordinating_elements_method = QComboBox()
        self.coordinating_elements_method.addItem("exactly")
        self.coordinating_elements_method.addItem("at least")
        self.coordinating_elements_method.currentTextChanged.connect(
            self.apply_filter)
        self.coordinating_elements_method.setVisible(
            self.filter_columns.currentText() == "coordinating elements")

        layout.addWidget(self.table, 0, 0, 1, 4)
        layout.addWidget(QLabel("filter based on"), 1, 0)
        layout.addWidget(self.filter_columns, 1, 1)
        layout.addWidget(self.coordinating_elements_method, 1, 2)
        layout.addWidget(self.name_regex_option, 1, 2)
        layout.addWidget(self.filterEdit, 1, 3)

        self.change_filter_method("name")

    def add_ligands(self, maxDenticity=None):
        from AaronTools.component import Component

        names = []

        for lib in [Component.AARON_LIBS, Component.BUILTIN]:
            if not os.path.exists(lib):
                continue
            for lig in os.listdir(lib):
                name, ext = os.path.splitext(lig)
                if not any(".%s" % x == ext for x in read_types):
                    continue

                if name in names:
                    continue

                names.append(name)

                geom = Geometry(
                    os.path.join(lib, lig),
                    refresh_connected=False,
                    refresh_ranks=False,
                )

                key_atoms = [geom.atoms[i] for i in geom.other["key_atoms"]]

                if maxDenticity and len(key_atoms) > maxDenticity:
                    continue

                row = self.table.rowCount()
                self.table.insertRow(row)
                self.table.setItem(row, 0, QTableWidgetItem(name))

                #this is an integer, so I need to initialize it then set the data
                denticity = QTableWidgetItem()
                denticity.setData(Qt.DisplayRole, len(key_atoms))
                self.table.setItem(row, 1, denticity)

                self.table.setItem(
                    row, 2,
                    QTableWidgetItem(", ".join(
                        sorted([atom.element for atom in key_atoms]))))

        if self._include_substituents:
            from AaronTools.substituent import Substituent

            for lib in [Substituent.AARON_LIBS, Substituent.BUILTIN]:
                if not os.path.exists(lib):
                    continue
                for sub in os.listdir(lib):
                    name, ext = os.path.splitext(sub)
                    if not any(".%s" % x == ext for x in read_types):
                        continue

                    if name in names:
                        continue

                    names.append(name)

                    geom = Geometry(
                        os.path.join(lib, sub),
                        refresh_connected=False,
                        refresh_ranks=False,
                    )

                    key_atoms = [geom.atoms[0]]

                    if maxDenticity and len(key_atoms) > maxDenticity:
                        continue

                    row = self.table.rowCount()
                    self.table.insertRow(row)
                    self.table.setItem(row, 0, QTableWidgetItem(name))

                    #this is an integer, so I need to initialize it then set the data
                    denticity = QTableWidgetItem()
                    denticity.setData(Qt.DisplayRole, len(key_atoms))
                    self.table.setItem(row, 1, denticity)

                    self.table.setItem(
                        row, 2,
                        QTableWidgetItem(", ".join(
                            sorted([atom.element for atom in key_atoms]))))

        self.ligand_list = names

    def change_filter_method(self, text):
        if text == "coordinating elements":
            self.filterEdit.setToolTip("comma and/or space delimited elements")
            self.coordinating_elements_method.setVisible(True)
            self.name_regex_option.setVisible(False)
        elif text == "name":
            self.filterEdit.setToolTip("name regex")
            self.coordinating_elements_method.setVisible(False)
            self.name_regex_option.setVisible(True)
        elif text == "denticity":
            self.filterEdit.setToolTip("number of key atoms")
            self.coordinating_elements_method.setVisible(False)
            self.name_regex_option.setVisible(False)

        self.apply_filter()

    def apply_filter(self, *args):
        text = self.filterEdit.text()
        if text:
            if self.filter_columns.currentText() == "name":
                m = QRegularExpression(text)
                if m.isValid():
                    if self.name_regex_option.currentText(
                    ) == "case-insensitive":
                        m.setPatternOptions(
                            QRegularExpression.CaseInsensitiveOption)

                    m.optimize()
                    filter = lambda row_num: m.match(
                        self.table.item(row_num, 0).text()).hasMatch()
                else:
                    return

            elif self.filter_columns.currentText() == "denticity":
                if text.isdigit():
                    filter = lambda row_num: int(
                        self.table.item(row_num, 1).text()) == int(text)
                else:
                    filter = lambda row: True

            elif self.filter_columns.currentText() == "coordinating elements":
                method = self.coordinating_elements_method.currentText()

                def filter(row_num):
                    row_key_atoms = [
                        item.strip() for item in self.table.item(
                            row_num, 2).text().split(',')
                    ]
                    search_atoms = []
                    for item in text.split():
                        for ele in item.split(','):
                            if ele.strip() != "":
                                search_atoms.append(ele)

                    if method == "exactly":
                        if all([row_key_atoms.count(element) == search_atoms.count(element) for element in set(search_atoms)]) and \
                            all([row_key_atoms.count(element) == search_atoms.count(element) for element in set(row_key_atoms)]):
                            return True
                        else:
                            return False

                    elif method == "at least":
                        if all([
                                row_key_atoms.count(element) >=
                                search_atoms.count(element)
                                for element in set(search_atoms)
                        ]):
                            return True
                        else:
                            return False

        else:
            filter = lambda row: True

        for i in range(0, self.table.rowCount()):
            self.table.setRowHidden(i, not filter(i))
Beispiel #3
0
class SubstituentTable(QWidget):
    def __init__(self, parent=None, singleSelect=False):
        super().__init__(parent)

        layout = QGridLayout(self)

        self.table = QTableWidget()

        self.table.setColumnCount(3)
        self.table.setHorizontalHeaderLabels(
            ['name', 'conformers', 'conf. angle'])

        self.add_subs()

        for i in range(0, 3):
            self.table.resizeColumnToContents(i)

        self.table.horizontalHeader().setStretchLastSection(False)
        self.table.horizontalHeader().setSectionResizeMode(
            0, QHeaderView.Fixed)
        self.table.horizontalHeader().setSectionResizeMode(
            1, QHeaderView.Fixed)
        self.table.horizontalHeader().setSectionResizeMode(
            2, QHeaderView.Stretch)

        self.table.setSortingEnabled(True)
        self.table.setSelectionBehavior(QTableWidget.SelectRows)
        if singleSelect:
            self.table.setSelectionMode(QTableWidget.SingleSelection)
        self.table.setEditTriggers(QTableWidget.NoEditTriggers)

        self.filterEdit = QLineEdit()
        self.filterEdit.textChanged.connect(self.apply_filter)
        self.filterEdit.setClearButtonEnabled(True)

        self.filter_columns = QComboBox()
        self.filter_columns.addItem("name")
        self.filter_columns.addItem("conformers")
        self.filter_columns.addItem("conf. angle")
        self.filter_columns.currentTextChanged.connect(
            self.change_filter_method)

        self.name_regex_option = QComboBox()
        self.name_regex_option.addItem("case-insensitive")
        self.name_regex_option.addItem("case-sensitive")
        self.name_regex_option.currentTextChanged.connect(self.apply_filter)
        self.name_regex_option.setVisible(
            self.filter_columns.currentText() == "name")

        layout.addWidget(self.table, 0, 0, 1, 4)
        layout.addWidget(QLabel("filter based on"), 1, 0)
        layout.addWidget(self.filter_columns, 1, 1)
        layout.addWidget(self.name_regex_option, 1, 2)
        layout.addWidget(self.filterEdit, 1, 3)

        self.change_filter_method("name")

    def change_filter_method(self, text):
        if text == "name":
            self.filterEdit.setToolTip("name regex")
            self.name_regex_option.setVisible(True)
        elif text == "conformers":
            self.filterEdit.setToolTip("number of conformers")
            self.name_regex_option.setVisible(False)
        elif text == "conf. angle":
            self.filterEdit.setToolTip("angle between conformers")
            self.name_regex_option.setVisible(False)

        self.apply_filter()

    def apply_filter(self, *args):
        text = self.filterEdit.text()
        if text:
            if self.filter_columns.currentText() == "name":
                m = QRegularExpression(text)
                if m.isValid():
                    if self.name_regex_option.currentText(
                    ) == "case-insensitive":
                        m.setPatternOptions(
                            QRegularExpression.CaseInsensitiveOption)

                    m.optimize()
                    filter = lambda row_num: m.match(
                        self.table.item(row_num, 0).text()).hasMatch()
                else:
                    return

            elif self.filter_columns.currentText() == "conformers":
                if text.isdigit():
                    filter = lambda row_num: int(
                        self.table.item(row_num, 1).text()) == int(text)
                else:
                    filter = lambda row: True

            elif self.filter_columns.currentText() == "conf. angle":
                if text.isdigit():
                    filter = lambda row_num: int(
                        self.table.item(row_num, 2).text()) == int(text)
                else:
                    filter = lambda row: True

        else:
            filter = lambda row: True

        for i in range(0, self.table.rowCount()):
            self.table.setRowHidden(i, not filter(i))

    def add_subs(self):
        from AaronTools.substituent import Substituent

        names = []

        for lib in [Substituent.AARON_LIBS, Substituent.BUILTIN]:
            if not os.path.exists(lib):
                continue
            for ring in os.listdir(lib):
                name, ext = os.path.splitext(ring)
                if not any(".%s" % x == ext for x in read_types):
                    continue

                if name in names:
                    continue

                names.append(name)

                geom = Geometry(
                    os.path.join(lib, ring),
                    refresh_connected=False,
                    refresh_ranks=False,
                )

                conf_info = re.search(r"CF:(\d+),(\d+)", geom.comment)

                row = self.table.rowCount()
                self.table.insertRow(row)
                self.table.setItem(row, 0, QTableWidgetItem(name))

                #the next two items are integers - need to initialize then setData so they sort and display correctly
                conf_num = QTableWidgetItem()
                conf_num.setData(Qt.DisplayRole, conf_info.group(1))
                self.table.setItem(row, 1, conf_num)

                conf_angle = QTableWidgetItem()
                conf_angle.setData(Qt.DisplayRole, conf_info.group(2))
                self.table.setItem(row, 2, conf_angle)

        self.substituent_list = names
Beispiel #4
0
class PrecisionRotate(ToolInstance):

    help = "https://github.com/QChASM/SEQCROW/wiki/Rotate-Tool"
    SESSION_ENDURING = False
    SESSION_SAVE = False

    def __init__(self, session, name):
        super().__init__(session, name)

        self.tool_window = MainToolWindow(self)

        self.settings = _PrecisionRotateSettings(session, name)

        self.bonds = {}
        self.bond_centers = {}
        self.groups = {}
        self.perpendiculars = {}
        self.perp_centers = {}
        self.manual_center = {}

        self._build_ui()

        self._show_rot_vec = self.session.triggers.add_handler(
            SELECTION_CHANGED, self.show_rot_vec)
        global_triggers = get_triggers()
        self._changes = global_triggers.add_handler("changes done",
                                                    self.show_rot_vec)

        self.show_rot_vec()

    def _build_ui(self):
        layout = QGridLayout()

        layout.addWidget(QLabel("center of rotation:"), 0, 0, 1, 1,
                         Qt.AlignLeft | Qt.AlignVCenter)

        self.cor_button = QComboBox()
        self.cor_button.addItems(
            ["automatic", "select atoms", "view's center of rotation"])
        layout.addWidget(self.cor_button, 0, 1, 1, 1, Qt.AlignTop)

        self.set_cor_selection = QPushButton("set selection")
        self.cor_button.currentTextChanged.connect(
            lambda t, widget=self.set_cor_selection: widget.setEnabled(
                t == "select atoms"))
        self.set_cor_selection.clicked.connect(self.manual_cor)
        layout.addWidget(self.set_cor_selection, 0, 2, 1, 1, Qt.AlignTop)
        self.set_cor_selection.setEnabled(False)

        layout.addWidget(QLabel("rotation vector:"), 1, 0, 1, 1,
                         Qt.AlignLeft | Qt.AlignVCenter)

        self.vector_option = QComboBox()
        self.vector_option.addItems([
            "axis", "view axis", "bond", "perpendicular to plane",
            "centroid of atoms", "custom"
        ])
        layout.addWidget(self.vector_option, 1, 1, 1, 1, Qt.AlignVCenter)

        vector = QWidget()
        vector.setToolTip("vector will be normalized before rotating")
        vector_layout = QHBoxLayout(vector)
        vector_layout.setContentsMargins(0, 0, 0, 0)
        self.vector_x = QDoubleSpinBox()
        self.vector_y = QDoubleSpinBox()
        self.vector_z = QDoubleSpinBox()
        self.vector_z.setValue(1.0)
        for c, t in zip([self.vector_x, self.vector_y, self.vector_z],
                        [" x", " y", " z"]):
            c.setSingleStep(0.01)
            c.setRange(-100, 100)
            # c.setSuffix(t)
            c.valueChanged.connect(self.show_rot_vec)
            vector_layout.addWidget(c)

        layout.addWidget(vector, 1, 2, 1, 1, Qt.AlignTop)
        vector.setVisible(self.vector_option.currentText() == "custom")
        self.vector_option.currentTextChanged.connect(
            lambda text, widget=vector: widget.setVisible(text == "custom"))

        self.view_axis = QComboBox()
        self.view_axis.addItems(["z", "y", "x"])
        layout.addWidget(self.view_axis, 1, 2, 1, 1, Qt.AlignTop)
        self.view_axis.setVisible(
            self.vector_option.currentText() == "view axis")
        self.vector_option.currentTextChanged.connect(
            lambda text, widget=self.view_axis: widget.setVisible(text ==
                                                                  "view axis"))

        self.axis = QComboBox()
        self.axis.addItems(["z", "y", "x"])
        layout.addWidget(self.axis, 1, 2, 1, 1, Qt.AlignTop)
        self.axis.setVisible(self.vector_option.currentText() == "axis")
        self.vector_option.currentTextChanged.connect(
            lambda text, widget=self.axis: widget.setVisible(text == "axis"))

        self.bond_button = QPushButton("set selected bond")
        self.bond_button.clicked.connect(self.set_bonds)
        layout.addWidget(self.bond_button, 1, 2, 1, 1, Qt.AlignTop)
        self.bond_button.setVisible(self.vector_option.currentText() == "bond")
        self.vector_option.currentTextChanged.connect(
            lambda text, widget=self.bond_button: widget.setVisible(text ==
                                                                    "bond"))

        self.perp_button = QPushButton("set selected atoms")
        self.perp_button.clicked.connect(self.set_perpendicular)
        layout.addWidget(self.perp_button, 1, 2, 1, 1, Qt.AlignTop)
        self.perp_button.setVisible(
            self.vector_option.currentText() == "perpendicular to plane")
        self.vector_option.currentTextChanged.connect(
            lambda text, widget=self.perp_button: widget.setVisible(
                text == "perpendicular to plane"))

        self.group_button = QPushButton("set selected atoms")
        self.group_button.clicked.connect(self.set_group)
        layout.addWidget(self.group_button, 1, 2, 1, 1, Qt.AlignTop)
        self.group_button.setVisible(
            self.vector_option.currentText() == "centroid of atoms")
        self.vector_option.currentTextChanged.connect(
            lambda text, widget=self.group_button: widget.setVisible(
                text == "centroid of atoms"))

        layout.addWidget(QLabel("angle:"), 2, 0, 1, 1,
                         Qt.AlignLeft | Qt.AlignVCenter)

        self.angle = QDoubleSpinBox()
        self.angle.setRange(-360, 360)
        self.angle.setSingleStep(5)
        self.angle.setSuffix("°")
        layout.addWidget(self.angle, 2, 1, 1, 1,
                         Qt.AlignLeft | Qt.AlignVCenter)

        layout.addWidget(QLabel("preview rotation axis:"), 3, 0, 1, 1,
                         Qt.AlignLeft | Qt.AlignVCenter)
        self.display_rot_vec = QCheckBox()
        self.display_rot_vec.setCheckState(Qt.Checked)
        self.display_rot_vec.stateChanged.connect(self.show_rot_vec)
        layout.addWidget(self.display_rot_vec, 3, 1, 1, 1,
                         Qt.AlignLeft | Qt.AlignVCenter)

        rotate_button = QPushButton("rotate selected atoms")
        rotate_button.clicked.connect(self.do_rotate)
        layout.addWidget(rotate_button, 4, 0, 1, 3, Qt.AlignTop)
        self.rotate_button = rotate_button

        self.status_bar = QStatusBar()
        self.status_bar.setSizeGripEnabled(False)
        layout.addWidget(self.status_bar, 5, 0, 1, 3, Qt.AlignTop)

        self.vector_option.currentTextChanged.connect(self.show_auto_status)
        self.cor_button.currentIndexChanged.connect(
            lambda *args: self.show_auto_status("select atoms"))

        self.cor_button.currentIndexChanged.connect(self.show_rot_vec)
        self.set_cor_selection.clicked.connect(self.show_rot_vec)
        self.vector_option.currentIndexChanged.connect(self.show_rot_vec)
        self.axis.currentIndexChanged.connect(self.show_rot_vec)
        self.view_axis.currentIndexChanged.connect(self.show_rot_vec)
        self.bond_button.clicked.connect(self.show_rot_vec)
        self.perp_button.clicked.connect(self.show_rot_vec)
        self.group_button.clicked.connect(self.show_rot_vec)

        layout.setRowStretch(0, 0)
        layout.setRowStretch(1, 0)
        layout.setRowStretch(2, 0)
        layout.setRowStretch(3, 0)
        layout.setRowStretch(4, 0)
        layout.setRowStretch(5, 1)

        layout.setColumnStretch(0, 0)
        layout.setColumnStretch(1, 1)
        layout.setColumnStretch(2, 1)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def manual_cor(self, *args):
        selection = selected_atoms(self.session)

        models = {}
        for atom in selection:
            if atom.structure not in models:
                models[atom.structure] = [atom]
            else:
                models[atom.structure].append(atom)

        self.manual_center = {}
        for model in models:
            atoms = models[model]
            coords = np.array([atom.coord for atom in atoms])
            self.manual_center[model] = np.mean(coords, axis=0)

    def show_auto_status(self, text):
        if self.cor_button.currentText() == "automatic":
            if text == "bond":
                self.status_bar.showMessage(
                    "center set to one of the bonded atoms")
            elif text == "perpendicular to plane":
                self.status_bar.showMessage("center set to centroid of atoms")
            else:
                self.status_bar.showMessage(
                    "center set to centroid of rotating atoms")

        elif self.cor_button.currentText() == "select atoms":
            self.status_bar.showMessage(
                "center set to centroid of specified atoms")

        else:
            self.status_bar.showMessage(
                "center set to view's center of rotation")

    def set_bonds(self, *args):
        bonds = selected_bonds(self.session)
        if len(bonds) == 0:
            self.session.logger.error("no bonds selected")
            return

        models = [bond.structure for bond in bonds]
        if any(models.count(m) > 1 for m in models):
            self.session.logger.error(
                "multiple bonds selected on the same structure")
            return

        self.bonds = {
            model: (bond.atoms[0].coord - bond.atoms[1].coord)
            for model, bond in zip(models, bonds)
        }
        self.bond_centers = {
            model: bond.atoms[1].coord
            for model, bond in zip(models, bonds)
        }

    def set_perpendicular(self, *args):
        atoms = selected_atoms(self.session)
        if len(atoms) == 0:
            self.session.logger.error("no atoms selected")
            return

        self.perpendiculars = {}
        self.perp_centers = {}

        models = set(atom.structure for atom in atoms)
        for model in models:
            atom_coords = []
            for atom in atoms:
                if atom.structure is model:
                    atom_coords.append(atom.coord)

            if len(atom_coords) < 3:
                self.session.logger.error("fewer than 3 atoms selected on %s" %
                                          model.atomspec)
                continue

            xyz = np.array(atom_coords)
            xyz -= np.mean(atom_coords, axis=0)
            R = np.dot(xyz.T, xyz)
            u, s, vh = np.linalg.svd(R, compute_uv=True)
            vector = u[:, -1]

            self.perpendiculars[model] = vector
            self.perp_centers[model] = np.mean(atom_coords, axis=0)

    def set_group(self, *args):
        atoms = selected_atoms(self.session)
        if len(atoms) == 0:
            self.session.logger.error("no atoms selected")
            return

        self.groups = {}

        models = set(atom.structure for atom in atoms)
        for model in models:
            atom_coords = []
            for atom in atoms:
                if atom.structure is model:
                    atom_coords.append(atom.coord)

            self.groups[model] = np.mean(atom_coords, axis=0)

    def do_rotate(self, *args):
        selection = selected_atoms(self.session)

        models = {}
        for atom in selection:
            if atom.structure not in models:
                models[atom.structure] = [atom]
            else:
                models[atom.structure].append(atom)

        if len(models.keys()) == 0:
            return

        if self.vector_option.currentText() == "axis":
            if self.axis.currentText() == "z":
                vector = np.array([0., 0., 1.])
            elif self.axis.currentText() == "y":
                vector = np.array([0., 1., 0.])
            elif self.axis.currentText() == "x":
                vector = np.array([1., 0., 0.])

        elif self.vector_option.currentText() == "view axis":
            if self.view_axis.currentText() == "z":
                vector = self.session.view.camera.get_position().axes()[2]
            elif self.view_axis.currentText() == "y":
                vector = self.session.view.camera.get_position().axes()[1]
            elif self.view_axis.currentText() == "x":
                vector = self.session.view.camera.get_position().axes()[0]

        elif self.vector_option.currentText() == "bond":
            vector = self.bonds

        elif self.vector_option.currentText() == "perpendicular to plane":
            vector = self.perpendiculars

        elif self.vector_option.currentText() == "centroid of atoms":
            vector = self.groups

        elif self.vector_option.currentText() == "custom":
            x = self.vector_x.value()
            y = self.vector_y.value()
            z = self.vector_z.value()
            vector = np.array([x, y, z])

        angle = np.deg2rad(self.angle.value())

        center = {}
        for model in models:
            atoms = models[model]
            coords = np.array([atom.coord for atom in atoms])
            center[model] = np.mean(coords, axis=0)

        if self.cor_button.currentText() == "automatic":
            if self.vector_option.currentText() == "perpendicular to plane":
                center = self.perp_centers

            elif self.vector_option.currentText() == "bond":
                center = self.bond_centers

        elif self.cor_button.currentText() == "select atoms":
            center = self.manual_center

        else:
            center = self.session.main_view.center_of_rotation

        for model in models:
            if isinstance(vector, dict):
                if model not in vector.keys():
                    continue
                else:
                    v = vector[model]

            else:
                v = vector

            if isinstance(center, dict):
                if model not in center.keys():
                    continue
                else:
                    c = center[model]

            else:
                c = center

            if self.vector_option.currentText(
            ) == "centroid of atoms" and self.cor_button.currentText(
            ) != "automatic":
                v = v - c

            v = v / np.linalg.norm(v)
            q = np.hstack(([np.cos(angle / 2)], v * np.sin(angle / 2)))

            q /= np.linalg.norm(q)
            qs = q[0]
            qv = q[1:]

            xyz = np.array([a.coord for a in models[model]])
            xyz -= c
            xprod = np.cross(qv, xyz)
            qs_xprod = 2 * qs * xprod
            qv_xprod = 2 * np.cross(qv, xprod)

            xyz += qs_xprod + qv_xprod + c
            for t, coord in zip(models[model], xyz):
                t.coord = coord

    def show_rot_vec(self, *args):
        for model in self.session.models.list(type=Generic3DModel):
            if model.name == "rotation vector":
                model.delete()

        if self.display_rot_vec.checkState() == Qt.Unchecked:
            return

        selection = selected_atoms(self.session)

        if len(selection) == 0:
            return

        models = {}
        for atom in selection:
            if atom.structure not in models:
                models[atom.structure] = [atom]
            else:
                models[atom.structure].append(atom)

        if len(models.keys()) == 0:
            return

        if self.vector_option.currentText() == "axis":
            if self.axis.currentText() == "z":
                vector = np.array([0., 0., 1.])
            elif self.axis.currentText() == "y":
                vector = np.array([0., 1., 0.])
            elif self.axis.currentText() == "x":
                vector = np.array([1., 0., 0.])

        elif self.vector_option.currentText() == "view axis":
            if self.view_axis.currentText() == "z":
                vector = self.session.view.camera.get_position().axes()[2]
            elif self.view_axis.currentText() == "y":
                vector = self.session.view.camera.get_position().axes()[1]
            elif self.view_axis.currentText() == "x":
                vector = self.session.view.camera.get_position().axes()[0]

        elif self.vector_option.currentText() == "bond":
            vector = self.bonds

        elif self.vector_option.currentText() == "perpendicular to plane":
            vector = self.perpendiculars

        elif self.vector_option.currentText() == "centroid of atoms":
            vector = self.groups

        elif self.vector_option.currentText() == "custom":
            x = self.vector_x.value()
            y = self.vector_y.value()
            z = self.vector_z.value()
            vector = np.array([x, y, z])

        center = {}
        for model in models:
            atoms = models[model]
            coords = np.array([atom.coord for atom in atoms])
            center[model] = np.mean(coords, axis=0)

        if self.cor_button.currentText() == "automatic":
            if self.vector_option.currentText() == "perpendicular to plane":
                center = self.perp_centers

            elif self.vector_option.currentText() == "bond":
                center = self.bond_centers

        elif self.cor_button.currentText() == "select atoms":
            center = self.manual_center

        else:
            center = self.session.main_view.center_of_rotation

        for model in models:
            if isinstance(vector, dict):
                if model not in vector.keys():
                    continue
                else:
                    v = vector[model]

            else:
                v = vector

            if isinstance(center, dict):
                if model not in center.keys():
                    continue
                else:
                    c = center[model]

            else:
                c = center

            if self.vector_option.currentText(
            ) == "centroid of atoms" and self.cor_button.currentText(
            ) != "automatic":
                v = v - c

            if np.linalg.norm(v) == 0:
                continue

            residues = []
            for atom in models[model]:
                if atom.residue not in residues:
                    residues.append(atom.residue)

            v_c = c + v

            s = ".color red\n"
            s += ".arrow %10.6f %10.6f %10.6f   %10.6f %10.6f %10.6f   0.2 0.4 0.7\n" % (
                *c, *v_c)

            stream = BytesIO(bytes(s, 'utf-8'))
            bild_obj, status = read_bild(self.session, stream,
                                         "rotation vector")

            self.session.models.add(bild_obj, parent=model)

    def delete(self):
        self.session.triggers.remove_handler(self._show_rot_vec)
        global_triggers = get_triggers()
        global_triggers.remove_handler(self._changes)

        for model in self.session.models.list(type=Generic3DModel):
            if model.name == "rotation vector":
                model.delete()

        return super().delete()

    def close(self):
        self.session.triggers.remove_handler(self._show_rot_vec)
        global_triggers = get_triggers()
        global_triggers.remove_handler(self._changes)

        for model in self.session.models.list(type=Generic3DModel):
            if model.name == "rotation vector":
                model.delete()

        return super().close()