Пример #1
0
class CoordinationComplexVomit(ToolInstance):
    
    help = "https://github.com/QChASM/SEQCROW/wiki/Coordination-Complex-Generator-Tool"
    
    def __init__(self, session, name):       
        super().__init__(session, name)
        
        self.tool_window = MainToolWindow(self)
        
        self._build_ui()
    
    def _build_ui(self):
        layout = QFormLayout()

        create_coord_items(self, layout)
        
        do_it = QPushButton("create coordination complexes")
        do_it.clicked.connect(self.create_complexes)
        layout.addRow(do_it)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def create_complexes(self):
        c2_symmetric = []
        ligands = []
        minimize = self.minimize.checkState() == Qt.Checked
        for i in range(0, self.ligand_table.rowCount() - 1):
            ligand_name = self.ligand_table.item(i, 0).text()
            ligands.append(ligand_name)
            
            checkbox = self.ligand_table.cellWidget(i, 1).layout().itemAt(0).widget()
            c2 = checkbox.checkState() == Qt.Checked
            c2_symmetric.append(c2)
        
        rescols, formula = ResidueCollection.get_coordination_complexes(
            self.element.text(),
            ligands,
            self.vsepr.currentText(),
            c2_symmetric=c2_symmetric,
            minimize=minimize,
            session=self.session,
        )
        
        self.session.logger.info("generic formula is %s" % formula)
        models = [rescol.get_chimera(self.session) for rescol in rescols]

        self.session.models.add(models)
        for model in models:
            apply_seqcrow_preset(model, fallback="Ball-Stick-Endcap")
Пример #2
0
class PrecisionRotate(ToolInstance):

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

    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])

        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

            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()
Пример #3
0
class TestRunner(ToolInstance):
    def __init__(self, session, name):
        super().__init__(session, name)
        self.tool_window = MainToolWindow(self)

        self._build_ui()

    def _build_ui(self):
        """
        ui should have:
            * table with a list of available tests and show results after they are done
            * way to filter tests
            * button to run tests
        """
        layout = QFormLayout()

        # table to list test classes and the results
        self.table = QTableWidget()
        self.table.setColumnCount(2)
        self.table.setHorizontalHeaderLabels(["test", "result"])
        self.table.horizontalHeader().setSectionResizeMode(
            0,
            self.table.horizontalHeader().Interactive)
        self.table.setEditTriggers(QTableWidget.NoEditTriggers)
        self.table.horizontalHeader().setSectionResizeMode(
            1,
            self.table.horizontalHeader().Stretch)
        self.table.setSizePolicy(QSizePolicy.MinimumExpanding,
                                 QSizePolicy.MinimumExpanding)
        self.table.setSelectionBehavior(self.table.SelectRows)
        layout.addRow(self.table)

        self.filter = QLineEdit()
        self.filter.setPlaceholderText("filter test names")
        self.filter.setClearButtonEnabled(True)
        self.filter.textChanged.connect(self.apply_filter)
        layout.addRow(self.filter)

        self.profile = QCheckBox()
        self.profile.setToolTip("profile functions called during testing "
                                "using cProfile")
        layout.addRow("profile calls:", self.profile)

        self.run_button = QPushButton("run tests")
        self.run_button.clicked.connect(self.run_tests)
        self.run_button.setToolTip(
            "if no tests are selected on the table, run all tests\n" +
            "otherwise, run selected tests")
        layout.addRow(self.run_button)

        self.fill_table()
        self.table.resizeColumnToContents(0)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def apply_filter(self, text=None):
        """filter table to only show tests matching text"""
        if text is None:
            text = self.filter.text()

        if text:
            text = text.replace("(", "\(")
            text = text.replace(")", "\)")
            m = QRegularExpression(text)
            m.setPatternOptions(QRegularExpression.CaseInsensitiveOption)
            if m.isValid():
                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 fill_table(self):
        """adds test names to the table"""
        mgr = self.session.test_manager

        for name in mgr.tests.keys():
            row = self.table.rowCount()
            self.table.insertRow(row)
            test_name = QTableWidgetItem()
            test_name.setData(Qt.DisplayRole, name)
            self.table.setItem(row, 0, test_name)

    def run_tests(self):
        """run the tests selected on the table and show the results"""
        from TestManager.commands.test import test

        test_list = []

        use_selected = True
        for row in self.table.selectionModel().selectedRows():
            if self.table.isRowHidden(row.row()):
                continue

            test_name = self.table.item(row.row(), 0).text()
            test_list.append(test_name)

        if not test_list:
            use_selected = False
            for i in range(0, self.table.rowCount()):
                if self.table.isRowHidden(i):
                    continue

                test_name = self.table.item(i, 0).text()
                test_list.append(test_name)

            if not test_list:
                test_list = ["all"]

        results, stats = test(self.session,
                              test_list,
                              profile=self.profile.checkState() == Qt.Checked)

        cell_widgets = []

        for name in results:
            widget = QWidget()
            widget_layout = QHBoxLayout(widget)
            widget_layout.setContentsMargins(0, 0, 0, 0)

            success_button = get_button("success")
            fail_button = get_button("fail")
            skip_button = get_button("skip")
            error_button = get_button("error")
            expected_fail_button = get_button("expected fail")
            unexpected_success_button = get_button("unexpected success")

            success_count = 0
            fail_count = 0
            error_count = 0
            unexpected_success_count = 0
            expected_fail_count = 0
            skip_count = 0

            success_tooltip = "Successes:\n"
            fail_tooltip = "Failed tests:\n"
            error_tooltip = "Errors during test:\n"
            unexpected_success_tooltip = "Unexpected successes:\n"
            expected_fail_tooltip = "Expected fails:\n"
            skip_tooltip = "Skipped tests:\n"

            for case in results[name]:
                result, msg = results[name][case]
                if result == "success":
                    success_count += 1
                    success_tooltip += "%s.%s: %s\n" % (
                        case.__class__.__qualname__, case._testMethodName, msg)

                elif result == "fail":
                    fail_count += 1
                    fail_tooltip += "%s.%s failed: %s\n" % (
                        case.__class__.__qualname__, case._testMethodName, msg)

                elif result == "error":
                    error_count += 1
                    error_tooltip += "error during %s.%s: %s\n" % (
                        case.__class__.__qualname__, case._testMethodName, msg)

                elif result == "expected_failure":
                    expected_fail_count += 1
                    expected_fail_tooltip += "intended failure during %s.%s: %s\n" % (
                        case.__class__.__qualname__, case._testMethodName, msg)

                elif result == "skip":
                    skip_count += 1
                    skip_tooltip += "%s.%s\n" % (case.__class__.__qualname__,
                                                 case._testMethodName)

                elif result == "unexpected_success":
                    unexpected_success_count += 1
                    unexpected_success_tooltip += "%s.%s should not have worked, but did\n" % (
                        case.__class__.__qualname__, case._testMethodName)

            success_tooltip = success_tooltip.strip()
            fail_tooltip = fail_tooltip.strip()
            error_tooltip = error_tooltip.strip()
            expected_fail_tooltip = expected_fail_tooltip.strip()
            skip_tooltip = skip_tooltip.strip()
            unexpected_success_tooltip = unexpected_success_tooltip.strip()

            icon_count = 0
            if success_count:
                success_button.setText("%i" % success_count)
                success_button.setToolTip(success_tooltip)
                success_button.clicked.connect(
                    lambda *args, t_name=name, res=success_tooltip: self.
                    tool_window.create_child_window(
                        "successes for %s" % t_name,
                        text=res,
                        window_class=ResultsWindow,
                    ))
                widget_layout.insertWidget(icon_count, success_button, 1)
                icon_count += 1

            if fail_count:
                fail_button.setText("%i" % fail_count)
                fail_button.setToolTip(fail_tooltip)
                fail_button.clicked.connect(
                    lambda *args, res=fail_tooltip: self.tool_window.
                    create_child_window(
                        "failures for %s" % name,
                        text=res,
                        window_class=ResultsWindow,
                    ))
                widget_layout.insertWidget(icon_count, fail_button, 1)
                icon_count += 1

            if error_count:
                error_button.setText("%i" % error_count)
                error_button.setToolTip(error_tooltip)
                error_button.clicked.connect(
                    lambda *args, res=error_tooltip: self.tool_window.
                    create_child_window(
                        "errors for %s" % name,
                        text=res,
                        window_class=ResultsWindow,
                    ))
                widget_layout.insertWidget(icon_count, error_button, 1)
                icon_count += 1

            if unexpected_success_count:
                unexpected_success_button.setText("%i" %
                                                  unexpected_success_count)
                unexpected_success_button.setToolTip(
                    unexpected_success_tooltip)
                unexpected_success_button.clicked.connect(
                    lambda *args, res=unexpected_success_tooltip: self.
                    tool_window.create_child_window(
                        "unexpected successes for %s" % name,
                        text=res,
                        window_class=ResultsWindow,
                    ))
                widget_layout.insertWidget(icon_count,
                                           unexpected_success_button, 1)
                icon_count += 1

            if expected_fail_count:
                expected_fail_button.setText("%i" % expected_fail_count)
                expected_fail_button.setToolTip(expected_fail_tooltip)
                expected_fail_button.clicked.connect(
                    lambda *args, res=expected_fail_tooltip: self.tool_window.
                    create_child_window(
                        "expected failures for %s" % name,
                        text=res,
                        window_class=ResultsWindow,
                    ))
                widget_layout.insertWidget(icon_count, expected_fail_button, 1)
                icon_count += 1

            if skip_count:
                skip_button.setText("%i" % skip_count)
                skip_button.setToolTip(skip_tooltip)
                skip_button.clicked.connect(
                    lambda *args, res=skip_tooltip: self.tool_window.
                    create_child_window(
                        "skipped tests for %s" % name,
                        text=res,
                        window_class=ResultsWindow,
                    ))
                widget_layout.insertWidget(icon_count, skip_button, 1)

            cell_widgets.append(widget)

        widget_count = 0
        if use_selected:
            for row in self.table.selectionModel().selectedRows():
                if self.table.isRowHidden(row.row()):
                    continue

                self.table.setCellWidget(row.row(), 1,
                                         cell_widgets[widget_count])
                self.table.resizeRowToContents(row.row())
                widget_count += 1

        else:
            for i in range(0, self.table.rowCount()):
                if self.table.isRowHidden(i):
                    continue

                self.table.setCellWidget(i, 1, cell_widgets[widget_count])
                # self.table.resizeRowToContents(i)
                widget_count += 1

        if self.profile.checkState() == Qt.Checked:
            self.tool_window.create_child_window("stats",
                                                 text=stats,
                                                 window_class=ResultsWindow)
Пример #4
0
class FileReaderPanel(ToolInstance):
    SESSION_ENDURING = False
    SESSION_SAVE = False         
    help = "https://github.com/QChASM/SEQCROW/wiki/Model-Manager-Tool"
    
    NAME_COL = 0
    ID_COL = 1
    COORDSETS_COL = 2
    NRG_COL = 3
    FREQ_COL = 4
    
    def __init__(self, session, name):       
        super().__init__(session, name)
        
        self.display_name = "SEQCROW Models"
        
        self.tool_window = MainToolWindow(self)        
        
        self._build_ui()
        self.fill_tree()
        
        self._fr_change = self.session.filereader_manager.triggers.add_handler(FILEREADER_CHANGE,
            lambda *args: self.fill_tree(*args))        
        self._add_models = self.session.triggers.add_handler(ADD_MODELS,
            lambda *args: self.fill_tree(*args))
        self._molid_change = self.session.triggers.add_handler(MODEL_ID_CHANGED,
            lambda *args: self.fill_tree(*args))
        self._molname_change = self.session.triggers.add_handler(MODEL_NAME_CHANGED,
            lambda *args: self.fill_tree(*args))

    def _build_ui(self):
        layout = QGridLayout()

        layout.setContentsMargins(0,0,0,0)
        layout.setSpacing(0)

        #TODO: make buttons disabled/enabled if items are selected that don't have the info
        self.tree = QTreeWidget()
        self.tree.setSelectionMode(QTreeWidget.ExtendedSelection)
        self.tree.setHeaderLabels(["Name", "ID", "movie", "energy", "frequencies"])
        self.tree.setUniformRowHeights(True)
        
        self.tree.setColumnWidth(self.NAME_COL, 200)
        layout.addWidget(self.tree, 0, 0, 3, 1)

        restore_button = QPushButton("restore")
        restore_button.clicked.connect(self.restore_selected)
        layout.addWidget(restore_button, 0, 1)
        
        nrg_plot_button = QPushButton("energy plot")
        nrg_plot_button.clicked.connect(self.open_nrg_plot)
        layout.addWidget(nrg_plot_button, 1, 1)
        
        coordset_slider_button = QPushButton("movie slider")
        coordset_slider_button.clicked.connect(self.open_movie_slider)
        layout.addWidget(coordset_slider_button, 2, 1)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(placement="side")

    def fill_tree(self, *args):        
        item_stack = [self.tree.invisibleRootItem()]
        
        self.tree.clear()
        self._items = []

        fr_dict = self.session.filereader_manager.filereader_dict

        for model in fr_dict.keys():
            id = model.id
            if id is None:
                continue
            
            name = model.name
            parent = item_stack[0]
            item = QTreeWidgetItem(parent)
            item._model = model
            item_stack.append(item)
            self._items.append(item)
            
            item.setData(self.NAME_COL, Qt.DisplayRole, model)
            item.setText(self.NAME_COL, name)
            item.setText(self.ID_COL, ".".join([str(x) for x in id]))
            
            if any(x.all_geom is not None and len(x.all_geom) > 1 for x in fr_dict[model]):
                item.setText(self.COORDSETS_COL, "yes")
            else:
                item.setText(self.COORDSETS_COL, "no")
                
            if any("energy" in x.other for x in fr_dict[model]):
                item.setText(self.NRG_COL, "yes")
            else:
                item.setText(self.NRG_COL, "no")
                
            if any("frequency" in x.other for x in fr_dict[model]):
                item.setText(self.FREQ_COL, "yes")
            else:
                item.setText(self.FREQ_COL, "no")
        
            for fr in fr_dict[model]:
                child = QTreeWidgetItem(item)
                child.setData(self.NAME_COL, Qt.DisplayRole, fr)
                child.setText(self.NAME_COL, fr.name)
                if fr.all_geom is not None and len(fr.all_geom) > 1:
                    child.setText(self.COORDSETS_COL, "yes")
                else:
                    child.setText(self.COORDSETS_COL, "no")
                    
                if "energy" in fr.other:
                    child.setText(self.NRG_COL, "%.6f" % fr.other["energy"])
                else:
                    child.setText(self.NRG_COL, "")
                    
                if "frequency" in fr.other:
                    child.setText(self.FREQ_COL, "yes")
                else:
                    child.setText(self.FREQ_COL, "no")    
            
            #self.tree.expandItem(item)
    
        for i in [self.ID_COL, self.COORDSETS_COL, self.NRG_COL, self.FREQ_COL]:
            self.tree.resizeColumnToContents(i)
   
    def restore_selected(self):
        items = [item for item in self.tree.selectedItems()]
        model_dict = self.session.filereader_manager.filereader_dict
        models = list(model_dict.keys())
        for item in items:
            parent = item.parent()
            mdl = models[self.tree.indexOfTopLevelItem(parent)]
            if parent is None:
                fr = model_dict[mdl][-1]
            else:
                fr = model_dict[mdl][parent.indexOfChild(item)]

            fr_rescol = ResidueCollection(fr)
            fr_rescol.update_chix(mdl)
            if fr.all_geom is not None and len(fr.all_geom) > 1:
                coordsets = fr_rescol.all_geom_coordsets(fr)

                mdl.remove_coordsets()
                mdl.add_coordsets(coordsets)

                for i, coordset in enumerate(coordsets):
                    mdl.active_coordset_id = i + 1
                    
                    for atom, coord in zip(mdl.atoms, coordset):
                        atom.coord = coord
                
                mdl.active_coordset_id = 1

    def open_nrg_plot(self):
        items = [item for item in self.tree.selectedItems()]
        model_dict = self.session.filereader_manager.filereader_dict
        models = list(model_dict.keys())
        for item in items:
            parent = item.parent()
            mdl = models[self.tree.indexOfTopLevelItem(parent)]
            if parent is None:
                fr = model_dict[mdl][-1]
            else:
                fr = model_dict[mdl][parent.indexOfChild(item)]

            EnergyPlot(self.session, mdl, fr)
    
    def open_movie_slider(self):
        items = [item for item in self.tree.selectedItems()]
        model_dict = self.session.filereader_manager.filereader_dict
        models = list(model_dict.keys())
        for item in items:
            parent = item.parent()
            mdl = models[self.tree.indexOfTopLevelItem(parent)]
            #coordset doesn't start out with the current coordset id
            #it looks like it should, but it doesn't
            #it starts at 1 instead
            slider = CoordinateSetSlider(self.session, mdl)
            slider.set_slider(mdl.active_coordset_id)
            #run(self.session, "coordset slider %s" % mdl.atomspec)
    
    def display_help(self):
        """Show the help for this tool in the help viewer."""
        from chimerax.core.commands import run
        run(self.session,
            'open %s' % self.help if self.help is not None else "")
    
    def delete(self):
        """overload delete"""
        self.session.filereader_manager.triggers.remove_handler(self._fr_change)
        self.session.triggers.remove_handler(self._add_models)
        self.session.triggers.remove_handler(self._molid_change)
        self.session.triggers.remove_handler(self._molname_change)
        super().delete()
    
    def close(self):
        """overload close"""
        self.session.filereader_manager.triggers.remove_handler(self._fr_change)
        self.session.triggers.remove_handler(self._add_models)
        self.session.triggers.remove_handler(self._molid_change)
        self.session.triggers.remove_handler(self._molname_change)
        super().close()
Пример #5
0
class Info(ToolInstance):

    help = "https://github.com/QChASM/SEQCROW/wiki/File-Info-Tool"

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

        self.tool_window = MainToolWindow(self)

        self.settings = _InfoSettings(self.session, name)

        self._build_ui()

    def _build_ui(self):
        layout = QVBoxLayout()

        self.file_selector = FilereaderComboBox(self.session)
        self.file_selector.currentIndexChanged.connect(self.fill_table)
        layout.insertWidget(0, self.file_selector, 0)

        self.table = QTableWidget()
        self.table.setColumnCount(2)
        self.table.setHorizontalHeaderLabels(['Data', 'Value'])
        self.table.horizontalHeader().setStretchLastSection(False)
        self.table.horizontalHeader().setSectionResizeMode(
            0, QHeaderView.Interactive)
        self.table.setEditTriggers(QTableWidget.NoEditTriggers)
        self.table.horizontalHeader().setSectionResizeMode(
            1, QHeaderView.Stretch)
        self.table.setSizePolicy(QSizePolicy.MinimumExpanding,
                                 QSizePolicy.MinimumExpanding)
        layout.insertWidget(1, self.table, 1)

        self.filter = QLineEdit()
        self.filter.setPlaceholderText("filter data")
        self.filter.textChanged.connect(self.apply_filter)
        self.filter.setClearButtonEnabled(True)
        layout.insertWidget(2, self.filter, 0)

        menu = QMenuBar()

        export = menu.addMenu("&Export")
        copy = QAction("&Copy CSV to clipboard", self.tool_window.ui_area)
        copy.triggered.connect(self.copy_csv)
        shortcut = QKeySequence(Qt.CTRL + Qt.Key_C)
        copy.setShortcut(shortcut)
        export.addAction(copy)
        self.copy = copy

        save = QAction("&Save CSV...", self.tool_window.ui_area)
        save.triggered.connect(self.save_csv)
        #this shortcut interferes with main window's save shortcut
        #I've tried different shortcut contexts to no avail
        #thanks Qt...
        #shortcut = QKeySequence(Qt.CTRL + Qt.Key_S)
        #save.setShortcut(shortcut)
        #save.setShortcutContext(Qt.WidgetShortcut)
        export.addAction(save)

        delimiter = export.addMenu("Delimiter")

        # comma = QAction("comma", self.tool_window.ui_area, checkable=True)
        # comma.setChecked(self.settings.delimiter == "comma")
        # comma.triggered.connect(lambda *args, delim="comma": self.settings.__setattr__("delimiter", delim))
        # delimiter.addAction(comma)

        tab = QAction("tab", self.tool_window.ui_area, checkable=True)
        tab.setChecked(self.settings.delimiter == "tab")
        tab.triggered.connect(lambda *args, delim="tab": self.settings.
                              __setattr__("delimiter", delim))
        delimiter.addAction(tab)

        # space = QAction("space", self.tool_window.ui_area, checkable=True)
        # space.setChecked(self.settings.delimiter == "space")
        # space.triggered.connect(lambda *args, delim="space": self.settings.__setattr__("delimiter", delim))
        # delimiter.addAction(space)

        semicolon = QAction("semicolon",
                            self.tool_window.ui_area,
                            checkable=True)
        semicolon.setChecked(self.settings.delimiter == "semicolon")
        semicolon.triggered.connect(lambda *args, delim="semicolon": self.
                                    settings.__setattr__("delimiter", delim))
        delimiter.addAction(semicolon)

        add_header = QAction("&Include CSV header",
                             self.tool_window.ui_area,
                             checkable=True)
        add_header.setChecked(self.settings.include_header)
        add_header.triggered.connect(self.header_check)
        export.addAction(add_header)

        tab.triggered.connect(
            lambda *args, action=semicolon: action.setChecked(False))
        semicolon.triggered.connect(
            lambda *args, action=tab: action.setChecked(False))

        archive = QAction("Include archive if present",
                          self.tool_window.ui_area,
                          checkable=True)
        archive.triggered.connect(
            lambda checked: setattr(self.settings, "archive", checked))
        archive.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        archive.setChecked(self.settings.archive)
        export.addAction(archive)

        unit = menu.addMenu("&Units")

        energy = unit.addMenu("energy")
        hartree = QAction("Hartree", self.tool_window.ui_area, checkable=True)
        hartree.setChecked(self.settings.energy == "Hartree")
        kcal = QAction("kcal/mol", self.tool_window.ui_area, checkable=True)
        kcal.setChecked(self.settings.energy == "kcal/mol")
        kjoule = QAction("kJ/mol", self.tool_window.ui_area, checkable=True)
        kjoule.setChecked(self.settings.energy == "kJ/mol")
        energy.addAction(hartree)
        energy.addAction(kcal)
        energy.addAction(kjoule)

        hartree.triggered.connect(
            lambda *args, val="Hartree": setattr(self.settings, "energy", val))
        hartree.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        hartree.triggered.connect(
            lambda *args, action=kcal: action.setChecked(False))
        hartree.triggered.connect(
            lambda *args, action=kjoule: action.setChecked(False))

        kcal.triggered.connect(lambda *args, val="kcal/mol": setattr(
            self.settings, "energy", val))
        kcal.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        kcal.triggered.connect(
            lambda *args, action=hartree: action.setChecked(False))
        kcal.triggered.connect(
            lambda *args, action=kjoule: action.setChecked(False))

        kjoule.triggered.connect(
            lambda *args, val="kJ/mol": setattr(self.settings, "energy", val))
        kjoule.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        kjoule.triggered.connect(
            lambda *args, action=hartree: action.setChecked(False))
        kjoule.triggered.connect(
            lambda *args, action=kcal: action.setChecked(False))

        mass = unit.addMenu("mass")
        kg = QAction("kg", self.tool_window.ui_area, checkable=True)
        kg.setChecked(self.settings.mass == "kg")
        amu = QAction("Da", self.tool_window.ui_area, checkable=True)
        amu.setChecked(self.settings.mass == "Da")
        mass.addAction(kg)
        mass.addAction(amu)

        kg.triggered.connect(
            lambda *args, val="kg": setattr(self.settings, "mass", val))
        kg.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        kg.triggered.connect(
            lambda *args, action=amu: action.setChecked(False))

        amu.triggered.connect(
            lambda *args, val="Da": setattr(self.settings, "mass", val))
        amu.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        amu.triggered.connect(
            lambda *args, action=kg: action.setChecked(False))

        rot_const = unit.addMenu("rotational constants")
        temperature = QAction("K", self.tool_window.ui_area, checkable=True)
        temperature.setChecked(self.settings.rot_const == "K")
        hertz = QAction("GHz", self.tool_window.ui_area, checkable=True)
        hertz.setChecked(self.settings.rot_const == "GHz")
        rot_const.addAction(temperature)
        rot_const.addAction(hertz)

        temperature.triggered.connect(
            lambda *args, val="K": setattr(self.settings, "rot_const", val))
        temperature.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        temperature.triggered.connect(
            lambda *args, action=hertz: action.setChecked(False))

        hertz.triggered.connect(
            lambda *args, val="GHz": setattr(self.settings, "rot_const", val))
        hertz.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        hertz.triggered.connect(
            lambda *args, action=temperature: action.setChecked(False))

        menu.setNativeMenuBar(False)
        self._menu = menu
        layout.setMenuBar(menu)

        if len(self.session.filereader_manager.list()) > 0:
            self.fill_table(0)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def header_check(self, state):
        """user has [un]checked the 'include header' option on the menu"""
        if state:
            self.settings.include_header = True
        else:
            self.settings.include_header = False

    def get_csv(self):
        if self.settings.delimiter == "comma":
            delim = ","
        elif self.settings.delimiter == "space":
            delim = " "
        elif self.settings.delimiter == "tab":
            delim = "\t"
        elif self.settings.delimiter == "semicolon":
            delim = ";"

        if self.settings.include_header:
            s = delim.join(["Data", "Value"])
            s += "\n"
        else:
            s = ""

        for i in range(0, self.table.rowCount()):
            if self.table.isRowHidden(i):
                continue
            s += delim.join([
                item.text().replace("<sub>", "").replace("</sub>", "")
                for item in [
                    self.table.item(i, j) if self.table.
                    item(i, j) is not None else self.table.cellWidget(i, j)
                    for j in range(0, 2)
                ]
            ])
            s += "\n"

        return s

    def copy_csv(self):
        app = QApplication.instance()
        clipboard = app.clipboard()
        csv = self.get_csv()
        clipboard.setText(csv)
        self.session.logger.status("copied to clipboard")

    def save_csv(self):
        """save data on current tab to CSV file"""
        filename, _ = QFileDialog.getSaveFileName(filter="CSV Files (*.csv)")
        if filename:
            s = self.get_csv()

            with open(filename, 'w') as f:
                f.write(s.strip())

            self.session.logger.status("saved to %s" % filename)

    def fill_table(self, ndx):
        self.table.setRowCount(0)

        if ndx < 0:
            return

        fr = self.file_selector.currentData()
        item = QTableWidgetItem()
        item.setData(Qt.DisplayRole, "name")
        val = QTableWidgetItem()
        val.setData(Qt.DisplayRole, fr.name)
        self.table.insertRow(0)
        self.table.setItem(0, 0, item)
        self.table.setItem(0, 1, val)
        for info in fr.other.keys():
            if info == "archive" and not self.settings.archive:
                continue

            if any(
                    isinstance(fr.other[info], obj)
                    for obj in [str, float, int]):
                row = self.table.rowCount()
                self.table.insertRow(row)

                item = QTableWidgetItem()
                info_name = info.replace("_", " ")
                val = fr.other[info]
                if info == "mass":
                    info_name += " (%s)" % self.settings.mass
                    if self.settings.mass == "Da":
                        val /= UNIT.AMU_TO_KG

                elif info == "temperature":
                    info_name += " (K)"

                elif (any(info == s for s in nrg_infos)
                      or info.lower().endswith("energy")
                      or info.startswith("E(")):
                    if self.settings.energy == "Hartree":
                        info_name += " (E<sub>h</sub>)"
                    else:
                        info_name += " (%s)" % self.settings.energy
                    info_name = info_name.replace("orrelation", "orr.")
                    info_name = info_name.replace("Same-spin", "SS")
                    info_name = info_name.replace("Opposite-spin", "OS")
                    if self.settings.energy == "kcal/mol":
                        val *= UNIT.HART_TO_KCAL
                    elif self.settings.energy == "kJ/mol":
                        val *= 4.184 * UNIT.HART_TO_KCAL

                    val = "%.6f" % val

                elif any(info == x for x in pg_infos):
                    info_name = info.replace("_", " ")
                    if re.search("\d", val):
                        val = re.sub(r"(\d+)", r"<sub>\1</sub>", val)
                    # gaussian uses * for infinity
                    val = val.replace("*", "<sub>∞</sub>")
                    # psi4 uses _inf_
                    val = val.replace("_inf_", "<sub>∞</sub>")
                    if any(val.endswith(char) for char in "vhdsiVHDSI"):
                        val = val[:-1] + "<sub>" + val[-1].lower() + "</sub>"

                if "<sub>" in info_name:
                    self.table.setCellWidget(row, 0, QLabel(info_name))
                else:
                    item = QTableWidgetItem()
                    item.setData(Qt.DisplayRole, info_name)
                    self.table.setItem(row, 0, item)

                value = QTableWidgetItem()
                val = str(val)
                if "<sub>" in val:
                    self.table.setCellWidget(row, 1, QLabel(val))
                else:
                    value.setData(Qt.DisplayRole, val)
                    self.table.setItem(row, 1, value)

            elif isinstance(fr.other[info], Theory):
                theory = fr.other[info]
                if theory.method is not None:
                    row = self.table.rowCount()
                    self.table.insertRow(row)

                    item = QTableWidgetItem()
                    item.setData(Qt.DisplayRole, "method")
                    self.table.setItem(row, 0, item)

                    value = QTableWidgetItem()
                    value.setData(Qt.DisplayRole, theory.method.name)
                    self.table.setItem(row, 1, value)

                if theory.basis is not None:
                    if theory.basis.basis:
                        for basis in theory.basis.basis:
                            row = self.table.rowCount()
                            self.table.insertRow(row)

                            item = QTableWidgetItem()
                            if not basis.elements:
                                item.setData(Qt.DisplayRole, "basis set")
                            else:
                                item.setData(
                                    Qt.DisplayRole,
                                    "basis for %s" % ", ".join(basis.elements))
                            self.table.setItem(row, 0, item)

                            value = QTableWidgetItem()
                            value.setData(Qt.DisplayRole, basis.name)
                            self.table.setItem(row, 1, value)

                    if theory.basis.ecp:
                        for ecp in theory.basis.ecp:
                            row = self.table.rowCount()
                            self.table.insertRow(row)

                            item = QTableWidgetItem()
                            if ecp.elements is None:
                                item.setData(Qt.DisplayRole, "ECP")
                            else:
                                item.setData(Qt.DisplayRole,
                                             "ECP %s" % " ".join(ecp.elements))
                            self.table.setItem(row, 0, item)

                            value = QTableWidgetItem()
                            value.setData(Qt.DisplayRole, ecp.name)
                            self.table.setItem(row, 1, value)

            elif hasattr(fr.other[info], "__iter__") and all(
                    isinstance(x, float) for x in fr.other[info]):
                row = self.table.rowCount()
                self.table.insertRow(row)

                item = QTableWidgetItem()
                info_name = info.replace("_", " ")
                vals = fr.other[info]
                if info == "rotational_temperature":
                    info_name = "rotational constants (%s)" % self.settings.rot_const
                    if self.settings.rot_const == "GHz":
                        vals = [
                            x * PHYSICAL.KB / (PHYSICAL.PLANCK * (10**9))
                            for x in vals
                        ]

                item.setData(Qt.DisplayRole, info_name)
                self.table.setItem(row, 0, item)

                value = QTableWidgetItem()
                value.setData(Qt.DisplayRole,
                              ", ".join(["%.4f" % x for x in vals]))
                self.table.setItem(row, 1, value)

        self.table.resizeColumnToContents(0)
        self.table.resizeColumnToContents(1)

        self.apply_filter()

    def apply_filter(self, text=None):
        if text is None:
            text = self.filter.text()

        if text:
            text = text.replace("(", "\(")
            text = text.replace(")", "\)")
            m = QRegularExpression(text)
            m.setPatternOptions(QRegularExpression.CaseInsensitiveOption)
            if m.isValid():
                m.optimize()
                filter = lambda row_num: m.match(
                    self.table.item(row_num, 0).text()
                    if self.table.item(row_num, 0) is not None else self.table.
                    cellWidget(row_num, 0).text().replace("<sub>", "").replace(
                        "</sub>", "")).hasMatch()
            else:
                return

        else:
            filter = lambda row: True

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

    def delete(self):
        self.file_selector.deleteLater()

        return super().delete()

    def close(self):
        self.file_selector.deleteLater()

        return super().close()
Пример #6
0
class PercentVolumeBuried(ToolInstance):

    help = "https://github.com/QChASM/SEQCROW/wiki/Buried-Volume-Tool"
    SESSION_ENDURING = True
    SESSION_SAVE = True

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

        self.tool_window = MainToolWindow(self)

        self.settings = _VburSettings(self.session, name)

        self.ligand_atoms = []

        self._build_ui()

    def _build_ui(self):
        layout = QGridLayout()

        tabs = QTabWidget()
        calc_widget = QWidget()
        calc_layout = QFormLayout(calc_widget)
        vis_widget = QWidget()
        vis_layout = QFormLayout(vis_widget)
        layout.addWidget(tabs)

        tabs.addTab(calc_widget, "calculation")
        tabs.addTab(vis_widget, "visualization")

        self.radii_option = QComboBox()
        self.radii_option.addItems(["Bondi", "UMN"])
        ndx = self.radii_option.findText(self.settings.radii, Qt.MatchExactly)
        self.radii_option.setCurrentIndex(ndx)
        calc_layout.addRow("radii:", self.radii_option)

        self.scale = QDoubleSpinBox()
        self.scale.setValue(self.settings.vdw_scale)
        self.scale.setSingleStep(0.01)
        self.scale.setRange(1., 1.5)
        calc_layout.addRow("VDW scale:", self.scale)

        set_ligand_atoms = QPushButton("set ligands to current selection")
        set_ligand_atoms.clicked.connect(self.set_ligand_atoms)
        set_ligand_atoms.setToolTip(
            "specify atoms to use in calculation\n" +
            "by default, all atoms will be used unless a single center is specified\n"
            +
            "in the case of a single center, all atoms except the center is used"
        )
        calc_layout.addRow(set_ligand_atoms)
        self.set_ligand_atoms = set_ligand_atoms

        self.radius = QDoubleSpinBox()
        self.radius.setValue(self.settings.center_radius)
        self.radius.setSuffix(" \u212B")
        self.radius.setDecimals(1)
        self.radius.setSingleStep(0.1)
        self.radius.setRange(1., 15.)
        calc_layout.addRow("radius around center:", self.radius)

        self.method = QComboBox()
        self.method.addItems(["Lebedev", "Monte-Carlo"])
        self.method.setToolTip("Lebedev: deterministic method\n" +
                               "Monte-Carlo: non-deterministic method")
        ndx = self.method.findText(self.settings.method, Qt.MatchExactly)
        self.method.setCurrentIndex(ndx)
        calc_layout.addRow("integration method:", self.method)

        leb_widget = QWidget()
        leb_layout = QFormLayout(leb_widget)
        leb_layout.setContentsMargins(0, 0, 0, 0)

        self.radial_points = QComboBox()
        self.radial_points.addItems(["20", "32", "64", "75", "99", "127"])
        self.radial_points.setToolTip(
            "more radial points will give more accurate results, but integration will take longer"
        )
        ndx = self.radial_points.findText(self.settings.radial_points,
                                          Qt.MatchExactly)
        self.radial_points.setCurrentIndex(ndx)
        leb_layout.addRow("radial points:", self.radial_points)

        self.angular_points = QComboBox()
        self.angular_points.addItems([
            "110", "194", "302", "590", "974", "1454", "2030", "2702", "5810"
        ])
        self.angular_points.setToolTip(
            "more angular points will give more accurate results, but integration will take longer"
        )
        ndx = self.angular_points.findText(self.settings.angular_points,
                                           Qt.MatchExactly)
        self.angular_points.setCurrentIndex(ndx)
        leb_layout.addRow("angular points:", self.angular_points)

        calc_layout.addRow(leb_widget)

        mc_widget = QWidget()
        mc_layout = QFormLayout(mc_widget)
        mc_layout.setContentsMargins(0, 0, 0, 0)

        self.min_iter = QSpinBox()
        self.min_iter.setValue(self.settings.minimum_iterations)
        self.min_iter.setRange(0, 10000)
        self.min_iter.setToolTip(
            "each iteration is 3000 points\n" +
            "iterations continue until convergence criteria are met")
        mc_layout.addRow("minimum interations:", self.min_iter)

        calc_layout.addRow(mc_widget)

        if self.settings.method == "Lebedev":
            mc_widget.setVisible(False)
        elif self.settings.method == "Monte-Carlo":
            leb_widget.setVisible(False)

        self.method.currentTextChanged.connect(
            lambda text, widget=leb_widget: widget.setVisible(text == "Lebedev"
                                                              ))
        self.method.currentTextChanged.connect(
            lambda text, widget=mc_widget: widget.setVisible(text ==
                                                             "Monte-Carlo"))

        self.use_centroid = QCheckBox()
        self.use_centroid.setChecked(self.settings.use_centroid)
        self.use_centroid.setToolTip(
            "place the center between selected atoms\n" +
            "might be useful for polydentate ligands")
        calc_layout.addRow("use centroid of centers:", self.use_centroid)

        steric_map_options = QGroupBox("steric map")
        steric_map_layout = QFormLayout(steric_map_options)

        self.steric_map = QCheckBox()
        self.steric_map.setChecked(self.settings.steric_map)
        self.steric_map.setToolTip(
            "produce a 2D projection of steric bulk\ncauses buried volume to be reported for individual quadrants"
        )
        steric_map_layout.addRow("create steric map:", self.steric_map)

        self.use_scene = QCheckBox()
        self.use_scene.setChecked(self.settings.use_scene)
        self.use_scene.setToolTip(
            "steric map will use the orientation the molecule is displayed in")
        steric_map_layout.addRow("use display orientation:", self.use_scene)

        self.num_pts = QSpinBox()
        self.num_pts.setRange(25, 250)
        self.num_pts.setValue(self.settings.num_pts)
        self.num_pts.setToolTip("number of points along x and y axes")
        steric_map_layout.addRow("number of points:", self.num_pts)

        self.include_vbur = QCheckBox()
        self.include_vbur.setChecked(self.settings.include_vbur)
        steric_map_layout.addRow("label quadrants with %V<sub>bur</sub>",
                                 self.include_vbur)

        self.map_shape = QComboBox()
        self.map_shape.addItems(["circle", "square"])
        ndx = self.map_shape.findText(self.settings.map_shape, Qt.MatchExactly)
        self.map_shape.setCurrentIndex(ndx)
        steric_map_layout.addRow("map shape:", self.map_shape)

        self.auto_minmax = QCheckBox()
        self.auto_minmax.setChecked(self.settings.auto_minmax)
        steric_map_layout.addRow("automatic min. and max.:", self.auto_minmax)

        self.map_min = QDoubleSpinBox()
        self.map_min.setRange(-15., 0.)
        self.map_min.setSuffix(" \u212B")
        self.map_min.setSingleStep(0.1)
        self.map_min.setValue(self.settings.map_min)
        steric_map_layout.addRow("minimum value:", self.map_min)

        self.map_max = QDoubleSpinBox()
        self.map_max.setRange(0., 15.)
        self.map_max.setSuffix(" \u212B")
        self.map_max.setSingleStep(0.1)
        self.map_max.setValue(self.settings.map_max)
        steric_map_layout.addRow("maximum value:", self.map_max)

        self.use_scene.setEnabled(self.settings.steric_map)
        self.steric_map.stateChanged.connect(
            lambda state, widget=self.use_scene: widget.setEnabled(state == Qt.
                                                                   Checked))

        self.num_pts.setEnabled(self.settings.steric_map)
        self.steric_map.stateChanged.connect(
            lambda state, widget=self.num_pts: widget.setEnabled(state == Qt.
                                                                 Checked))

        self.include_vbur.setEnabled(self.settings.steric_map)
        self.steric_map.stateChanged.connect(
            lambda state, widget=self.include_vbur: widget.setEnabled(
                state == Qt.Checked))

        self.map_shape.setEnabled(self.settings.steric_map)
        self.steric_map.stateChanged.connect(
            lambda state, widget=self.map_shape: widget.setEnabled(state == Qt.
                                                                   Checked))

        self.auto_minmax.setEnabled(self.settings.steric_map)
        self.steric_map.stateChanged.connect(
            lambda state, widget=self.auto_minmax: widget.setEnabled(
                state == Qt.Checked))

        self.map_min.setEnabled(not self.settings.auto_minmax
                                and self.settings.steric_map)
        self.steric_map.stateChanged.connect(
            lambda state, widget=self.map_min, widget2=self.auto_minmax: widget
            .setEnabled(state == Qt.Checked and not widget2.isChecked()))
        self.auto_minmax.stateChanged.connect(
            lambda state, widget=self.map_min, widget2=self.steric_map: widget.
            setEnabled(not state == Qt.Checked and widget2.isChecked()))

        self.map_max.setEnabled(not self.settings.auto_minmax
                                and self.settings.steric_map)
        self.steric_map.stateChanged.connect(
            lambda state, widget=self.map_max, widget2=self.auto_minmax: widget
            .setEnabled(state == Qt.Checked and not widget2.isChecked()))
        self.auto_minmax.stateChanged.connect(
            lambda state, widget=self.map_max, widget2=self.steric_map: widget.
            setEnabled(not state == Qt.Checked and widget2.isChecked()))

        vis_layout.addRow(steric_map_options)

        cutout_options = QGroupBox("volume cutout")
        cutout_layout = QFormLayout(cutout_options)

        self.display_cutout = QComboBox()
        self.display_cutout.addItems(["no", "free", "buried"])
        ndx = self.display_cutout.findText(self.settings.display_cutout,
                                           Qt.MatchExactly)
        self.display_cutout.setCurrentIndex(ndx)
        self.display_cutout.setToolTip("show free or buried volume")
        cutout_layout.addRow("display volume:", self.display_cutout)

        self.point_spacing = QDoubleSpinBox()
        self.point_spacing.setDecimals(3)
        self.point_spacing.setRange(0.01, 0.5)
        self.point_spacing.setSingleStep(0.005)
        self.point_spacing.setSuffix(" \u212B")
        self.point_spacing.setValue(self.settings.point_spacing)
        self.point_spacing.setToolTip(
            "distance between points on cutout\n" +
            "smaller spacing will narrow gaps, but increase time to create the cutout"
        )
        cutout_layout.addRow("point spacing:", self.point_spacing)

        self.intersection_scale = QDoubleSpinBox()
        self.intersection_scale.setDecimals(2)
        self.intersection_scale.setRange(1., 10.)
        self.intersection_scale.setSingleStep(0.5)
        self.intersection_scale.setSuffix("x")
        self.intersection_scale.setToolTip(
            "relative density of points where VDW radii intersect\n" +
            "higher density will narrow gaps, but increase time to create cutout"
        )
        self.intersection_scale.setValue(self.settings.intersection_scale)
        cutout_layout.addRow("intersection density:", self.intersection_scale)

        self.point_spacing.setEnabled(self.settings.display_cutout != "no")
        self.intersection_scale.setEnabled(
            self.settings.display_cutout != "no")

        self.display_cutout.currentTextChanged.connect(
            lambda text, widget=self.point_spacing: widget.setEnabled(text !=
                                                                      "no"))
        self.display_cutout.currentTextChanged.connect(
            lambda text, widget=self.intersection_scale: widget.setEnabled(
                text != "no"))

        vis_layout.addRow(cutout_options)

        calc_vbur_button = QPushButton(
            "calculate % buried volume for selected centers")
        calc_vbur_button.clicked.connect(self.calc_vbur)
        calc_layout.addRow(calc_vbur_button)
        self.calc_vbur_button = calc_vbur_button

        remove_vbur_button = QPushButton(
            "remove % buried volume visualizations")
        remove_vbur_button.clicked.connect(self.del_vbur)
        cutout_layout.addRow(remove_vbur_button)

        self.table = QTableWidget()
        self.table.setColumnCount(3)
        self.table.setHorizontalHeaderLabels(['model', 'center', '%Vbur'])
        self.table.setSelectionBehavior(QTableWidget.SelectRows)
        self.table.setEditTriggers(QTableWidget.NoEditTriggers)
        self.table.resizeColumnToContents(0)
        self.table.resizeColumnToContents(1)
        self.table.resizeColumnToContents(2)
        self.table.horizontalHeader().setSectionResizeMode(
            2, QHeaderView.Stretch)
        calc_layout.addRow(self.table)

        menu = QMenuBar()

        export = menu.addMenu("&Export")
        copy = QAction("&Copy CSV to clipboard", self.tool_window.ui_area)
        copy.triggered.connect(self.copy_csv)
        shortcut = QKeySequence(Qt.CTRL + Qt.Key_C)
        copy.setShortcut(shortcut)
        export.addAction(copy)
        self.copy = copy

        save = QAction("&Save CSV...", self.tool_window.ui_area)
        save.triggered.connect(self.save_csv)
        #this shortcut interferes with main window's save shortcut
        #I've tried different shortcut contexts to no avail
        #thanks Qt...
        #shortcut = QKeySequence(Qt.CTRL + Qt.Key_S)
        #save.setShortcut(shortcut)
        #save.setShortcutContext(Qt.WidgetShortcut)
        export.addAction(save)

        delimiter = export.addMenu("Delimiter")

        comma = QAction("comma", self.tool_window.ui_area, checkable=True)
        comma.setChecked(self.settings.delimiter == "comma")
        comma.triggered.connect(lambda *args, delim="comma": self.settings.
                                __setattr__("delimiter", delim))
        delimiter.addAction(comma)

        tab = QAction("tab", self.tool_window.ui_area, checkable=True)
        tab.setChecked(self.settings.delimiter == "tab")
        tab.triggered.connect(lambda *args, delim="tab": self.settings.
                              __setattr__("delimiter", delim))
        delimiter.addAction(tab)

        space = QAction("space", self.tool_window.ui_area, checkable=True)
        space.setChecked(self.settings.delimiter == "space")
        space.triggered.connect(lambda *args, delim="space": self.settings.
                                __setattr__("delimiter", delim))
        delimiter.addAction(space)

        semicolon = QAction("semicolon",
                            self.tool_window.ui_area,
                            checkable=True)
        semicolon.setChecked(self.settings.delimiter == "semicolon")
        semicolon.triggered.connect(lambda *args, delim="semicolon": self.
                                    settings.__setattr__("delimiter", delim))
        delimiter.addAction(semicolon)

        add_header = QAction("&Include CSV header",
                             self.tool_window.ui_area,
                             checkable=True)
        add_header.setChecked(self.settings.include_header)
        add_header.triggered.connect(self.header_check)
        export.addAction(add_header)

        comma.triggered.connect(
            lambda *args, action=tab: action.setChecked(False))
        comma.triggered.connect(
            lambda *args, action=space: action.setChecked(False))
        comma.triggered.connect(
            lambda *args, action=semicolon: action.setChecked(False))

        tab.triggered.connect(
            lambda *args, action=comma: action.setChecked(False))
        tab.triggered.connect(
            lambda *args, action=space: action.setChecked(False))
        tab.triggered.connect(
            lambda *args, action=semicolon: action.setChecked(False))

        space.triggered.connect(
            lambda *args, action=comma: action.setChecked(False))
        space.triggered.connect(
            lambda *args, action=tab: action.setChecked(False))
        space.triggered.connect(
            lambda *args, action=semicolon: action.setChecked(False))

        semicolon.triggered.connect(
            lambda *args, action=comma: action.setChecked(False))
        semicolon.triggered.connect(
            lambda *args, action=tab: action.setChecked(False))
        semicolon.triggered.connect(
            lambda *args, action=space: action.setChecked(False))

        menu.setNativeMenuBar(False)
        self._menu = menu
        layout.setMenuBar(menu)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def set_ligand_atoms(self):
        self.ligand_atoms = selected_atoms(self.session)
        self.session.logger.status("set ligand to current selection")

    def calc_vbur(self):
        args = dict()

        cur_sel = selected_atoms(self.session)
        if len(cur_sel) == 0:
            return

        models = []
        for atom in cur_sel:
            if atom.structure not in models:
                models.append(atom.structure)

        center = []
        for atom in cur_sel:
            center.append(atom)

        args["center"] = center

        radii = self.radii_option.currentText()
        self.settings.radii = radii
        args["radii"] = radii

        scale = self.scale.value()
        self.settings.vdw_scale = scale
        args["scale"] = scale

        radius = self.radius.value()
        self.settings.center_radius = radius
        args["radius"] = radius

        steric_map = self.steric_map.checkState() == Qt.Checked
        self.settings.steric_map = steric_map
        args["steric_map"] = steric_map

        use_scene = self.use_scene.checkState() == Qt.Checked
        self.settings.use_scene = use_scene
        args["use_scene"] = use_scene

        num_pts = self.num_pts.value()
        self.settings.num_pts = num_pts
        args["num_pts"] = num_pts

        include_vbur = self.include_vbur.checkState() == Qt.Checked
        self.settings.include_vbur = include_vbur

        use_centroid = self.use_centroid.checkState() == Qt.Checked
        self.settings.use_centroid = use_centroid
        args["useCentroid"] = use_centroid

        shape = self.map_shape.currentText()
        self.settings.map_shape = shape
        args["shape"] = shape

        method = self.method.currentText()
        self.settings.method = method

        if method == "Lebedev":
            args["method"] = "lebedev"
            rad_pts = self.radial_points.currentText()
            self.settings.radial_points = rad_pts
            args["radialPoints"] = rad_pts

            ang_pts = self.angular_points.currentText()
            self.settings.angular_points = ang_pts
            args["angularPoints"] = ang_pts

        elif method == "Monte-Carlo":
            args["method"] = "mc"
            min_iter = self.min_iter.value()
            self.settings.minimum_iterations = min_iter
            args["minimumIterations"] = min_iter

        display_cutout = self.display_cutout.currentText()
        self.settings.display_cutout = display_cutout
        if display_cutout != "no":
            args["displaySphere"] = display_cutout

        if display_cutout is not None:
            point_spacing = self.point_spacing.value()
            self.settings.point_spacing = point_spacing
            args["pointSpacing"] = point_spacing

            intersection_scale = self.intersection_scale.value()
            self.settings.intersection_scale = intersection_scale
            args["intersectionScale"] = intersection_scale

        if len(self.ligand_atoms) > 0:
            args["onlyAtoms"] = [a for a in self.ligand_atoms if not a.deleted]
            if len(args["onlyAtoms"]) == 0:
                args["onlyAtoms"] = None

        auto_minmax = self.auto_minmax.checkState() == Qt.Checked
        self.settings.auto_minmax = auto_minmax
        if not auto_minmax:
            map_max = self.map_max.value()
            self.settings.map_max = map_max

            map_min = self.settings.map_min
            self.settings.map_min = map_min

        info = percent_vbur_cmd(self.session,
                                models,
                                return_values=True,
                                **args)

        self.table.setRowCount(0)

        if steric_map:
            for mdl, cent, vbur, map_info in info:
                row = self.table.rowCount()
                self.table.insertRow(row)

                m = QTableWidgetItem()
                m.setData(Qt.DisplayRole, mdl)
                self.table.setItem(row, 0, m)

                c = QTableWidgetItem()
                c.setData(Qt.DisplayRole, cent)
                self.table.setItem(row, 1, c)

                v = QTableWidgetItem()
                v.setData(Qt.DisplayRole, ",".join(["%.1f" % x for x in vbur]))
                v.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
                self.table.setItem(row, 2, v)

                x, y, z, min_alt, max_alt = map_info
                plot = self.tool_window.create_child_window(
                    "steric map of %s" % mdl, window_class=StericMap)
                if auto_minmax:
                    plot.set_data(x, y, z, min_alt, max_alt, vbur, radius,
                                  include_vbur)
                else:
                    plot.set_data(x, y, z, map_min, map_max, vbur, radius,
                                  include_vbur)

        else:
            for mdl, cent, vbur in info:
                row = self.table.rowCount()
                self.table.insertRow(row)

                m = QTableWidgetItem()
                m.setData(Qt.DisplayRole, mdl)
                self.table.setItem(row, 0, m)

                c = QTableWidgetItem()
                c.setData(Qt.DisplayRole, cent)
                self.table.setItem(row, 1, c)

                v = QTableWidgetItem()
                v.setData(Qt.DisplayRole, "%.1f" % vbur)
                v.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
                self.table.setItem(row, 2, v)

        self.table.resizeColumnToContents(0)
        self.table.resizeColumnToContents(1)

    def header_check(self, state):
        """user has [un]checked the 'include header' option on the menu"""
        if state:
            self.settings.include_header = True
        else:
            self.settings.include_header = False

    def get_csv(self):
        if self.settings.delimiter == "comma":
            delim = ","
        elif self.settings.delimiter == "space":
            delim = " "
        elif self.settings.delimiter == "tab":
            delim = "\t"
        elif self.settings.delimiter == "semicolon":
            delim = ";"

        if self.settings.include_header:
            s = delim.join(["model", "center", "%Vbur"])
            s += "\n"
        else:
            s = ""

        for i in range(0, self.table.rowCount()):
            s += delim.join([
                item.data(Qt.DisplayRole)
                for item in [self.table.item(i, j) for j in range(0, 3)]
            ])
            s += "\n"

        return s

    def copy_csv(self):
        app = QApplication.instance()
        clipboard = app.clipboard()
        csv = self.get_csv()
        clipboard.setText(csv)
        self.session.logger.status("copied to clipboard")

    def save_csv(self):
        """save data on current tab to CSV file"""
        filename, _ = QFileDialog.getSaveFileName(filter="CSV Files (*.csv)")
        if filename:
            s = self.get_csv()

            with open(filename, 'w') as f:
                f.write(s.strip())

            self.session.logger.status("saved to %s" % filename)

    def del_vbur(self):
        for model in self.session.models.list(type=Surface):
            if model.name.startswith("%Vbur") or model.name.startswith(
                    "%Vfree"):
                model.delete()

    def display_help(self):
        """Show the help for this tool in the help viewer."""
        from chimerax.core.commands import run
        run(self.session,
            'open %s' % self.help if self.help is not None else "")
Пример #7
0
class Thermochem(ToolInstance):
    """tool for calculating free energy corrections based on frequencies and energies 
    associated with FileReaders
    there are two tabs: absolute and relative
    
    the absolute tab can be used to combine the thermo corrections from one
    FileReader with the energy of another
    
    the relative tab can do the same, but it prints the energies relative to
    those of another FileReader
    multiple molecule groups can be added (i.e. for reactions with multiple
    reactants and products)
    each molecule group can have multiple conformers
    the energies of these conformers are boltzmann weighted, and the boltzmann-weighted
    energy is used to calculate the energy of either the reference group or the 'other' group"""

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

    theory_helper = {
        "Grimme's Quasi-RRHO": "https://doi.org/10.1002/chem.201200497",
        "Truhlar's Quasi-Harmonic": "https://doi.org/10.1021/jp205508z"
    }

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

        self.display_name = "Thermochemistry"

        self.tool_window = MainToolWindow(self)

        self.settings = _ComputeThermoSettings(self.session, name)

        self.nrg_fr = {}
        self.thermo_fr = {}
        self.thermo_co = {}

        self._build_ui()

        self.set_sp()
        self.set_thermo_mdl()

        self.calc_relative_thermo()

    def _build_ui(self):
        #each group has an empty widget at the bottom so they resize the way I want while also having the
        #labels where I want them
        layout = QGridLayout()

        self.tab_widget = QTabWidget()
        layout.addWidget(self.tab_widget)

        #layout for absolute thermo stuff
        absolute_widget = QWidget()
        absolute_layout = QGridLayout(absolute_widget)

        #box for sp
        sp_area_widget = QGroupBox("Single-point")
        sp_layout = QGridLayout(sp_area_widget)

        self.sp_selector = FilereaderComboBox(self.session,
                                              otherItems=['energy'])
        self.sp_selector.currentIndexChanged.connect(self.set_sp)
        sp_layout.addWidget(self.sp_selector, 0, 0, 1, 3, Qt.AlignTop)

        nrg_label = QLabel("E =")
        sp_layout.addWidget(nrg_label, 1, 0, 1, 1,
                            Qt.AlignRight | Qt.AlignVCenter)

        self.sp_nrg_line = QLineEdit()
        self.sp_nrg_line.setReadOnly(True)
        self.sp_nrg_line.setToolTip("electronic energy")
        sp_layout.addWidget(self.sp_nrg_line, 1, 1, 1, 1, Qt.AlignTop)

        sp_layout.addWidget(QLabel("E<sub>h</sub>"), 1, 2, 1, 1,
                            Qt.AlignVCenter)

        sp_layout.addWidget(QWidget(), 2, 0)

        sp_layout.setColumnStretch(0, 0)
        sp_layout.setColumnStretch(1, 1)
        sp_layout.setRowStretch(0, 0)
        sp_layout.setRowStretch(1, 0)
        sp_layout.setRowStretch(2, 1)

        row = 0
        #box for thermo
        therm_area_widget = QGroupBox("Thermal corrections")
        thermo_layout = QGridLayout(therm_area_widget)

        self.thermo_selector = FilereaderComboBox(self.session,
                                                  otherItems=['frequency'])
        self.thermo_selector.currentIndexChanged.connect(self.set_thermo_mdl)
        thermo_layout.addWidget(self.thermo_selector, row, 0, 1, 3,
                                Qt.AlignTop)

        row += 1

        thermo_layout.addWidget(QLabel("T ="), row, 0, 1, 1,
                                Qt.AlignRight | Qt.AlignVCenter)

        self.temperature_line = QDoubleSpinBox()
        self.temperature_line.setMaximum(2**31 - 1)
        self.temperature_line.setValue(298.15)
        self.temperature_line.setSingleStep(10)
        self.temperature_line.setSuffix(" K")
        self.temperature_line.setMinimum(0)
        self.temperature_line.valueChanged.connect(self.set_thermo)
        thermo_layout.addWidget(self.temperature_line, row, 1, 1, 2,
                                Qt.AlignTop)

        row += 1

        thermo_layout.addWidget(QLabel("𝜔<sub>0</sub> ="), row, 0, 1, 1,
                                Qt.AlignRight | Qt.AlignVCenter)

        self.v0_edit = QDoubleSpinBox()
        self.v0_edit.setMaximum(4000)
        self.v0_edit.setValue(self.settings.w0)
        self.v0_edit.setSingleStep(25)
        self.v0_edit.setSuffix(" cm\u207b\u00b9")
        self.v0_edit.valueChanged.connect(self.set_thermo)
        self.v0_edit.setMinimum(0)
        self.v0_edit.setToolTip(
            "frequency parameter for quasi treatments of entropy")
        thermo_layout.addWidget(self.v0_edit, row, 1, 1, 2, Qt.AlignTop)

        row += 1

        thermo_layout.addWidget(QLabel("𝛿ZPE ="), row, 0, 1, 1,
                                Qt.AlignRight | Qt.AlignVCenter)

        self.zpe_line = QLineEdit()
        self.zpe_line.setReadOnly(True)
        self.zpe_line.setToolTip("zero-point energy correction")
        thermo_layout.addWidget(self.zpe_line, row, 1, 1, 1, Qt.AlignTop)

        thermo_layout.addWidget(QLabel("E<sub>h</sub>"), row, 2, 1, 1,
                                Qt.AlignVCenter)

        row += 1

        thermo_layout.addWidget(QLabel("𝛿H<sub>RRHO</sub> ="), row, 0, 1, 1,
                                Qt.AlignRight | Qt.AlignVCenter)

        self.enthalpy_line = QLineEdit()
        self.enthalpy_line.setReadOnly(True)
        self.enthalpy_line.setToolTip("RRHO enthalpy correction")
        thermo_layout.addWidget(self.enthalpy_line, row, 1, 1, 1, Qt.AlignTop)

        thermo_layout.addWidget(QLabel("E<sub>h</sub>"), row, 2, 1, 1,
                                Qt.AlignVCenter)

        row += 1

        thermo_layout.addWidget(QLabel("𝛿G<sub>RRHO</sub> ="), row, 0, 1, 1,
                                Qt.AlignRight | Qt.AlignVCenter)

        self.rrho_g_line = QLineEdit()
        self.rrho_g_line.setReadOnly(True)
        self.rrho_g_line.setToolTip(
            """RRHO free energy correction\nRotational motion is completely independent from vibrations\nNormal mode vibrations behave like harmonic oscillators"""
        )
        thermo_layout.addWidget(self.rrho_g_line, row, 1, 1, 1, Qt.AlignTop)

        thermo_layout.addWidget(QLabel("E<sub>h</sub>"), row, 2, 1, 1,
                                Qt.AlignVCenter)

        row += 1

        dqrrho_g_label = QLabel()
        dqrrho_g_label.setText(
            "<a href=\"Grimme's Quasi-RRHO\" style=\"text-decoration: none;\">𝛿G<sub>Quasi-RRHO</sub></a> ="
        )
        dqrrho_g_label.setTextFormat(Qt.RichText)
        dqrrho_g_label.setTextInteractionFlags(Qt.TextBrowserInteraction)
        dqrrho_g_label.linkActivated.connect(self.open_link)
        dqrrho_g_label.setToolTip("click to open a relevant reference\n" + \
                                  "see the \"Computation of internal (gas‐phase) entropies\" section for a description of the method")

        thermo_layout.addWidget(dqrrho_g_label, row, 0, 1, 1,
                                Qt.AlignRight | Qt.AlignVCenter)

        self.qrrho_g_line = QLineEdit()
        self.qrrho_g_line.setReadOnly(True)
        self.qrrho_g_line.setToolTip("Quasi-RRHO free energy correction\n" + \
        "Vibrational entropy of each mode is weighted and complemented with rotational entropy\n" + \
        "The weighting is much lower for modes with frequencies less than 𝜔\u2080")
        thermo_layout.addWidget(self.qrrho_g_line, row, 1, 1, 1, Qt.AlignTop)

        thermo_layout.addWidget(QLabel("E<sub>h</sub>"), row, 2, 1, 1,
                                Qt.AlignVCenter)

        row += 1

        dqharm_g_label = QLabel("")
        dqharm_g_label.setText(
            "<a href=\"Truhlar's Quasi-Harmonic\" style=\"text-decoration: none;\">𝛿G<sub>Quasi-Harmonic</sub></a> ="
        )
        dqharm_g_label.setTextFormat(Qt.RichText)
        dqharm_g_label.setTextInteractionFlags(Qt.TextBrowserInteraction)
        dqharm_g_label.linkActivated.connect(self.open_link)
        dqharm_g_label.setToolTip("click to open a relevant reference\n" + \
                                  "see the \"Computations\" section for a description of the method")

        thermo_layout.addWidget(dqharm_g_label, row, 0, 1, 1,
                                Qt.AlignRight | Qt.AlignVCenter)

        self.qharm_g_line = QLineEdit()
        self.qharm_g_line.setReadOnly(True)
        self.qharm_g_line.setToolTip("Quasi-hamonic free energy correction\n" + \
        "For entropy, real vibrational modes lower than 𝜔\u2080 are treated as 𝜔\u2080")
        thermo_layout.addWidget(self.qharm_g_line, row, 1, 1, 1, Qt.AlignTop)

        thermo_layout.addWidget(QLabel("E<sub>h</sub>"), row, 2, 1, 1,
                                Qt.AlignVCenter)

        thermo_layout.addWidget(QWidget(), row + 1, 0)

        thermo_layout.setColumnStretch(0, 0)
        thermo_layout.setColumnStretch(1, 1)
        for i in range(0, row):
            thermo_layout.setRowStretch(i, 0)

        thermo_layout.setRowStretch(row + 1, 1)

        row = 0
        # for for total
        sum_area_widget = QGroupBox("Thermochemistry")
        sum_layout = QGridLayout(sum_area_widget)

        sum_layout.addWidget(QLabel("ZPE ="), row, 0, 1, 1,
                             Qt.AlignRight | Qt.AlignVCenter)

        self.zpe_sum_line = QLineEdit()
        self.zpe_sum_line.setReadOnly(True)
        self.zpe_sum_line.setToolTip(
            "sum of electronic energy and ZPE correction")
        sum_layout.addWidget(self.zpe_sum_line, row, 1, 1, 1, Qt.AlignTop)

        sum_layout.addWidget(QLabel("E<sub>h</sub>"), row, 2, 1, 1,
                             Qt.AlignVCenter)

        row += 1

        sum_layout.addWidget(QLabel("H<sub>RRHO</sub> ="), row, 0, 1, 1,
                             Qt.AlignRight | Qt.AlignVCenter)

        self.h_sum_line = QLineEdit()
        self.h_sum_line.setReadOnly(True)
        self.h_sum_line.setToolTip(
            "sum of electronic energy and RRHO enthalpy correction")
        sum_layout.addWidget(self.h_sum_line, row, 1, 1, 1, Qt.AlignTop)

        sum_layout.addWidget(QLabel("E<sub>h</sub>"), row, 2, 1, 1,
                             Qt.AlignVCenter)

        row += 1

        sum_layout.addWidget(QLabel("G<sub>RRHO</sub> ="), row, 0, 1, 1,
                             Qt.AlignRight | Qt.AlignVCenter)

        self.rrho_g_sum_line = QLineEdit()
        self.rrho_g_sum_line.setReadOnly(True)
        self.rrho_g_sum_line.setToolTip(
            "sum of electronic energy and RRHO free energy correction\nRotational motion is completely independent from vibrations\nNormal mode vibrations behave like harmonic oscillators"
        )
        sum_layout.addWidget(self.rrho_g_sum_line, row, 1, 1, 1, Qt.AlignTop)

        sum_layout.addWidget(QLabel("E<sub>h</sub>"), row, 2, 1, 1,
                             Qt.AlignVCenter)

        row += 1

        sum_layout.addWidget(QLabel("G<sub>Quasi-RRHO</sub> ="), row, 0, 1, 1,
                             Qt.AlignRight | Qt.AlignVCenter)

        self.qrrho_g_sum_line = QLineEdit()
        self.qrrho_g_sum_line.setReadOnly(True)
        self.qrrho_g_sum_line.setToolTip("Sum of electronic energy and quasi-RRHO free energy correction\n" + \
        "Vibrational entropy of each mode is weighted and complemented with rotational entropy\n" + \
        "The weighting is much lower for modes with frequencies less than 𝜔\u2080")
        sum_layout.addWidget(self.qrrho_g_sum_line, row, 1, 1, 1, Qt.AlignTop)

        sum_layout.addWidget(QLabel("E<sub>h</sub>"), row, 2, 1, 1,
                             Qt.AlignVCenter)

        row += 1

        sum_layout.addWidget(QLabel("G<sub>Quasi-Harmonic</sub> ="), row, 0, 1,
                             1, Qt.AlignRight | Qt.AlignVCenter)

        self.qharm_g_sum_line = QLineEdit()
        self.qharm_g_sum_line.setReadOnly(True)
        self.qharm_g_sum_line.setToolTip("Sum of electronic energy and quasi-harmonic free energy correction\n" + \
        "For entropy, real vibrational modes lower than 𝜔\u2080 are treated as 𝜔\u2080")
        sum_layout.addWidget(self.qharm_g_sum_line, row, 1, 1, 1, Qt.AlignTop)

        sum_layout.addWidget(QLabel("E<sub>h</sub>"), row, 2, 1, 1,
                             Qt.AlignVCenter)

        sum_layout.addWidget(QWidget(), row + 1, 0)

        sum_layout.setColumnStretch(0, 0)
        for i in range(0, row):
            sum_layout.setRowStretch(i, 0)

        sum_layout.setRowStretch(row + 1, 1)

        splitter = QSplitter(Qt.Horizontal)
        splitter.setChildrenCollapsible(False)
        splitter.addWidget(sp_area_widget)
        splitter.addWidget(therm_area_widget)
        splitter.addWidget(sum_area_widget)

        absolute_layout.addWidget(splitter)

        self.status = QStatusBar()
        self.status.setSizeGripEnabled(False)
        self.status.setStyleSheet("color: red")
        absolute_layout.addWidget(self.status, 1, 0, 1, 1, Qt.AlignTop)

        self.tab_widget.addTab(absolute_widget, "absolute")

        relative_widget = QWidget()
        relative_layout = QGridLayout(relative_widget)

        size = [self.settings.ref_col_1, self.settings.ref_col_2]
        self.ref_group = ThermoGroup("reference group", self.session,
                                     self.nrg_fr, self.thermo_co, size)
        self.ref_group.changes.connect(self.calc_relative_thermo)
        relative_layout.addWidget(self.ref_group, 0, 0, 1, 3, Qt.AlignTop)

        size = [self.settings.other_col_1, self.settings.other_col_2]
        self.other_group = ThermoGroup("other group", self.session,
                                       self.nrg_fr, self.thermo_co, size)
        self.other_group.changes.connect(self.calc_relative_thermo)
        relative_layout.addWidget(self.other_group, 0, 3, 1, 3, Qt.AlignTop)

        self.relative_temperature = QDoubleSpinBox()
        self.relative_temperature.setMaximum(2**31 - 1)
        self.relative_temperature.setValue(self.settings.rel_temp)
        self.relative_temperature.setSingleStep(10)
        self.relative_temperature.setSuffix(" K")
        self.relative_temperature.setMinimum(0)
        self.relative_temperature.valueChanged.connect(
            self.calc_relative_thermo)
        relative_layout.addWidget(QLabel("T ="), 1, 0, 1, 1,
                                  Qt.AlignRight | Qt.AlignVCenter)
        relative_layout.addWidget(self.relative_temperature, 1, 1, 1, 5,
                                  Qt.AlignLeft | Qt.AlignVCenter)

        self.relative_v0 = QDoubleSpinBox()
        self.relative_v0.setMaximum(2**31 - 1)
        self.relative_v0.setValue(self.settings.w0)
        self.relative_v0.setSingleStep(25)
        self.relative_v0.setSuffix(" cm\u207b\u00b9")
        self.relative_v0.setMinimum(0)
        self.relative_v0.setToolTip(
            "frequency parameter for quasi treatments of entropy")
        self.relative_v0.valueChanged.connect(self.calc_relative_thermo)
        relative_layout.addWidget(QLabel("𝜔<sub>0</sub> ="), 2, 0, 1, 1,
                                  Qt.AlignRight | Qt.AlignVCenter)
        relative_layout.addWidget(self.relative_v0, 2, 1, 1, 5,
                                  Qt.AlignLeft | Qt.AlignVCenter)

        relative_layout.addWidget(
            QLabel("Boltzmann-weighted relative energies in kcal/mol:"), 3, 0,
            1, 6, Qt.AlignVCenter | Qt.AlignLeft)

        relative_layout.addWidget(QLabel("ΔE"), 4, 0,
                                  Qt.AlignTop | Qt.AlignVCenter)
        self.relative_dE = QLineEdit()
        self.relative_dE.setReadOnly(True)
        relative_layout.addWidget(self.relative_dE, 5, 0,
                                  Qt.AlignTop | Qt.AlignVCenter)

        relative_layout.addWidget(QLabel("ΔZPE"), 4, 1,
                                  Qt.AlignTop | Qt.AlignVCenter)
        self.relative_dZPVE = QLineEdit()
        self.relative_dZPVE.setReadOnly(True)
        relative_layout.addWidget(self.relative_dZPVE, 5, 1,
                                  Qt.AlignTop | Qt.AlignVCenter)

        relative_layout.addWidget(QLabel("ΔH<sub>RRHO</sub>"), 4, 2,
                                  Qt.AlignTop | Qt.AlignVCenter)
        self.relative_dH = QLineEdit()
        self.relative_dH.setReadOnly(True)
        relative_layout.addWidget(self.relative_dH, 5, 2,
                                  Qt.AlignTop | Qt.AlignVCenter)

        relative_layout.addWidget(QLabel("ΔG<sub>RRHO</sub>"), 4, 3,
                                  Qt.AlignTop | Qt.AlignVCenter)
        self.relative_dG = QLineEdit()
        self.relative_dG.setReadOnly(True)
        relative_layout.addWidget(self.relative_dG, 5, 3,
                                  Qt.AlignTop | Qt.AlignVCenter)

        relative_layout.addWidget(QLabel("ΔG<sub>Quasi-RRHO</sub>"), 4, 4,
                                  Qt.AlignTop | Qt.AlignVCenter)
        self.relative_dQRRHOG = QLineEdit()
        self.relative_dQRRHOG.setReadOnly(True)
        relative_layout.addWidget(self.relative_dQRRHOG, 5, 4,
                                  Qt.AlignTop | Qt.AlignVCenter)

        relative_layout.addWidget(QLabel("ΔG<sub>Quasi-Harmonic</sub>"), 4, 5,
                                  Qt.AlignTop | Qt.AlignVCenter)
        self.relative_dQHARMG = QLineEdit()
        self.relative_dQHARMG.setReadOnly(True)
        relative_layout.addWidget(self.relative_dQHARMG, 5, 5,
                                  Qt.AlignTop | Qt.AlignVCenter)

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

        self.tab_widget.addTab(relative_widget, "relative")

        #menu stuff
        menu = QMenuBar()

        export = menu.addMenu("&Export")
        copy = QAction("&Copy CSV to clipboard", self.tool_window.ui_area)
        copy.triggered.connect(self.copy_csv)
        shortcut = QKeySequence(Qt.CTRL + Qt.Key_C)
        copy.setShortcut(shortcut)
        export.addAction(copy)
        self.copy = copy

        save = QAction("&Save CSV...", self.tool_window.ui_area)
        save.triggered.connect(self.save_csv)
        #this shortcut interferes with main window's save shortcut
        #I've tried different shortcut contexts to no avail
        #thanks Qt...
        #shortcut = QKeySequence(Qt.CTRL + Qt.Key_S)
        #save.setShortcut(shortcut)
        #save.setShortcutContext(Qt.WidgetShortcut)
        export.addAction(save)

        delimiter = export.addMenu("Delimiter")

        comma = QAction("comma", self.tool_window.ui_area, checkable=True)
        comma.setChecked(self.settings.delimiter == "comma")
        comma.triggered.connect(lambda *args, delim="comma": self.settings.
                                __setattr__("delimiter", delim))
        delimiter.addAction(comma)

        tab = QAction("tab", self.tool_window.ui_area, checkable=True)
        tab.setChecked(self.settings.delimiter == "tab")
        tab.triggered.connect(lambda *args, delim="tab": self.settings.
                              __setattr__("delimiter", delim))
        delimiter.addAction(tab)

        space = QAction("space", self.tool_window.ui_area, checkable=True)
        space.setChecked(self.settings.delimiter == "space")
        space.triggered.connect(lambda *args, delim="space": self.settings.
                                __setattr__("delimiter", delim))
        delimiter.addAction(space)

        semicolon = QAction("semicolon",
                            self.tool_window.ui_area,
                            checkable=True)
        semicolon.setChecked(self.settings.delimiter == "semicolon")
        semicolon.triggered.connect(lambda *args, delim="semicolon": self.
                                    settings.__setattr__("delimiter", delim))
        delimiter.addAction(semicolon)

        add_header = QAction("&Include CSV header",
                             self.tool_window.ui_area,
                             checkable=True)
        add_header.setChecked(self.settings.include_header)
        add_header.triggered.connect(self.header_check)
        export.addAction(add_header)

        comma.triggered.connect(
            lambda *args, action=tab: action.setChecked(False))
        comma.triggered.connect(
            lambda *args, action=space: action.setChecked(False))
        comma.triggered.connect(
            lambda *args, action=semicolon: action.setChecked(False))

        tab.triggered.connect(
            lambda *args, action=comma: action.setChecked(False))
        tab.triggered.connect(
            lambda *args, action=space: action.setChecked(False))
        tab.triggered.connect(
            lambda *args, action=semicolon: action.setChecked(False))

        space.triggered.connect(
            lambda *args, action=comma: action.setChecked(False))
        space.triggered.connect(
            lambda *args, action=tab: action.setChecked(False))
        space.triggered.connect(
            lambda *args, action=semicolon: action.setChecked(False))

        semicolon.triggered.connect(
            lambda *args, action=comma: action.setChecked(False))
        semicolon.triggered.connect(
            lambda *args, action=tab: action.setChecked(False))
        semicolon.triggered.connect(
            lambda *args, action=space: action.setChecked(False))

        menu.setNativeMenuBar(False)
        self._menu = menu
        layout.setMenuBar(menu)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def calc_relative_thermo(self, *args):
        """changes the values on the 'relative' tab
        called when the tool is opened and whenever something changes on the relative tab"""
        def calc_free_energies(nrg_list, co_list, T, w0):
            """returns lists for ZPVE, H, RRHO G, QRRHO G, and QHARM G
            for the items in nrg_list and co_list at temperature T
            and frequency parameter w0"""
            ZPVEs = []
            Hs = []
            Gs = []
            RRHOG = []
            QHARMG = []
            for i in range(0, len(nrg_list)):
                ZPVEs.append([])
                Hs.append([])
                Gs.append([])
                RRHOG.append([])
                QHARMG.append([])
                for nrg, co in zip(nrg_list[i], co_list[i]):
                    ZPVE = co.ZPVE
                    ZPVEs[-1].append(nrg + ZPVE)

                    dH = co.therm_corr(T, w0, CompOutput.RRHO)[1]
                    Hs[-1].append(nrg + dH)

                    dG = co.calc_G_corr(temperature=T,
                                        v0=w0,
                                        method=CompOutput.RRHO)
                    Gs[-1].append(nrg + dG)

                    dQRRHOG = co.calc_G_corr(temperature=T,
                                             v0=w0,
                                             method=CompOutput.QUASI_RRHO)
                    RRHOG[-1].append(nrg + dQRRHOG)

                    dQHARMG = co.calc_G_corr(temperature=T,
                                             v0=w0,
                                             method=CompOutput.QUASI_HARMONIC)
                    QHARMG[-1].append(nrg + dQHARMG)

            return ZPVEs, Hs, Gs, RRHOG, QHARMG

        def boltzmann_weight(energies1, energies2, T):
            """
            energies - list of lists
                       list axis 0 - molecule groups
                            axis 1 - energies of conformers
            boltzmann weight energies for conformers 
            combine energies for molecule groups 
            return the difference"""
            totals1 = []
            totals2 = []

            beta = UNIT.HART_TO_JOULE / (PHYSICAL.KB * T)

            for energy_group in energies1:
                if len(energy_group) == 0:
                    continue

                rezero = min(energy_group)
                rel_nrgs = [(x - rezero) for x in energy_group]
                weights = [np.exp(-beta * nrg) for nrg in rel_nrgs]

                totals1.append(-PHYSICAL.BOLTZMANN * T * np.log(sum(weights)) +
                               rezero * UNIT.HART_TO_KCAL)

            for energy_group in energies2:
                if len(energy_group) == 0:
                    continue

                rezero = min(energy_group)
                rel_nrgs = [(x - rezero) for x in energy_group]
                weights = [np.exp(-beta * nrg) for nrg in rel_nrgs]

                totals2.append(-PHYSICAL.BOLTZMANN * T * np.log(sum(weights)) +
                               rezero * UNIT.HART_TO_KCAL)

            return sum(totals2) - sum(totals1)

        ref_Es = self.ref_group.energies()
        ref_cos = self.ref_group.compOutputs()

        empty_groups = []
        for i, group in enumerate(ref_cos):
            if len(group) == 0:
                empty_groups.append(i)

        for ndx in empty_groups[::-1]:
            ref_Es.pop(ndx)
            ref_cos.pop(ndx)

        other_Es = self.other_group.energies()
        other_cos = self.other_group.compOutputs()

        empty_groups = []
        for i, group in enumerate(other_cos):
            if len(group) == 0:
                empty_groups.append(i)

        for ndx in empty_groups[::-1]:
            other_Es.pop(ndx)
            other_cos.pop(ndx)

        if any(
                len(x) == 0 or all(len(x) == 0 for y in x)
                for x in [ref_Es, other_Es, ref_cos, other_cos]):
            self.relative_dE.setText("")
            self.relative_dZPVE.setText("")
            self.relative_dH.setText("")
            self.relative_dG.setText("")
            self.relative_dQRRHOG.setText("")
            self.relative_dQHARMG.setText("")
            return

        T = self.relative_temperature.value()
        self.settings.rel_temp = T
        w0 = self.relative_v0.value()

        if w0 != self.settings.w0:
            self.settings.w0 = w0

        ref_ZPVEs, ref_Hs, ref_Gs, ref_QRRHOGs, ref_QHARMGs = calc_free_energies(
            ref_Es, ref_cos, T, w0)
        other_ZPVEs, other_Hs, other_Gs, other_QRRHOGs, other_QHARMGs = calc_free_energies(
            other_Es, other_cos, T, w0)

        rel_E = boltzmann_weight(ref_Es, other_Es, T)
        rel_ZPVE = boltzmann_weight(ref_ZPVEs, other_ZPVEs, T)
        rel_H = boltzmann_weight(ref_Hs, other_Hs, T)
        rel_G = boltzmann_weight(ref_Gs, other_Gs, T)
        rel_QRRHOG = boltzmann_weight(ref_QRRHOGs, other_QRRHOGs, T)
        rel_QHARMG = boltzmann_weight(ref_QHARMGs, other_QHARMGs, T)

        self.relative_dE.setText("%.1f" % rel_E)
        self.relative_dZPVE.setText("%.1f" % rel_ZPVE)
        self.relative_dH.setText("%.1f" % rel_H)
        self.relative_dG.setText("%.1f" % rel_G)
        self.relative_dQRRHOG.setText("%.1f" % rel_QRRHOG)
        self.relative_dQHARMG.setText("%.1f" % rel_QHARMG)

    def open_link(self, theory):
        """open the oft-cited QRRHO or QHARM reference"""
        link = self.theory_helper[theory]
        run(self.session, "open %s" % link)

    def save_csv(self):
        """save data on current tab to CSV file"""
        filename, _ = QFileDialog.getSaveFileName(filter="CSV Files (*.csv)")
        if filename:
            s = self.get_csv()

            with open(filename, 'w') as f:
                f.write(s.strip())

            self.session.logger.status("saved to %s" % filename)

    def copy_csv(self):
        """put CSV data for current tab on the clipboard"""
        app = QApplication.instance()
        clipboard = app.clipboard()
        csv = self.get_csv()
        clipboard.setText(csv)
        self.session.logger.status("copied to clipboard")

    def get_csv(self):
        """get CSV data for the current tab"""
        if self.settings.delimiter == "comma":
            delim = ","
        elif self.settings.delimiter == "space":
            delim = " "
        elif self.settings.delimiter == "tab":
            delim = "\t"
        elif self.settings.delimiter == "semicolon":
            delim = ";"

        if self.tab_widget.currentIndex() == 0:
            if self.settings.include_header:
                s = delim.join(["E" , "ZPE", "H(RRHO)", "G(RRHO)", "G(Quasi-RRHO)", "G(Quasi-harmonic)", \
                                "dZPE", "dH(RRHO)", "dG(RRHO)", "dG(Quasi-RRHO)", "dG(Quasi-harmonic)", \
                                "SP File", "Thermo File"])

                s += "\n"
            else:
                s = ""

            float_fmt = "%.12f" + delim
            str_dmt = "%s" + delim + "%s"

            fmt = 11 * float_fmt + str_dmt + "\n"

            E = float(self.sp_nrg_line.text())

            dZPE = float(self.zpe_line.text())
            dH = float(self.enthalpy_line.text())
            rrho_dG = float(self.rrho_g_line.text())
            qrrho_dG = float(self.qrrho_g_line.text())
            qharm_dG = float(self.qharm_g_line.text())

            ZPE = float(self.zpe_sum_line.text())
            H = float(self.h_sum_line.text())
            rrho_G = float(self.rrho_g_sum_line.text())
            qrrho_G = float(self.qrrho_g_sum_line.text())
            qharm_G = float(self.qharm_g_sum_line.text())

            sp_mdl = self.sp_selector.currentData()
            sp_name = sp_mdl.name

            therm_mdl = self.thermo_selector.currentData()
            therm_name = therm_mdl.name

            s += fmt % (E, ZPE, H, rrho_G, qrrho_G, qharm_G, dZPE, dH, rrho_dG,
                        qrrho_dG, qharm_dG, sp_name, therm_name)

        elif self.tab_widget.currentIndex() == 1:
            if self.settings.include_header:
                s = delim.join([
                    "ΔE", "ΔZPE", "ΔH(RRHO)", "ΔG(RRHO)", "ΔG(Quasi-RRHO)",
                    "ΔG(Quasi-Harmonic)"
                ])
                s += "\n"
            else:
                s = ""

            float_fmt = "%.1f" + delim
            fmt = 6 * float_fmt
            try:
                dE = float(self.relative_dE.text())
                dZPVE = float(self.relative_dZPVE.text())
                dH = float(self.relative_dH.text())
                dG = float(self.relative_dG.text())
                dQRRHOG = float(self.relative_dQRRHOG.text())
                dQHARMG = float(self.relative_dQHARMG.text())
                s += fmt % (dE, dZPVE, dH, dG, dQRRHOG, dQHARMG)
            except ValueError:
                pass

        return s

    def header_check(self, state):
        """user has [un]checked the 'include header' option on the menu"""
        if state:
            self.settings.include_header = True
        else:
            self.settings.include_header = False

    def set_sp(self):
        """set energy entry for when sp model changes"""
        if self.sp_selector.currentIndex() >= 0:
            fr = self.sp_selector.currentData()

            self.check_geometry_rmsd("SP")

            self.sp_nrg_line.setText("%.6f" % fr.other['energy'])
        else:
            self.sp_nrg_line.setText("")

        self.update_sum()

    def set_thermo_mdl(self):
        """frequencies filereader has changed on the absolute tab
        also changes the temperature option (on the absolute tab only)"""
        if self.thermo_selector.currentIndex() >= 0:
            fr = self.thermo_selector.currentData()

            self.check_geometry_rmsd("THERM")

            if 'temperature' in fr.other:
                self.temperature_line.setValue(fr.other['temperature'])

        self.set_thermo()

    def check_geometry_rmsd(self, *args):
        """check RMSD between energy and frequency filereader on the absolute tab
        if the RMSD is > 10^-5 or the number of atoms is different, put a warning in the
        status bar"""
        if self.thermo_selector.currentIndex(
        ) >= 0 and self.sp_selector.currentIndex() >= 0:
            fr = self.sp_selector.currentData()
            fr2 = self.thermo_selector.currentData()

            if len(fr.atoms) != len(fr2.atoms):
                self.status.showMessage(
                    "structures are not the same: different number of atoms")
                return

            geom = Geometry(fr)
            geom2 = Geometry(fr2)
            rmsd = geom.RMSD(geom2)
            if not isclose(rmsd, 0, atol=10**-5):
                rmsd = geom.RMSD(geom2, sort=True)

            if not isclose(rmsd, 0, atol=10**-5):
                self.status.showMessage(
                    "structures might not be the same - RMSD = %.4f" % rmsd)
            else:
                self.status.showMessage("")

    def set_thermo(self):
        """computes thermo corrections and sets thermo entries for when thermo model changes"""
        #index of combobox is -1 when combobox has no entries
        if self.thermo_selector.currentIndex() >= 0:
            fr = self.thermo_selector.currentData()
            if fr not in self.thermo_co:
                self.thermo_co[fr] = CompOutput(fr)
            co = self.thermo_co[fr]

            v0 = self.v0_edit.value()

            if v0 != self.settings.w0:
                self.settings.w0 = v0

            T = self.temperature_line.value()
            if not T:
                return

            dZPE = co.ZPVE
            #compute enthalpy and entropy at this temperature
            #AaronTools uses Grimme's Quasi-RRHO, but this is the same as RRHO when w0=0
            dE, dH, s = co.therm_corr(temperature=T, v0=0, method="RRHO")
            rrho_dg = dH - T * s
            #compute G with quasi entropy treatments
            qrrho_dg = co.calc_G_corr(v0=v0, temperature=T, method="QRRHO")
            qharm_dg = co.calc_G_corr(v0=v0, temperature=T, method="QHARM")

            self.zpe_line.setText("%.6f" % dZPE)
            self.enthalpy_line.setText("%.6f" % dH)
            self.rrho_g_line.setText("%.6f" % rrho_dg)
            self.qrrho_g_line.setText("%.6f" % qrrho_dg)
            self.qharm_g_line.setText("%.6f" % qharm_dg)
        else:
            self.zpe_line.setText("")
            self.enthalpy_line.setText("")
            self.rrho_g_line.setText("")
            self.qrrho_g_line.setText("")
            self.qharm_g_line.setText("")

        self.update_sum()

    def update_sum(self):
        """updates the sum of energy and thermo corrections"""
        dZPE = self.zpe_line.text()
        dH = self.enthalpy_line.text()
        dG = self.rrho_g_line.text()
        dG_qrrho = self.qrrho_g_line.text()
        dG_qharm = self.qharm_g_line.text()

        nrg = self.sp_nrg_line.text()

        if len(dH) == 0 or len(nrg) == 0:
            self.zpe_sum_line.setText("")
            self.h_sum_line.setText("")
            self.rrho_g_sum_line.setText("")
            self.qrrho_g_sum_line.setText("")
            self.qharm_g_sum_line.setText("")
            return
        else:
            dZPE = float(dZPE)
            dH = float(dH)
            dG = float(dG)
            dG_qrrho = float(dG_qrrho)
            dG_qharm = float(dG_qharm)

            nrg = float(nrg)

            zpe = nrg + dZPE
            enthalpy = nrg + dH
            rrho_g = nrg + dG
            qrrho_g = nrg + dG_qrrho
            qharm_g = nrg + dG_qharm

            self.zpe_sum_line.setText("%.6f" % zpe)
            self.h_sum_line.setText("%.6f" % enthalpy)
            self.rrho_g_sum_line.setText("%.6f" % rrho_g)
            self.qrrho_g_sum_line.setText("%.6f" % qrrho_g)
            self.qharm_g_sum_line.setText("%.6f" % qharm_g)

    def display_help(self):
        """Show the help for this tool in the help viewer."""
        from chimerax.core.commands import run
        run(self.session,
            'open %s' % self.help if self.help is not None else "")

    def delete(self):
        #overload because closing a tool window doesn't destroy any widgets on it
        self.settings.ref_col_1 = self.ref_group.tree.columnWidth(0)
        self.settings.ref_col_2 = self.ref_group.tree.columnWidth(1)

        self.settings.other_col_1 = self.other_group.tree.columnWidth(0)
        self.settings.other_col_2 = self.other_group.tree.columnWidth(1)

        self.sp_selector.deleteLater()
        self.thermo_selector.deleteLater()
        self.ref_group.deleteLater()
        self.other_group.deleteLater()

        return super().delete()

    def close(self):
        #overload because closing a tool window doesn't destroy any widgets on it
        self.settings.ref_col_1 = self.ref_group.tree.columnWidth(0)
        self.settings.ref_col_2 = self.ref_group.tree.columnWidth(1)

        self.settings.other_col_1 = self.other_group.tree.columnWidth(0)
        self.settings.other_col_2 = self.other_group.tree.columnWidth(1)

        self.sp_selector.deleteLater()
        self.thermo_selector.deleteLater()
        self.ref_group.deleteLater()
        self.other_group.deleteLater()

        return super().close()
Пример #8
0
class AARONInputBuilder(ToolInstance):
    SESSION_ENDURING = False
    SESSION_SAVE = False

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

        self.settings = _InputGeneratorSettings(session,
                                                "Build QM Input",
                                                version="2")

        self.tool_window = MainToolWindow(self)

        self._build_ui()

    def _build_ui(self):
        layout = QGridLayout()

        tab_widget = QTabWidget()
        layout.addWidget(tab_widget)

        #job details
        job_widget = QWidget()
        job_layout = QFormLayout(job_widget)

        self.reaction_type = QComboBox()
        job_layout.addRow("reaction type:", self.reaction_type)

        self.template = QComboBox()
        job_layout.addRow("template:", self.template)

        self.input_format = QComboBox()
        self.input_format.addItems(['Perl', 'Python'])
        job_layout.addRow("AARON input format:", self.input_format)

        self.charge = QSpinBox()
        self.charge.setRange(-5, 5)
        job_layout.addRow("charge:", self.charge)

        self.multiplicity = QSpinBox()
        self.multiplicity.setRange(1, 3)
        job_layout.addRow("multiplicity:", self.multiplicity)

        tab_widget.addTab(job_widget, "job details")

        #settings
        settings_widget = QWidget()
        settings_layout = QGridLayout(settings_widget)

        settings_tabs = QTabWidget()
        settings_layout.addWidget(settings_tabs)

        self.low_widget = AARONSettingsWidget(self.settings)
        settings_tabs.addTab(self.low_widget, "low")

        self.default_widget = AARONSettingsWidget(self.settings)
        settings_tabs.addTab(self.default_widget, "default")

        self.high_widget = AARONSettingsWidget(self.settings)
        settings_tabs.addTab(self.high_widget, "single point energy")

        tab_widget.addTab(settings_widget, "settings")

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)
Пример #9
0
class _ElementPicker(ToolInstance):
    def __init__(self, *args):
        super().__init__(*args)

        self.tool_window = MainToolWindow(self)
        self._build_ui()

    def _build_ui(self):
        layout = QFormLayout()

        initial_elements = []
        if ChangeElementMouseMode.element:
            initial_elements = [ChangeElementMouseMode.element]
        else:
            initial_elements = ["C"]

        self.periodic_table = PeriodicTable(select_multiple=False,
                                            initial_elements=initial_elements)
        self.periodic_table.elementSelectionChanged.connect(
            self.element_changed)
        layout.addRow(self.periodic_table)

        self.vsepr = QComboBox()
        self.vsepr.addItems([
            "do not change",  # 0
            "linear (1 bond)",  # 1
            "linear (2 bonds)",  # 2 
            "trigonal planar (2 bonds)",  # 3
            "tetrahedral (2 bonds)",  # 4 
            "trigonal planar",  # 5
            "tetrahedral (3 bonds)",  # 6
            "T-shaped",  # 7
            "trigonal pyramidal",  # 8
            "tetrahedral",  # 9
            "sawhorse",  #10
            "seesaw",  #11
            "square planar",  #12
            "trigonal bipyramidal",  #13
            "square pyramidal",  #14
            "pentagonal",  #15
            "octahedral",  #16
            "hexagonal",  #17
            "trigonal prismatic",  #18
            "pentagonal pyramidal",  #19
            "capped octahedral",  #20
            "capped trigonal prismatic",  #21
            "heptagonal",  #22
            "hexagonal pyramidal",  #23
            "pentagonal bipyramidal",  #24
            "biaugmented trigonal prismatic",  #25
            "cubic",  #26
            "elongated trigonal bipyramidal",  #27
            "hexagonal bipyramidal",  #28
            "heptagonal pyramidal",  #29
            "octagonal",  #30
            "square antiprismatic",  #31
            "trigonal dodecahedral",  #32
            "capped cube",  #33
            "capped square antiprismatic",  #34
            "enneagonal",  #35
            "heptagonal bipyramidal",  #36
            "hula-hoop",  #37
            "triangular cupola",  #38
            "tridiminished icosahedral",  #39
            "muffin",  #40
            "octagonal pyramidal",  #41
            "tricapped trigonal prismatic",  #42
        ])
        self.vsepr.setCurrentIndex(9)

        self.vsepr.insertSeparator(33)
        self.vsepr.insertSeparator(25)
        self.vsepr.insertSeparator(20)
        self.vsepr.insertSeparator(16)
        self.vsepr.insertSeparator(13)
        self.vsepr.insertSeparator(8)
        self.vsepr.insertSeparator(5)
        self.vsepr.insertSeparator(2)
        self.vsepr.insertSeparator(1)
        self.vsepr.insertSeparator(0)
        self.vsepr.currentTextChanged.connect(self.element_changed)
        layout.addRow("shape:", self.vsepr)

        self.keep_open = QCheckBox()
        layout.addRow("keep table open:", self.keep_open)

        if ChangeElementMouseMode.vsepr:
            ndx = self.vsepr.findText(ChangeElementMouseMode.vsepr,
                                      Qt.MatchExactly)
            if ndx != -1:
                self.vsepr.setCurrentIndex(ndx)

        do_it = QPushButton("set element and shape")
        do_it.clicked.connect(self.set_selected)
        layout.addRow(do_it)

        self.keep_open.stateChanged.connect(
            lambda state: do_it.setVisible(state != Qt.Checked))
        self.keep_open.stateChanged.connect(self.element_changed)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def element_changed(self):
        if self.keep_open.checkState() != Qt.Checked:
            return
        element = self.periodic_table.selectedElements()
        if element:
            ChangeElementMouseMode.element = element[0]
            ChangeElementMouseMode.vsepr = self.vsepr.currentText()
            self.session.logger.status(
                "change element mouse mode set to use %s %s" %
                (ChangeElementMouseMode.vsepr, ChangeElementMouseMode.element))

    def set_selected(self, *args):
        element = self.periodic_table.selectedElements()
        if element:
            ChangeElementMouseMode.element = element[0]
            ChangeElementMouseMode.vsepr = self.vsepr.currentText()
            self.session.logger.status(
                "change element mouse mode set to use %s %s" %
                (ChangeElementMouseMode.vsepr, ChangeElementMouseMode.element))

        if self.keep_open.checkState() != Qt.Checked:
            self.delete()
Пример #10
0
class AARONInputBuilder(ToolInstance):
    SESSION_ENDURING = False
    SESSION_SAVE = False

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

        self.theory_settings = _InputGeneratorSettings(session,
                                                       "Build QM Input",
                                                       version="3")

        self.tool_window = MainToolWindow(self)

        self._build_ui()

    def _build_ui(self):
        layout = QGridLayout()

        tab_widget = QTabWidget()
        layout.addWidget(tab_widget)

        structure_widget = QWidget()
        structure_layout = QGridLayout(structure_widget)
        tab_widget.addTab(structure_widget, "structure")

        self.structure_source = QComboBox()
        self.structure_source.addItems(
            ["AARON template", "file", "coordination complexes", "SMILES"])

        structure_layout.addWidget(QLabel("structure source:"), 0, 0, 1, 1,
                                   Qt.AlignLeft | Qt.AlignVCenter)

        structure_layout.addWidget(
            self.structure_source,
            0,
            1,
            1,
            1,
        )

        open_button = QPushButton("open")
        open_button.clicked.connect(self.open_template)
        structure_layout.addWidget(
            open_button,
            0,
            2,
            1,
            1,
        )

        # SMILES options
        self.smiles_options = QGroupBox("SMILES options")
        smiles_layout = QFormLayout(self.smiles_options)
        structure_layout.addWidget(self.smiles_options, 1, 0, 1, 3,
                                   Qt.AlignTop | Qt.AlignHCenter)

        self.smiles_line = QLineEdit()
        smiles_layout.addRow("SMILES string:", self.smiles_line)

        self.smiles_options.setVisible(
            self.structure_source.currentText() == "SMILES")
        self.structure_source.currentTextChanged.connect(
            lambda text: self.smiles_options.setVisible(text == "SMILES"))

        # coordination complexes options
        self.coord_comp_options = QGroupBox("Coordination Complex Options")
        coord_layout = QFormLayout(self.coord_comp_options)
        structure_layout.addWidget(self.coord_comp_options, 1, 0, 1, 3,
                                   Qt.AlignTop | Qt.AlignHCenter)

        create_coord_items(self,
                           coord_layout,
                           allow_minimization=False,
                           default_ele="Pd")

        self.coord_comp_options.setVisible(
            self.structure_source.currentText() == "coordination complexes")
        self.structure_source.currentTextChanged.connect(
            lambda text: self.coord_comp_options.setVisible(
                text == "coordination complexes"))

        # aaron template options
        self.aaron_options = QGroupBox("AARON template structures")
        aaron_layout = QFormLayout(self.aaron_options)
        structure_layout.addWidget(self.aaron_options, 1, 0, 1, 3,
                                   Qt.AlignTop | Qt.AlignHCenter)

        self.aaron_options.setVisible(
            self.structure_source.currentText() == "AARON template")
        self.structure_source.currentTextChanged.connect(
            lambda text: self.aaron_options.setVisible(text == "AARON template"
                                                       ))

        # file options
        self.file_options = QGroupBox("single file options")
        file_layout = QFormLayout(self.file_options)
        structure_layout.addWidget(self.file_options, 1, 0, 1, 3,
                                   Qt.AlignTop | Qt.AlignHCenter)

        file_widget = QWidget()
        file_browse_layout = QGridLayout(file_widget)

        self.template_file = QLineEdit()
        file_browse_layout.addWidget(self.template_file, 0, 0, 1, 1,
                                     Qt.AlignCenter)
        browse_button = QPushButton("browse...")
        # browse_button.clicked.connect(self.browse_file_template)
        file_browse_layout.addWidget(browse_button, 0, 1, 1, 1, Qt.AlignCenter)

        file_browse_layout.setColumnStretch(0, 1)
        file_browse_layout.setColumnStretch(1, 0)
        margins = file_browse_layout.contentsMargins()
        file_browse_layout.setContentsMargins(margins.left(), 0,
                                              margins.right(), 0)

        file_layout.addRow("file:", file_widget)

        self.file_options.setVisible(
            self.structure_source.currentText() == "file")
        self.structure_source.currentTextChanged.connect(
            lambda text: self.file_options.setVisible(text == "file"))

        self.optimize_template = QCheckBox()
        self.optimize_template.setCheckState(Qt.Checked)
        structure_layout.addWidget(
            QLabel("optimize template:"),
            2,
            0,
            1,
            1,
            Qt.AlignLeft | Qt.AlignVCenter,
        )
        structure_layout.addWidget(
            self.optimize_template,
            2,
            1,
            1,
            2,
            Qt.AlignLeft | Qt.AlignVCenter,
        )

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

        # structure changes
        changes_widget = QWidget()
        changes_layout = QGridLayout(changes_widget)
        tab_widget.addTab(changes_widget, "changes")

        # HPC settings
        hpc_widget = QWidget()
        hpc_layout = QGridLayout(hpc_widget)
        tab_widget.addTab(hpc_widget, "HPC")

        # theory settings
        theory_widget = QWidget()
        theory_layout = QGridLayout(theory_widget)
        tab_widget.addTab(theory_widget, "theory")

        # results
        results_widget = QWidget()
        results_layout = QGridLayout(results_widget)
        tab_widget.addTab(results_widget, "results")

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def open_template(self):
        pass
Пример #11
0
class Info(ToolInstance):

    help = "https://github.com/QChASM/SEQCROW/wiki/File-Info-Tool"

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

        self.tool_window = MainToolWindow(self)

        self.settings = _InfoSettings(self.session, name)

        self._build_ui()

    def _build_ui(self):
        layout = QVBoxLayout()

        self.file_selector = FilereaderComboBox(self.session)
        self.file_selector.currentIndexChanged.connect(self.fill_table)
        layout.insertWidget(0, self.file_selector, 0)

        tabs = QTabWidget()
        self.tabs = tabs
        layout.insertWidget(1, self.tabs, 1)

        general_info = QWidget()
        general_layout = QVBoxLayout(general_info)
        tabs.addTab(general_info, "general")

        self.table = QTableWidget()
        self.table.setColumnCount(2)
        self.table.setHorizontalHeaderLabels(['Data', 'Value'])
        self.table.horizontalHeader().setStretchLastSection(False)
        self.table.horizontalHeader().setSectionResizeMode(
            0, QHeaderView.Interactive)
        self.table.setEditTriggers(QTableWidget.NoEditTriggers)
        self.table.horizontalHeader().setSectionResizeMode(
            1, QHeaderView.Stretch)
        self.table.setSizePolicy(QSizePolicy.MinimumExpanding,
                                 QSizePolicy.MinimumExpanding)
        general_layout.insertWidget(1, self.table, 1)

        self.filter = QLineEdit()
        self.filter.setPlaceholderText("filter data")
        self.filter.textChanged.connect(self.apply_filter)
        self.filter.setClearButtonEnabled(True)
        general_layout.insertWidget(2, self.filter, 0)

        freq_info = QWidget()
        freq_layout = QVBoxLayout(freq_info)
        tabs.addTab(freq_info, "harmonic frequencies")

        self.freq_table = QTableWidget()
        self.freq_table.setColumnCount(4)
        self.freq_table.setHorizontalHeaderLabels([
            "Frequency (cm\u207b\u00b9)",
            "symmetry",
            "IR intensity",
            "force constant (mDyne/\u212B)",
        ])
        self.freq_table.setSortingEnabled(True)
        self.freq_table.setEditTriggers(QTableWidget.NoEditTriggers)
        for i in range(0, 4):
            self.freq_table.resizeColumnToContents(i)

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

        self.freq_table.setSizePolicy(QSizePolicy.MinimumExpanding,
                                      QSizePolicy.MinimumExpanding)
        freq_layout.insertWidget(1, self.freq_table, 1)

        anharm_info = QWidget()
        anharm_layout = QVBoxLayout(anharm_info)
        tabs.addTab(anharm_info, "anharmonic frequencies")

        anharm_layout.insertWidget(0, QLabel("fundamentals:"), 0)

        self.fundamental_table = QTableWidget()
        self.fundamental_table.setColumnCount(3)
        self.fundamental_table.setHorizontalHeaderLabels([
            "Fundamental (cm\u207b\u00b9)",
            "Δ\u2090\u2099\u2095 (cm\u207b\u00b9)",
            "IR intensity",
        ])
        self.fundamental_table.setSortingEnabled(True)
        self.fundamental_table.setEditTriggers(QTableWidget.NoEditTriggers)
        for i in range(0, 3):
            self.fundamental_table.resizeColumnToContents(i)

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

        self.fundamental_table.setSizePolicy(QSizePolicy.MinimumExpanding,
                                             QSizePolicy.MinimumExpanding)
        anharm_layout.insertWidget(1, self.fundamental_table, 1)

        # self.overtone_table = QTableWidget()
        # self.overtone_table.setColumnCount(3)
        # self.overtone_table.setHorizontalHeaderLabels(
        #     [
        #         "Fundamental (cm\u207b\u00b9)",
        #         "Overtone (cm\u207b\u00b9)",
        #         "IR intensity",
        #     ]
        # )
        # self.overtone_table.setSortingEnabled(True)
        # self.overtone_table.setEditTriggers(QTableWidget.NoEditTriggers)
        # for i in range(0, 3):
        #     self.overtone_table.resizeColumnToContents(i)
        #
        # self.overtone_table.horizontalHeader().setStretchLastSection(False)
        # self.overtone_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Fixed)
        # self.overtone_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Fixed)
        # self.overtone_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
        #
        # self.overtone_table.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
        # anharm_layout.insertWidget(2, self.overtone_table, 1)

        anharm_layout.insertWidget(2, QLabel("combinations and overtones:"), 0)

        self.combo_table = QTableWidget()
        self.combo_table.setColumnCount(4)
        self.combo_table.setHorizontalHeaderLabels([
            "Fundamental (cm\u207b\u00b9)",
            "Fundamental (cm\u207b\u00b9)",
            "Combination (cm\u207b\u00b9)",
            "IR intensity",
        ])
        self.combo_table.setSortingEnabled(True)
        self.combo_table.setEditTriggers(QTableWidget.NoEditTriggers)
        for i in range(0, 3):
            self.combo_table.resizeColumnToContents(i)

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

        self.combo_table.setSizePolicy(QSizePolicy.MinimumExpanding,
                                       QSizePolicy.MinimumExpanding)
        anharm_layout.insertWidget(3, self.combo_table, 1)

        menu = QMenuBar()

        export = menu.addMenu("&Export")
        copy = QAction("&Copy CSV to clipboard", self.tool_window.ui_area)
        copy.triggered.connect(self.copy_csv)
        shortcut = QKeySequence(Qt.CTRL + Qt.Key_C)
        copy.setShortcut(shortcut)
        export.addAction(copy)
        self.copy = copy

        save = QAction("&Save CSV...", self.tool_window.ui_area)
        save.triggered.connect(self.save_csv)
        export.addAction(save)

        delimiter = export.addMenu("Delimiter")

        comma = QAction("comma", self.tool_window.ui_area, checkable=True)
        comma.setChecked(self.settings.delimiter == "comma")
        comma.triggered.connect(lambda *args, delim="comma": self.settings.
                                __setattr__("delimiter", delim))
        delimiter.addAction(comma)

        tab = QAction("tab", self.tool_window.ui_area, checkable=True)
        tab.setChecked(self.settings.delimiter == "tab")
        tab.triggered.connect(lambda *args, delim="tab": self.settings.
                              __setattr__("delimiter", delim))
        delimiter.addAction(tab)

        # space = QAction("space", self.tool_window.ui_area, checkable=True)
        # space.setChecked(self.settings.delimiter == "space")
        # space.triggered.connect(lambda *args, delim="space": self.settings.__setattr__("delimiter", delim))
        # delimiter.addAction(space)

        semicolon = QAction("semicolon",
                            self.tool_window.ui_area,
                            checkable=True)
        semicolon.setChecked(self.settings.delimiter == "semicolon")
        semicolon.triggered.connect(lambda *args, delim="semicolon": self.
                                    settings.__setattr__("delimiter", delim))
        delimiter.addAction(semicolon)

        add_header = QAction("&Include CSV header",
                             self.tool_window.ui_area,
                             checkable=True)
        add_header.setChecked(self.settings.include_header)
        add_header.triggered.connect(self.header_check)
        export.addAction(add_header)

        tab.triggered.connect(
            lambda *args, action=semicolon: action.setChecked(False))
        semicolon.triggered.connect(
            lambda *args, action=tab: action.setChecked(False))

        archive = QAction("Include archive if present",
                          self.tool_window.ui_area,
                          checkable=True)
        archive.triggered.connect(
            lambda checked: setattr(self.settings, "archive", checked))
        archive.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        archive.setChecked(self.settings.archive)
        export.addAction(archive)

        unit = menu.addMenu("&Units")

        energy = unit.addMenu("energy")
        hartree = QAction("Hartree", self.tool_window.ui_area, checkable=True)
        hartree.setChecked(self.settings.energy == "Hartree")
        kcal = QAction("kcal/mol", self.tool_window.ui_area, checkable=True)
        kcal.setChecked(self.settings.energy == "kcal/mol")
        kjoule = QAction("kJ/mol", self.tool_window.ui_area, checkable=True)
        kjoule.setChecked(self.settings.energy == "kJ/mol")
        energy.addAction(hartree)
        energy.addAction(kcal)
        energy.addAction(kjoule)

        hartree.triggered.connect(
            lambda *args, val="Hartree": setattr(self.settings, "energy", val))
        hartree.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        hartree.triggered.connect(
            lambda *args, action=kcal: action.setChecked(False))
        hartree.triggered.connect(
            lambda *args, action=kjoule: action.setChecked(False))

        kcal.triggered.connect(lambda *args, val="kcal/mol": setattr(
            self.settings, "energy", val))
        kcal.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        kcal.triggered.connect(
            lambda *args, action=hartree: action.setChecked(False))
        kcal.triggered.connect(
            lambda *args, action=kjoule: action.setChecked(False))

        kjoule.triggered.connect(
            lambda *args, val="kJ/mol": setattr(self.settings, "energy", val))
        kjoule.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        kjoule.triggered.connect(
            lambda *args, action=hartree: action.setChecked(False))
        kjoule.triggered.connect(
            lambda *args, action=kcal: action.setChecked(False))

        mass = unit.addMenu("mass")
        kg = QAction("kg", self.tool_window.ui_area, checkable=True)
        kg.setChecked(self.settings.mass == "kg")
        amu = QAction("Da", self.tool_window.ui_area, checkable=True)
        amu.setChecked(self.settings.mass == "Da")
        mass.addAction(kg)
        mass.addAction(amu)

        kg.triggered.connect(
            lambda *args, val="kg": setattr(self.settings, "mass", val))
        kg.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        kg.triggered.connect(
            lambda *args, action=amu: action.setChecked(False))

        amu.triggered.connect(
            lambda *args, val="Da": setattr(self.settings, "mass", val))
        amu.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        amu.triggered.connect(
            lambda *args, action=kg: action.setChecked(False))

        rot_const = unit.addMenu("rotational constants")
        temperature = QAction("K", self.tool_window.ui_area, checkable=True)
        temperature.setChecked(self.settings.rot_const == "K")
        hertz = QAction("GHz", self.tool_window.ui_area, checkable=True)
        hertz.setChecked(self.settings.rot_const == "GHz")
        rot_const.addAction(temperature)
        rot_const.addAction(hertz)

        temperature.triggered.connect(
            lambda *args, val="K": setattr(self.settings, "rot_const", val))
        temperature.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        temperature.triggered.connect(
            lambda *args, action=hertz: action.setChecked(False))

        hertz.triggered.connect(
            lambda *args, val="GHz": setattr(self.settings, "rot_const", val))
        hertz.triggered.connect(
            lambda *args: self.fill_table(self.file_selector.count() - 1))
        hertz.triggered.connect(
            lambda *args, action=temperature: action.setChecked(False))

        menu.setNativeMenuBar(False)
        self._menu = menu
        layout.setMenuBar(menu)
        menu.setVisible(True)

        if len(self.session.filereader_manager.list()) > 0:
            self.fill_table(0)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def header_check(self, state):
        """user has [un]checked the 'include header' option on the menu"""
        if state:
            self.settings.include_header = True
        else:
            self.settings.include_header = False

    def get_csv(self):
        if self.settings.delimiter == "comma":
            delim = ","
        elif self.settings.delimiter == "space":
            delim = " "
        elif self.settings.delimiter == "tab":
            delim = "\t"
        elif self.settings.delimiter == "semicolon":
            delim = ";"

        if self.tabs.currentIndex() == 0:
            if self.settings.include_header:
                s = delim.join(["Data", "Value"])
                s += "\n"
            else:
                s = ""

            for i in range(0, self.table.rowCount()):
                if self.table.isRowHidden(i):
                    continue
                s += delim.join([
                    item.text().replace("<sub>", "").replace("</sub>", "")
                    for item in [
                        self.table.item(i, j) if self.table.item(i, j)
                        is not None else self.table.cellWidget(i, j)
                        for j in range(0, 2)
                    ]
                ])
                s += "\n"

        elif self.tabs.currentIndex() == 1:
            if self.settings.include_header:
                s = delim.join([
                    "Frequency (cm\u207b\u00b9)", "symmetry", "IR intensity",
                    "force constant"
                ])
                s += "\n"
            else:
                s = ""

            for i in range(0, self.freq_table.rowCount()):
                if self.freq_table.isRowHidden(i):
                    continue
                s += delim.join([
                    item.text().replace("<sub>", "").replace("</sub>", "")
                    for item in [
                        self.freq_table.item(i, j) if self.freq_table.
                        item(i, j) is not None else self.freq_table.
                        cellWidget(i, j) for j in range(0, 4)
                    ]
                ])
                s += "\n"

        else:
            if self.settings.include_header:
                s = delim.join(
                    ["Fundamental (cm\u207b\u00b9)", "Δanh", "IR intensity"])
                s += "\n"
            else:
                s = ""

            for i in range(0, self.fundamental_table.rowCount()):
                if self.fundamental_table.isRowHidden(i):
                    continue
                s += delim.join([
                    item.text().replace("<sub>", "").replace("</sub>", "")
                    for item in [
                        self.fundamental_table.item(i, j) if self.
                        fundamental_table.item(i, j) is not None else self.
                        fundamental_table.cellWidget(i, j)
                        for j in range(0, 3)
                    ]
                ])
                s += "\n"

            if self.settings.include_header:
                s += delim.join([
                    "Fundamental (cm\u207b\u00b9)",
                    "Fundamental (cm\u207b\u00b9)",
                    "Combination (cm\u207b\u00b9)", "IR intensity"
                ])
                s += "\n"
            else:
                s += "\n"

            for i in range(0, self.combo_table.rowCount()):
                if self.combo_table.isRowHidden(i):
                    continue
                s += delim.join([
                    item.text().replace("<sub>", "").replace("</sub>", "")
                    for item in [
                        self.combo_table.item(i, j) if self.combo_table.
                        item(i, j) is not None else self.combo_table.
                        cellWidget(i, j) for j in range(0, 4)
                    ]
                ])
                s += "\n"

        return s

    def copy_csv(self):
        app = QApplication.instance()
        clipboard = app.clipboard()
        csv = self.get_csv()
        clipboard.setText(csv)
        self.session.logger.status("copied to clipboard")

    def save_csv(self):
        """save data on current tab to CSV file"""
        filename, _ = QFileDialog.getSaveFileName(filter="CSV Files (*.csv)")
        if filename:
            s = self.get_csv()

            with open(filename, 'w') as f:
                f.write(s.strip())

            self.session.logger.status("saved to %s" % filename)

    def fill_table(self, ndx):
        self.table.setRowCount(0)
        self.freq_table.setRowCount(0)
        self.fundamental_table.setRowCount(0)
        # self.overtone_table.setRowCount(0)
        self.combo_table.setRowCount(0)

        if ndx < 0:
            self.fundamental_table.setVisible(False)
            self.combo_table.setVisible(False)
            return

        fr = self.file_selector.currentData()
        item = QTableWidgetItem()
        item.setData(Qt.DisplayRole, "name")
        val = QTableWidgetItem()
        val.setData(Qt.DisplayRole, fr.name)
        self.table.insertRow(0)
        self.table.setItem(0, 0, item)
        self.table.setItem(0, 1, val)
        for info in fr.other.keys():
            if info == "archive" and not self.settings.archive:
                continue

            if any(
                    isinstance(fr.other[info], obj)
                    for obj in [str, float, int]):
                row = self.table.rowCount()
                self.table.insertRow(row)

                item = QTableWidgetItem()
                info_name = info.replace("_", " ")
                val = fr.other[info]
                if info == "mass":
                    info_name += " (%s)" % self.settings.mass
                    if self.settings.mass == "Da":
                        val /= UNIT.AMU_TO_KG

                elif info == "temperature":
                    info_name += " (K)"

                elif (any(info == s for s in nrg_infos)
                      or info.lower().endswith("energy")
                      or info.startswith("E(")):
                    if self.settings.energy == "Hartree":
                        info_name += " (E<sub>h</sub>)"
                    else:
                        info_name += " (%s)" % self.settings.energy
                    info_name = info_name.replace("orrelation", "orr.")
                    info_name = info_name.replace("Same-spin", "SS")
                    info_name = info_name.replace("Opposite-spin", "OS")
                    if self.settings.energy == "kcal/mol":
                        val *= UNIT.HART_TO_KCAL
                    elif self.settings.energy == "kJ/mol":
                        val *= 4.184 * UNIT.HART_TO_KCAL

                    val = "%.6f" % val

                elif info.startswith("optical rotation"):
                    info_name += " (°)"

                elif any(info == x for x in pg_infos):
                    info_name = info.replace("_", " ")
                    if re.search("\d", val):
                        val = re.sub(r"(\d+)", r"<sub>\1</sub>", val)
                    # gaussian uses * for infinity
                    val = val.replace("*", "<sub>∞</sub>")
                    # psi4 uses _inf_
                    val = val.replace("_inf_", "<sub>∞</sub>")
                    if any(val.endswith(char) for char in "vhdsiVHDSI"):
                        val = val[:-1] + "<sub>" + val[-1].lower() + "</sub>"

                if "<sub>" in info_name:
                    self.table.setCellWidget(row, 0, QLabel(info_name))
                else:
                    item = QTableWidgetItem()
                    item.setData(Qt.DisplayRole, info_name)
                    self.table.setItem(row, 0, item)

                value = QTableWidgetItem()
                val = str(val)
                if "<sub>" in val:
                    self.table.setCellWidget(row, 1, QLabel(val))
                else:
                    value.setData(Qt.DisplayRole, val)
                    self.table.setItem(row, 1, value)

            elif isinstance(fr.other[info], Theory):
                theory = fr.other[info]
                if theory.method is not None:
                    row = self.table.rowCount()
                    self.table.insertRow(row)

                    item = QTableWidgetItem()
                    item.setData(Qt.DisplayRole, "method")
                    self.table.setItem(row, 0, item)

                    value = QTableWidgetItem()
                    value.setData(Qt.DisplayRole, theory.method.name)
                    self.table.setItem(row, 1, value)

                if theory.basis is not None:
                    if theory.basis.basis:
                        for basis in theory.basis.basis:
                            row = self.table.rowCount()
                            self.table.insertRow(row)

                            item = QTableWidgetItem()
                            if not basis.elements:
                                item.setData(Qt.DisplayRole, "basis set")
                            else:
                                item.setData(
                                    Qt.DisplayRole,
                                    "basis for %s" % ", ".join(basis.elements))
                            self.table.setItem(row, 0, item)

                            value = QTableWidgetItem()
                            value.setData(Qt.DisplayRole, basis.name)
                            self.table.setItem(row, 1, value)

                    if theory.basis.ecp:
                        for ecp in theory.basis.ecp:
                            row = self.table.rowCount()
                            self.table.insertRow(row)

                            item = QTableWidgetItem()
                            if ecp.elements is None:
                                item.setData(Qt.DisplayRole, "ECP")
                            else:
                                item.setData(Qt.DisplayRole,
                                             "ECP %s" % " ".join(ecp.elements))
                            self.table.setItem(row, 0, item)

                            value = QTableWidgetItem()
                            value.setData(Qt.DisplayRole, ecp.name)
                            self.table.setItem(row, 1, value)

            elif (hasattr(fr.other[info], "__iter__")
                  and all(isinstance(x, float) for x in fr.other[info])
                  and len(fr.other[info]) > 1):
                row = self.table.rowCount()
                self.table.insertRow(row)

                item = QTableWidgetItem()
                info_name = info.replace("_", " ")
                vals = fr.other[info]
                if "rotational_temperature" in info:
                    info_name = info_name.replace(
                        "temperature",
                        "constants (%s)" % self.settings.rot_const)
                    if self.settings.rot_const == "GHz":
                        vals = [
                            x * PHYSICAL.KB / (PHYSICAL.PLANCK * (10**9))
                            for x in vals
                        ]

                item.setData(Qt.DisplayRole, info_name)
                self.table.setItem(row, 0, item)

                value = QTableWidgetItem()
                value.setData(Qt.DisplayRole,
                              ", ".join(["%.4f" % x for x in vals]))
                self.table.setItem(row, 1, value)

        if "frequency" in fr.other:
            self.tabs.setTabEnabled(1, True)
            freq_data = fr.other['frequency'].data

            for i, mode in enumerate(freq_data):
                row = self.freq_table.rowCount()
                self.freq_table.insertRow(row)

                freq = FreqTableWidgetItem()
                freq.setData(
                    Qt.DisplayRole, "%.2f%s" %
                    (abs(mode.frequency), "i" if mode.frequency < 0 else ""))
                freq.setData(Qt.UserRole, i)
                self.freq_table.setItem(row, 0, freq)

                if mode.symmetry:
                    text = mode.symmetry
                    if re.search("\d", text):
                        text = re.sub(r"(\d+)", r"<sub>\1</sub>", text)
                    if text.startswith("SG"):
                        text = "Σ" + text[2:]
                    elif text.startswith("PI"):
                        text = "Π" + text[2:]
                    elif text.startswith("DLT"):
                        text = "Δ" + text[3:]
                    if any(text.endswith(char) for char in "vhdugVHDUG"):
                        text = text[:-1] + "<sub>" + text[-1].lower(
                        ) + "</sub>"

                    label = QLabel(text)
                    label.setAlignment(Qt.AlignCenter)
                    self.freq_table.setCellWidget(row, 1, label)

                intensity = QTableWidgetItem()
                if mode.intensity is not None:
                    intensity.setData(Qt.DisplayRole, round(mode.intensity, 2))
                self.freq_table.setItem(row, 2, intensity)

                forcek = QTableWidgetItem()
                if mode.forcek is not None:
                    forcek.setData(Qt.DisplayRole, round(mode.forcek, 2))
                self.freq_table.setItem(row, 3, forcek)

            if fr.other["frequency"].anharm_data:
                self.fundamental_table.setVisible(True)
                self.combo_table.setVisible(True)

                freq = fr.other["frequency"]
                self.tabs.setTabEnabled(2, True)
                anharm_data = sorted(
                    freq.anharm_data,
                    key=lambda x: x.harmonic_frequency,
                )

                for i, mode in enumerate(anharm_data):
                    row = self.fundamental_table.rowCount()
                    self.fundamental_table.insertRow(row)

                    fund = FreqTableWidgetItem()
                    fund.setData(
                        Qt.DisplayRole,
                        "%.2f%s" % (abs(mode.frequency),
                                    "i" if mode.frequency < 0 else ""))
                    fund.setData(Qt.UserRole, i)
                    self.fundamental_table.setItem(row, 0, fund)

                    delta_anh = QTableWidgetItem()
                    delta_anh.setData(Qt.DisplayRole, round(mode.delta_anh, 2))
                    self.fundamental_table.setItem(row, 1, delta_anh)

                    intensity = QTableWidgetItem()
                    if mode.intensity is not None:
                        intensity.setData(Qt.DisplayRole,
                                          round(mode.intensity, 2))
                    self.fundamental_table.setItem(row, 2, intensity)

                    for overtone in mode.overtones:
                        row = self.combo_table.rowCount()
                        self.combo_table.insertRow(row)

                        fund = FreqTableWidgetItem()
                        fund.setData(
                            Qt.DisplayRole,
                            "%.2f%s" % (abs(mode.frequency),
                                        "i" if mode.frequency < 0 else ""))
                        fund.setData(Qt.UserRole, i)
                        self.combo_table.setItem(row, 0, fund)

                        fund = FreqTableWidgetItem()
                        fund.setData(Qt.UserRole, i)
                        self.combo_table.setItem(row, 1, fund)

                        ot = FreqTableWidgetItem()
                        ot.setData(
                            Qt.DisplayRole,
                            "%.2f%s" % (abs(overtone.frequency),
                                        "i" if overtone.frequency < 0 else ""))
                        ot.setData(Qt.UserRole, i)
                        self.combo_table.setItem(row, 2, ot)

                        intensity = QTableWidgetItem()
                        if overtone.intensity is not None:
                            intensity.setData(Qt.DisplayRole,
                                              round(overtone.intensity, 2))
                        self.combo_table.setItem(row, 3, intensity)

                    for key in mode.combinations:
                        for combination in mode.combinations[key]:
                            row = self.combo_table.rowCount()
                            self.combo_table.insertRow(row)

                            fund = FreqTableWidgetItem()
                            fund.setData(
                                Qt.DisplayRole,
                                "%.2f%s" % (abs(mode.frequency),
                                            "i" if mode.frequency < 0 else ""))
                            fund.setData(Qt.UserRole, i)
                            self.combo_table.setItem(row, 0, fund)

                            other_freq = freq.anharm_data[key].frequency
                            fund = FreqTableWidgetItem()
                            fund.setData(
                                Qt.DisplayRole,
                                "%.2f%s" % (abs(other_freq),
                                            "i" if other_freq < 0 else ""))
                            fund.setData(Qt.UserRole,
                                         i + len(freq.anharm_data) * key)
                            self.combo_table.setItem(row, 1, fund)

                            combo = FreqTableWidgetItem()
                            combo.setData(
                                Qt.DisplayRole, "%.2f%s" %
                                (abs(combination.frequency),
                                 "i" if combination.frequency < 0 else ""))
                            combo.setData(Qt.UserRole, i)
                            self.combo_table.setItem(row, 2, combo)

                            intensity = QTableWidgetItem()
                            if combination.intensity is not None:
                                intensity.setData(
                                    Qt.DisplayRole,
                                    round(combination.intensity, 2))
                            self.combo_table.setItem(row, 3, intensity)

            else:
                self.fundamental_table.setVisible(False)
                self.combo_table.setVisible(False)
                self.tabs.setTabEnabled(2, False)

        else:
            self.fundamental_table.setVisible(False)
            self.combo_table.setVisible(False)

            self.tabs.setTabEnabled(1, False)
            self.tabs.setTabEnabled(2, False)

        self.table.resizeColumnToContents(0)
        self.table.resizeColumnToContents(1)

        self.freq_table.resizeColumnToContents(0)
        self.freq_table.resizeColumnToContents(1)
        self.freq_table.resizeColumnToContents(2)

        self.apply_filter()

    def apply_filter(self, text=None):
        if text is None:
            text = self.filter.text()

        if text:
            text = text.replace("(", "\(")
            text = text.replace(")", "\)")
            m = QRegularExpression(text)
            m.setPatternOptions(QRegularExpression.CaseInsensitiveOption)
            if m.isValid():
                m.optimize()
                filter = lambda row_num: m.match(
                    self.table.item(row_num, 0).text()
                    if self.table.item(row_num, 0) is not None else self.table.
                    cellWidget(row_num, 0).text().replace("<sub>", "").replace(
                        "</sub>", "")).hasMatch()
            else:
                return

        else:
            filter = lambda row: True

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

    def delete(self):
        self.file_selector.deleteLater()

        return super().delete()

    def close(self):
        self.file_selector.deleteLater()

        return super().close()
Пример #12
0
class JobQueue(ToolInstance):
    SESSION_ENDURING = False
    SESSION_SAVE = False

    NAME_COL = 0
    STATUS_COL = 1
    SERVER_COL = 2
    CHANGE_PRIORITY = 3
    KILL_COL = 4
    DEL_COL = 5
    BROWSE_COL = 6

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

        self.display_name = "Job Queue"

        self.tool_window = MainToolWindow(self)

        if not self.session.seqcrow_job_manager.initialized:
            self.session.seqcrow_job_manager.init_queue()

        self._build_ui()

        self._job_queued = self.session.seqcrow_job_manager.triggers.add_handler(
            JOB_QUEUED, lambda *args: self.fill_tree(*args))
        self._job_started = self.session.seqcrow_job_manager.triggers.add_handler(
            JOB_STARTED, lambda *args: self.fill_tree(*args))
        self._job_finished = self.session.seqcrow_job_manager.triggers.add_handler(
            JOB_FINISHED, lambda *args: self.fill_tree(*args))

        self.fill_tree()

    def _build_ui(self):
        layout = QGridLayout()

        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self.tree = QTreeWidget()
        self.tree.setSelectionMode(QTreeWidget.ExtendedSelection)
        self.tree.setHeaderLabels([
            "name", "status", "server", "prioritize", "kill", "delete",
            "browse"
        ])
        self.tree.setUniformRowHeights(True)

        self.tree.setColumnWidth(0, 150)
        layout.addWidget(self.tree, 0, 0, 6, 1, Qt.AlignTop)

        row = 0

        pause_button = QPushButton(
            "pause new jobs"
            if not self.session.seqcrow_job_manager.paused else "resume jobs")
        pause_button.setCheckable(True)
        pause_button.clicked.connect(lambda check: pause_button.setText(
            "pause new jobs" if not check else "resume jobs"))
        pause_button.setChecked(self.session.seqcrow_job_manager.paused)
        pause_button.clicked.connect(self.pause_queue)
        layout.addWidget(pause_button, row, 1, 1, 1, Qt.AlignTop)

        row += 1

        open_button = QPushButton("open structure")
        open_button.clicked.connect(self.open_jobs)
        layout.addWidget(open_button, row, 1, 1, 1, Qt.AlignTop)

        row += 1

        log_button = QPushButton("log")
        log_button.clicked.connect(self.open_log)
        layout.addWidget(log_button, row, 1, 1, 1, Qt.AlignTop)

        row += 1

        output_button = QPushButton("raw output")
        output_button.clicked.connect(self.open_output)
        layout.addWidget(output_button, row, 1, 1, 1, Qt.AlignTop)

        row += 1

        refresh_button = QToolButton()
        refresh_button.setSizePolicy(QSizePolicy.Fixed,
                                     QSizePolicy.MinimumExpanding)
        refresh_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        refresh_button.setIcon(
            QIcon(refresh_button.style().standardIcon(
                QStyle.SP_BrowserReload)))
        refresh_button.setText('check jobs')
        refresh_button.clicked.connect(
            lambda *args: self.session.seqcrow_job_manager.triggers.
            activate_trigger(JOB_QUEUED, "refresh"))
        layout.addWidget(refresh_button, row, 1, 1, 1, Qt.AlignTop)

        row += 1

        for i in range(0, row - 1):
            layout.setRowStretch(i, 0)

        layout.setRowStretch(row - 1, 1)
        layout.setColumnStretch(0, 1)
        layout.setColumnStretch(1, 0)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def fill_tree(self, trigger_name=None, trigger_job=None):
        item_stack = [self.tree.invisibleRootItem()]

        self.tree.clear()

        jobs = self.session.seqcrow_job_manager.jobs

        for job in jobs:
            name = job.name
            parent = item_stack[0]
            item = QTreeWidgetItem(parent)
            item_stack.append(item)

            item.setData(self.NAME_COL, Qt.DisplayRole, job)
            item.setText(self.NAME_COL, name)

            if isinstance(job, LocalJob):
                if job.killed:
                    item.setText(self.STATUS_COL, "killed")

                    del_job_widget = QWidget()
                    del_job_layout = QGridLayout(del_job_widget)
                    del_job = QPushButton()
                    del_job.clicked.connect(
                        lambda *args, job=job: self.remove_job(job))
                    del_job.setIcon(
                        QIcon(del_job_widget.style().standardIcon(
                            QStyle.SP_DialogDiscardButton)))
                    del_job.setFlat(True)
                    del_job_layout.addWidget(del_job, 0, 0, 1, 1,
                                             Qt.AlignHCenter)
                    del_job_layout.setColumnStretch(0, 1)
                    del_job_layout.setContentsMargins(0, 0, 0, 0)
                    self.tree.setItemWidget(item, self.DEL_COL, del_job_widget)

                elif job.isRunning():
                    if job in self.session.seqcrow_job_manager.unknown_status_jobs:
                        unk_widget = QWidget()
                        unk_layout = QGridLayout(unk_widget)
                        unk = QPushButton()
                        unk.setIcon(
                            QIcon(unk_widget.style().standardIcon(
                                QStyle.SP_MessageBoxQuestion)))
                        unk.setFlat(True)
                        unk.clicked.connect(lambda *args, job=job: self.
                                            show_ask_if_running(job))
                        unk_layout.addWidget(unk, 0, 0, 1, 1, Qt.AlignHCenter)
                        unk_layout.setColumnStretch(0, 1)
                        unk_layout.setContentsMargins(0, 0, 0, 0)
                        self.tree.setItemWidget(item, self.STATUS_COL,
                                                unk_widget)

                    else:
                        item.setText(self.STATUS_COL, "running")

                        kill_widget = QWidget()
                        kill_layout = QGridLayout(kill_widget)
                        kill = QPushButton()
                        kill.setIcon(
                            QIcon(kill_widget.style().standardIcon(
                                QStyle.SP_DialogCancelButton)))
                        kill.setFlat(True)
                        kill.clicked.connect(lambda *args, job=job: job.kill())
                        kill.clicked.connect(
                            lambda *args, session=self.session: session.
                            seqcrow_job_manager.triggers.activate_trigger(
                                JOB_QUEUED, "resume"))
                        kill_layout.addWidget(kill, 0, 0, 1, 1, Qt.AlignLeft)
                        kill_layout.setColumnStretch(0, 0)
                        kill_layout.setContentsMargins(0, 0, 0, 0)
                        self.tree.setItemWidget(item, self.KILL_COL,
                                                kill_widget)

                elif job.isFinished():
                    if not job.error:
                        item.setText(self.STATUS_COL, "finished")
                    else:
                        error_widget = QWidget()
                        error_layout = QGridLayout(error_widget)
                        error = QPushButton()
                        error.setIcon(
                            QIcon(error_widget.style().standardIcon(
                                QStyle.SP_MessageBoxWarning)))
                        error.setFlat(True)
                        error.setToolTip(
                            "job did not finish without errors or output file cannot be found"
                        )
                        error_layout.addWidget(error, 0, 0, 1, 1,
                                               Qt.AlignHCenter)
                        error_layout.setColumnStretch(0, 1)
                        error_layout.setContentsMargins(0, 0, 0, 0)
                        self.tree.setItemWidget(item, self.STATUS_COL,
                                                error_widget)

                    del_job_widget = QWidget()
                    del_job_layout = QGridLayout(del_job_widget)
                    del_job = QPushButton()
                    del_job.clicked.connect(
                        lambda *args, job=job: self.remove_job(job))
                    del_job.setIcon(
                        QIcon(del_job_widget.style().standardIcon(
                            QStyle.SP_DialogDiscardButton)))
                    del_job.setFlat(True)
                    del_job_layout.addWidget(del_job, 0, 0, 1, 1,
                                             Qt.AlignHCenter)
                    del_job_layout.setColumnStretch(0, 1)
                    del_job_layout.setContentsMargins(0, 0, 0, 0)
                    self.tree.setItemWidget(item, self.DEL_COL, del_job_widget)

                else:
                    item.setText(self.STATUS_COL, "queued")

                    priority_widget = QWidget()
                    priority_layout = QGridLayout(priority_widget)
                    inc_priority = QPushButton()
                    inc_priority.setIcon(
                        QIcon(priority_widget.style().standardIcon(
                            QStyle.SP_ArrowUp)))
                    inc_priority.setFlat(True)
                    inc_priority.clicked.connect(
                        lambda *args, job=job: self.session.seqcrow_job_manager
                        .increase_priotity(job))
                    priority_layout.addWidget(inc_priority, 0, 0, 1, 1,
                                              Qt.AlignRight)
                    dec_priority = QPushButton()
                    dec_priority.setIcon(
                        QIcon(priority_widget.style().standardIcon(
                            QStyle.SP_ArrowDown)))
                    dec_priority.setFlat(True)
                    dec_priority.clicked.connect(
                        lambda *args, job=job: self.session.seqcrow_job_manager
                        .decrease_priotity(job))
                    priority_layout.addWidget(dec_priority, 0, 1, 1, 1,
                                              Qt.AlignLeft)
                    priority_layout.setColumnStretch(0, 1)
                    priority_layout.setColumnStretch(1, 1)
                    priority_layout.setContentsMargins(0, 0, 0, 0)
                    self.tree.setItemWidget(item, self.CHANGE_PRIORITY,
                                            priority_widget)

                    kill_widget = QWidget()
                    kill_layout = QGridLayout(kill_widget)
                    kill = QPushButton()
                    kill.setIcon(
                        QIcon(kill_widget.style().standardIcon(
                            QStyle.SP_DialogCancelButton)))
                    kill.setFlat(True)
                    kill.clicked.connect(lambda *args, job=job: job.kill())
                    kill.clicked.connect(
                        lambda *args, session=self.session: session.
                        seqcrow_job_manager.triggers.activate_trigger(
                            JOB_QUEUED, "resume"))
                    kill_layout.addWidget(kill, 0, 0, 1, 1, Qt.AlignLeft)
                    kill_layout.setColumnStretch(0, 0)
                    kill_layout.setContentsMargins(0, 0, 0, 0)
                    self.tree.setItemWidget(item, self.KILL_COL, kill_widget)

                item.setText(self.SERVER_COL, "local")

                if job.scratch_dir and os.path.exists(job.scratch_dir):
                    browse_widget = QWidget()
                    browse_layout = QGridLayout(browse_widget)
                    browse = QPushButton()
                    browse.clicked.connect(
                        lambda *args, job=job: self.browse_local(job))
                    browse.setIcon(
                        QIcon(browse_widget.style().standardIcon(
                            QStyle.SP_DirOpenIcon)))
                    browse.setFlat(True)
                    browse_layout.addWidget(browse, 0, 0, 1, 1, Qt.AlignLeft)
                    browse_layout.setColumnStretch(0, 1)
                    browse_layout.setContentsMargins(0, 0, 0, 0)
                    self.tree.setItemWidget(item, self.BROWSE_COL,
                                            browse_widget)

            self.tree.expandItem(item)

        self.tree.resizeColumnToContents(self.STATUS_COL)
        self.tree.resizeColumnToContents(self.SERVER_COL)
        self.tree.resizeColumnToContents(self.CHANGE_PRIORITY)
        self.tree.resizeColumnToContents(self.KILL_COL)
        self.tree.resizeColumnToContents(self.DEL_COL)
        self.tree.resizeColumnToContents(self.BROWSE_COL)

    def pause_queue(self):
        self.session.seqcrow_job_manager.paused = not self.session.seqcrow_job_manager.paused
        self.session.seqcrow_job_manager.triggers.activate_trigger(
            JOB_QUEUED, "pauseunpause")

    def resume_queue(self):
        self.session.seqcrow_job_manager.paused = False
        self.session.seqcrow_job_manager.triggers.activate_trigger(
            JOB_QUEUED, "resume")

    def open_jobs(self):
        jobs = self.session.seqcrow_job_manager.jobs
        ndxs = list(set([item.row() for item in self.tree.selectedIndexes()]))
        for ndx in ndxs:
            job = jobs[ndx]
            if hasattr(job, "output_name"
                       ) and job.output_name and job.format_name in read_types:
                run(job.session,
                    "open \"%s\" coordsets true" % job.output_name,
                    log=False)
            elif hasattr(job, "output_name") and job.output_name:
                run(job.session, "open \"%s\"" % job.output_name, log=False)

    def open_log(self):
        jobs = self.session.seqcrow_job_manager.jobs
        ndxs = list(set([item.row() for item in self.tree.selectedIndexes()]))
        for ndx in ndxs:
            job = jobs[ndx]
            if hasattr(job, "scratch_dir") and os.path.exists(job.scratch_dir):
                self.tool_window.create_child_window("%s log" % job.name,
                                                     window_class=JobLog,
                                                     scr_dir=job.scratch_dir)

    def open_output(self):
        jobs = self.session.seqcrow_job_manager.jobs
        ndxs = list(set([item.row() for item in self.tree.selectedIndexes()]))
        for ndx in ndxs:
            job = jobs[ndx]
            if hasattr(job, "output_name"):
                self.tool_window.create_child_window("%s log" % job.name,
                                                     window_class=JobOutput,
                                                     file=job.output_name)

    def kill_running(self):
        jobs = self.session.seqcrow_job_manager.jobs
        ndxs = list(set([item.row() for item in self.tree.selectedIndexes()]))
        for ndx in ndxs:
            job = jobs[ndx]
            if not job.isFinished():
                job.kill()
                job.wait()

        self.session.seqcrow_job_manager.triggers.activate_trigger(
            JOB_QUEUED, "kill")

    def browse_local(self, job):
        #for some reason, this doesn't use the native file browser if i just do QFileDialog
        filename, _ = QFileDialog.getOpenFileName(
            directory=os.path.abspath(job.scratch_dir))

        if filename:
            run(self.session, "open \"%s\"" % filename)

    def remove_job(self, job):
        if isinstance(job, LocalJob):
            self.tree.takeTopLevelItem(
                self.session.seqcrow_job_manager.jobs.index(job))
            self.session.seqcrow_job_manager.remove_local_job(job)
            if hasattr(job, "scratch_dir") and os.path.exists(job.scratch_dir):
                yes = QMessageBox.question(
                    self.tool_window.ui_area,
                    "Remove local files?",
                    "%s has been removed from the queue.\n" % (job.name) + \
                    "Would you also like to move '%s' to the trash bin?" % job.scratch_dir,
                    QMessageBox.Yes | QMessageBox.No
                )

                if yes == QMessageBox.Yes:
                    send2trash(job.scratch_dir)

    def show_ask_if_running(self, job):
        if isinstance(job, LocalJob):
            yes = QMessageBox.question(self.tool_window.ui_area, \
                                       "Job status unknown", \
                                       "%s was running the last time ChimeraX was closed.\n" % (str(job)) + \
                                       "If the job is still running, it might compete with other jobs for you computer's resources, " + \
                                       "which could cause any running job to error out.\n\n" + \
                                       "Has this job finished running?", \
                                       QMessageBox.Yes | QMessageBox.No)

            if yes == QMessageBox.Yes:
                self.session.seqcrow_job_manager.unknown_status_jobs.remove(
                    job)
                job.isRunning = lambda *args, **kwargs: False
                job.isFinished = lambda *args, **kwargs: True

                self.session.seqcrow_job_manager.triggers.activate_trigger(
                    JOB_FINISHED, job)

    def delete(self):
        """overload delete"""
        self.session.seqcrow_job_manager.triggers.remove_handler(
            self._job_queued)
        self.session.seqcrow_job_manager.triggers.remove_handler(
            self._job_started)
        self.session.seqcrow_job_manager.triggers.remove_handler(
            self._job_finished)
        super().delete()

    def close(self):
        """overload close"""
        self.session.seqcrow_job_manager.triggers.remove_handler(
            self._job_queued)
        self.session.seqcrow_job_manager.triggers.remove_handler(
            self._job_started)
        self.session.seqcrow_job_manager.triggers.remove_handler(
            self._job_finished)
        super().close()
Пример #13
0
class EditStructure(ToolInstance):

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

    def __init__(self, session, name):       
        super().__init__(session, name)
        
        self.settings = _EditStructureSettings(session, "Structure Modification")
        
        self.tool_window = MainToolWindow(self)        

        self.close_previous_bool = self.settings.modify

        self._build_ui()

    def _build_ui(self):
        layout = QGridLayout()
        
        self.alchemy_tabs = QTabWidget()
        
        #substitute
        substitute_tab = QWidget()
        substitute_layout = QGridLayout(substitute_tab) 

        sublabel = QLabel("substituent name:")
        substitute_layout.addWidget(sublabel, 0, 0, Qt.AlignVCenter)
        
        self.subname = QLineEdit()
        # self.subname.setText("Et")
        sub_completer = NameCompleter(Substituent.list(), self.subname)
        self.subname.setCompleter(sub_completer)
        self.subname.setToolTip("name of substituent in the AaronTools library or your personal library\nseparate names with commas and uncheck 'modify selected structure' to create several structures")
        substitute_layout.addWidget(self.subname, 0, 1, Qt.AlignVCenter)
        
        open_sub_lib = QPushButton("from library...")
        open_sub_lib.clicked.connect(self.open_sub_selector)
        substitute_layout.addWidget(open_sub_lib, 0, 2, Qt.AlignTop)        
        
        substitute_layout.addWidget(QLabel("modify selected structure:"), 1, 0, 1, 1, Qt.AlignVCenter)
        
        self.close_previous_sub = QCheckBox()
        self.close_previous_sub.setToolTip("checked: selected structure will be modified\nunchecked: new model will be created for the modified structure")
        self.close_previous_sub.setChecked(self.settings.modify)
        self.close_previous_sub.stateChanged.connect(self.close_previous_change)
        substitute_layout.addWidget(self.close_previous_sub, 1, 1, 1, 2, Qt.AlignTop)    
        
        substitute_layout.addWidget(QLabel("relax substituent:"), 2, 0, 1, 1, Qt.AlignVCenter)
        
        self.minimize = QCheckBox()
        self.minimize.setToolTip("spin the added substituents to try to minimize the LJ potential energy")
        self.minimize.setChecked(self.settings.minimize)
        substitute_layout.addWidget(self.minimize, 2, 1, 1, 1, Qt.AlignTop)
        
        substitute_layout.addWidget(QLabel("guess previous substituent:"), 3, 0, 1, 1, Qt.AlignVCenter)
        
        self.guess_old = QCheckBox()
        self.guess_old.setToolTip("checked: leave the longest connected fragment in the residue\nunchecked: previous substituent must be selected")
        self.guess_old.setChecked(self.settings.guess)
        self.guess_old.stateChanged.connect(lambda state, settings=self.settings: settings.__setattr__("guess", True if state == Qt.Checked else False))
        substitute_layout.addWidget(self.guess_old, 3, 1, 1, 2, Qt.AlignTop)
        
        substitute_layout.addWidget(QLabel("new residue:"), 5, 0, 1, 1, Qt.AlignVCenter)

        self.new_residue = QCheckBox()
        self.new_residue.setToolTip("put the new substituent in its own residue instead\nof adding it to the residue of the old substituent")
        self.new_residue.setChecked(self.settings.new_residue)
        self.new_residue.stateChanged.connect(lambda state, settings=self.settings: settings.__setattr__("new_residue", True if state == Qt.Checked else False))
        substitute_layout.addWidget(self.new_residue, 5, 1, 1, 2, Qt.AlignTop)
        
        substitute_layout.addWidget(QLabel("use distance names:"), 4, 0, 1, 1, Qt.AlignVCenter)
        
        self.use_greek = QCheckBox()
        self.use_greek.setChecked(self.settings.use_greek)
        self.use_greek.setToolTip("indicate distance from point of attachment with atom name")
        substitute_layout.addWidget(self.use_greek, 4, 1, 1, 1, Qt.AlignTop)

        substitute_layout.addWidget(QLabel("change residue name:"), 6, 0, 1, 1, Qt.AlignVCenter)
        
        self.new_sub_name = QLineEdit()
        self.new_sub_name.setToolTip("change name of modified residues")
        self.new_sub_name.setPlaceholderText("leave blank to keep current")
        substitute_layout.addWidget(self.new_sub_name, 6, 1, 1, 2, Qt.AlignTop)

        substitute_button = QPushButton("substitute current selection")
        substitute_button.clicked.connect(self.do_substitute)
        substitute_layout.addWidget(substitute_button, 7, 0, 1, 3, Qt.AlignTop)
        self.substitute_button = substitute_button
        
        substitute_layout.setRowStretch(0, 0)
        substitute_layout.setRowStretch(1, 0)
        substitute_layout.setRowStretch(2, 0)
        substitute_layout.setRowStretch(3, 0)
        substitute_layout.setRowStretch(4, 0)
        substitute_layout.setRowStretch(5, 0)
        substitute_layout.setRowStretch(6, 0)
        substitute_layout.setRowStretch(7, 1)
        
        
        #map ligand
        maplig_tab = QWidget()
        maplig_layout = QGridLayout(maplig_tab)
        
        liglabel = QLabel("ligand name:")
        maplig_layout.addWidget(liglabel, 0, 0, Qt.AlignVCenter)
        
        self.ligname = QLineEdit()
        lig_completer = NameCompleter(Component.list(), self.ligname)
        self.ligname.setCompleter(lig_completer)
        self.ligname.setToolTip("name of ligand in the AaronTools library or your personal library\nseparate names with commas and uncheck 'modify selected structure' to create several structures")
        maplig_layout.addWidget(self.ligname, 0, 1, Qt.AlignVCenter)
        
        open_lig_lib = QPushButton("from library...")
        open_lig_lib.clicked.connect(self.open_lig_selector)
        maplig_layout.addWidget(open_lig_lib, 0, 2, Qt.AlignTop)        
        
        maplig_layout.addWidget(QLabel("modify selected structure:"), 1, 0, 1, 1, Qt.AlignVCenter)
        
        self.close_previous_lig = QCheckBox()
        self.close_previous_lig.setToolTip("checked: selected structure will be modified\nunchecked: new model will be created for the modified structure")
        self.close_previous_lig.setChecked(self.settings.modify)
        self.close_previous_lig.stateChanged.connect(self.close_previous_change)
        maplig_layout.addWidget(self.close_previous_lig, 1, 1, 1, 2, Qt.AlignTop)

        maplig_button = QPushButton("swap ligand with selected coordinating atoms")
        maplig_button.clicked.connect(self.do_maplig)
        maplig_layout.addWidget(maplig_button, 2, 0, 1, 3, Qt.AlignTop)
        self.maplig_button = maplig_button

        start_structure_button = QPushButton("place in:")
        self.lig_model_selector = ModelComboBox(self.session, addNew=True)
        start_structure_button.clicked.connect(self.do_new_lig)
        maplig_layout.addWidget(start_structure_button, 3, 0, 1, 1, Qt.AlignTop)
        maplig_layout.addWidget(self.lig_model_selector, 3, 1, 1, 2, Qt.AlignTop)

        maplig_layout.setRowStretch(0, 0)
        maplig_layout.setRowStretch(1, 0)
        maplig_layout.setRowStretch(2, 0)
        maplig_layout.setRowStretch(3, 1)
        
        
        #close ring
        closering_tab = QWidget()
        closering_layout = QGridLayout(closering_tab)
        
        ringlabel = QLabel("ring name:")
        closering_layout.addWidget(ringlabel, 0, 0, Qt.AlignVCenter)
        
        self.ringname = QLineEdit()
        ring_completer = NameCompleter(Ring.list(), self.ringname)
        self.ringname.setCompleter(ring_completer)
        self.ringname.setToolTip("name of ring in the AaronTools library or your personal library\nseparate names with commas and uncheck 'modify selected structure' to create several structures")
        closering_layout.addWidget(self.ringname, 0, 1, Qt.AlignVCenter)
        
        open_ring_lib = QPushButton("from library...")
        open_ring_lib.clicked.connect(self.open_ring_selector)
        closering_layout.addWidget(open_ring_lib, 0, 2, Qt.AlignTop)        
        
        closering_layout.addWidget(QLabel("modify selected structure:"), 1, 0, 1, 1, Qt.AlignVCenter) 
        
        self.close_previous_ring = QCheckBox()
        self.close_previous_ring.setToolTip("checked: selected structure will be modified\nunchecked: new model will be created for the modified structure")
        self.close_previous_ring.setChecked(self.settings.modify)
        self.close_previous_ring.stateChanged.connect(self.close_previous_change)
        closering_layout.addWidget(self.close_previous_ring, 1, 1, 1, 2, Qt.AlignTop)

        closering_layout.addWidget(QLabel("try multiple:"), 2, 0, 1, 1, Qt.AlignVCenter)

        self.minimize_ring = QCheckBox()
        self.minimize_ring.setToolTip("try to use other versions of this ring in the library to find the one that fits best")
        self.minimize_ring.setChecked(self.settings.minimize_ring)
        closering_layout.addWidget(self.minimize_ring, 2, 1, 1, 2, Qt.AlignTop)

        closering_layout.addWidget(QLabel("new residue name:"), 3, 0, 1, 1, Qt.AlignVCenter)
        
        self.new_ring_name = QLineEdit()
        self.new_ring_name.setToolTip("change name of modified residues")
        self.new_ring_name.setPlaceholderText("leave blank to keep current")
        closering_layout.addWidget(self.new_ring_name, 3, 1, 1, 2, Qt.AlignTop)

        closering_button = QPushButton("put a ring on current selection")
        closering_button.clicked.connect(self.do_fusering)

        closering_layout.addWidget(closering_button, 4, 0, 1, 3, Qt.AlignTop)
        self.closering_button = closering_button

        start_structure_button = QPushButton("place in:")
        self.ring_model_selector = ModelComboBox(self.session, addNew=True)
        start_structure_button.clicked.connect(self.do_new_ring)
        closering_layout.addWidget(start_structure_button, 5, 0, 1, 1, Qt.AlignTop)
        closering_layout.addWidget(self.ring_model_selector, 5, 1, 1, 2, Qt.AlignTop)

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


        #change element
        changeelement_tab = QWidget()
        changeelement_layout = QFormLayout(changeelement_tab)
        
        self.element = ElementButton("C", single_state=True)
        self.element.clicked.connect(self.open_ptable)
        changeelement_layout.addRow("element:", self.element)
        
        self.vsepr = QComboBox()
        self.vsepr.addItems([
            "do not change",                  # 0
            
            "linear (1 bond)",                # 1
            
            "linear (2 bonds)",               # 2 
            "trigonal planar (2 bonds)",      # 3
            "tetrahedral (2 bonds)",          # 4 
            
            "trigonal planar",                # 5
            "tetrahedral (3 bonds)",          # 6
            "T-shaped",                       # 7
            
            "trigonal pyramidal",             # 8
            "tetrahedral",                    # 9
            "sawhorse",                       #10
            "seesaw",                         #11
            "square planar",                  #12
            
            "trigonal bipyramidal",           #13
            "square pyramidal",               #14
            "pentagonal",                     #15
            
            "octahedral",                     #16
            "hexagonal",                      #17
            "trigonal prismatic",             #18
            "pentagonal pyramidal",           #19
            
            "capped octahedral",              #20
            "capped trigonal prismatic",      #21
            "heptagonal",                     #22
            "hexagonal pyramidal",            #23
            "pentagonal bipyramidal",         #24
            
            "biaugmented trigonal prismatic", #25
            "cubic",                          #26
            "elongated trigonal bipyramidal", #27
            "hexagonal bipyramidal",          #28
            "heptagonal pyramidal",           #29
            "octagonal",                      #30
            "square antiprismatic",           #31
            "trigonal dodecahedral",          #32
            
            "capped cube",                    #33
            "capped square antiprismatic",    #34
            "enneagonal",                     #35
            "heptagonal bipyramidal",         #36
            "hula-hoop",                      #37
            "triangular cupola",              #38
            "tridiminished icosahedral",      #39
            "muffin",                         #40
            "octagonal pyramidal",            #41
            "tricapped trigonal prismatic",   #42
        ])
        
        self.vsepr.setCurrentIndex(9)
        
        self.vsepr.insertSeparator(33)
        self.vsepr.insertSeparator(25)
        self.vsepr.insertSeparator(20)
        self.vsepr.insertSeparator(16)
        self.vsepr.insertSeparator(13)
        self.vsepr.insertSeparator(8)
        self.vsepr.insertSeparator(5)
        self.vsepr.insertSeparator(2)
        self.vsepr.insertSeparator(1)
        self.vsepr.insertSeparator(0)
        changeelement_layout.addRow("geometry:", self.vsepr)
        
        self.change_bonds = QCheckBox()
        self.change_bonds.setChecked(self.settings.change_bonds)
        changeelement_layout.addRow("adjust bond lengths:", self.change_bonds)
        
        change_element_button = QPushButton("change selected elements")
        change_element_button.clicked.connect(self.do_change_element)
        changeelement_layout.addRow(change_element_button)
        self.change_element_button = change_element_button

        start_structure_button = QPushButton("place in:")
        self.model_selector = ModelComboBox(self.session, addNew=True)
        start_structure_button.clicked.connect(self.do_new_atom)
        changeelement_layout.addRow(start_structure_button, self.model_selector)
        
        delete_atoms_button = QPushButton("delete selected atoms")
        delete_atoms_button.clicked.connect(self.delete_atoms)
        changeelement_layout.addRow(delete_atoms_button)

        self.alchemy_tabs.addTab(substitute_tab, "substitute")
        self.alchemy_tabs.addTab(maplig_tab, "swap ligand")
        self.alchemy_tabs.addTab(closering_tab, "fuse ring")
        self.alchemy_tabs.addTab(changeelement_tab, "change element")

        layout.addWidget(self.alchemy_tabs)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)
    
    def close_previous_change(self, state):
        if state == Qt.Checked:
            self.settings.modify = True
            for checkbox in [self.close_previous_lig, self.close_previous_sub, self.close_previous_ring]:
                checkbox.setChecked(True)
            self.close_previous_bool = True
        else:
            self.settings.modify = False
            for checkbox in [self.close_previous_lig, self.close_previous_sub, self.close_previous_ring]:
                checkbox.setChecked(False)
            self.close_previous_bool = False
    
    def do_substitute(self):
        subnames = self.subname.text()
        
        new_name = self.new_sub_name.text()

        use_attached = not self.guess_old.isChecked()
        
        minimize = self.minimize.isChecked()
        
        new_residue = self.new_residue.isChecked()

        use_greek = self.use_greek.isChecked()
        
        self.settings.minimize = minimize
        self.settings.use_greek = use_greek

        if len(new_name.strip()) > 0:
            run(
                self.session, 
                "substitute sel substituents %s newName %s guessAttachment %s modify %s minimize %s useRemoteness %s newResidue %s" %
                (
                    subnames,
                    new_name,
                    not use_attached,
                    self.close_previous_bool,
                    minimize,
                    use_greek,
                    new_residue,
                )
            )

        else:
            run(
                self.session,
                "substitute sel substituents %s guessAttachment %s modify %s minimize %s useRemoteness %s newResidue %s" %
                (
                    subnames,
                    not use_attached,
                    self.close_previous_bool,
                    minimize,
                    use_greek,
                    new_residue,
                )
            )

    def open_sub_selector(self):
        self.tool_window.create_child_window("select substituents", window_class=SubstituentSelection, textBox=self.subname)

    def do_maplig(self):
        lignames = self.ligname.text()
        selection = selected_atoms(self.session)
        
        if len(selection) < 1:
            raise RuntimeWarning("nothing selected")
        
        models = {}
        for atom in selection:
            if atom.structure not in models:
                models[atom.structure] = [AtomSpec(atom.atomspec)]
            else:
                models[atom.structure].append(AtomSpec(atom.atomspec))        
        
        first_pass = True
        new_structures = []
        for ligname in lignames.split(','):
            ligname = ligname.strip()
            lig = Component(ligname)
            for model in models:
                if self.close_previous_bool and first_pass:
                    rescol = ResidueCollection(model)
                elif self.close_previous_bool and not first_pass:
                    raise RuntimeError("only the first model can be replaced")
                else:
                    model_copy = model.copy()
                    rescol = ResidueCollection(model_copy)
                    for i, atom in enumerate(model.atoms):
                        rescol.atoms[i].atomspec = atom.atomspec
                        rescol.atoms[i].add_tag(atom.atomspec)
                        rescol.atoms[i].chix_atom = atom

                target = rescol.find(models[model])
                if len(target) % len(lig.key_atoms) == 0:
                    k = 0
                    ligands = []
                    while k != len(target):
                        res_lig = ResidueCollection(lig.copy(), comment=lig.comment)
                        res_lig.parse_comment()
                        res_lig = Component(res_lig, key_atoms = ",".join([str(k + 1) for k in res_lig.other["key_atoms"]]))
                        ligands.append(res_lig)
                        k += len(lig.key_atoms)
                else:
                    raise RuntimeError("number of key atoms no not match: %i now, new ligand has %i" % (len(target), len(lig.key_atoms)))
                
                rescol.map_ligand(ligands, target)

                for center_atom in rescol.center:
                    center_atom.connected = set([])
                    for atom in rescol.atoms:
                        if atom not in rescol.center:
                            if center_atom.is_connected(atom):
                                atom.connected.add(center_atom)
                                center_atom.connected.add(atom)
                
                if self.close_previous_bool:    
                    rescol.update_chix(model)
                else:
                    struc = rescol.get_chimera(self.session)
                    new_structures.append(struc)
            
            first_pass = False
        
        if not self.close_previous_bool:
            self.session.models.add(new_structures)

    def open_lig_selector(self):
        self.tool_window.create_child_window("select ligands", window_class=LigandSelection, textBox=self.ligname)
    
    def do_fusering(self):
        ring_names = self.ringname.text()
        
        new_name = self.new_ring_name.text()

        minimize_ring = self.minimize_ring.isChecked()
        self.settings.minimize_ring = minimize_ring

        if len(new_name.strip()) > 0:
            run(
                self.session,
                "fuseRing sel rings %s newName %s modify %s minimize %s" % (
                    ring_names,
                    new_name,
                    self.close_previous_bool,
                    minimize_ring,
                )
            )

        else:
            run(
                self.session,
                "fuseRing sel rings %s modify %s minimize %s" % (
                    ring_names,
                    self.close_previous_bool,
                    minimize_ring,
                )
            )

    def open_ring_selector(self):
        self.tool_window.create_child_window("select rings", window_class=RingSelection, textBox=self.ringname)

    def open_ptable(self):
        self.tool_window.create_child_window("select element", window_class=_PTable, button=self.element)
    
    def display_help(self):
        """Show the help for this tool in the help viewer."""
        from chimerax.core.commands import run
        run(self.session,
            'open %s' % self.help if self.help is not None else "")
    
    def do_change_element(self):
        element = self.element.text()
        adjust_bonds = self.change_bonds.isChecked()
        self.settings.change_bonds = adjust_bonds
        vsepr = self.vsepr.currentText()
        
        if vsepr == "do not change":
            vsepr = False
        elif vsepr == "linear (1 bond)":
            vsepr = "linear 1"
            goal = 1
        elif vsepr == "linear (2 bonds)":
            vsepr = "linear 2"
            goal = 2
        elif vsepr == "trigonal planar (2 bonds)":
            vsepr = "bent 2 planar"
            goal = 2
        elif vsepr == "tetrahedral (2 bonds)":
            vsepr = "bent 2 tetrahedral"
            goal = 2
        elif vsepr == "trigonal planar":
            goal = 3
        elif vsepr == "tetrahedral (3 bonds)":
            vsepr = "bent 3 tetrahedral"
            goal = 3
        else:
            goal = len(Atom.get_shape(vsepr)) - 1
        
        sel = selected_atoms(self.session)
        models, _ = guessAttachmentTargets(sel, self.session, allow_adjacent=False)
        for model in models:
            conv_res = list(models[model].keys())
            for res in models[model]:
                for target in models[model][res]:
                    for neighbor in target.neighbors:
                        if neighbor.residue not in conv_res:
                            conv_res.append(neighbor.residue)
            
                    for pbg in self.session.models.list(type=PseudobondGroup):
                        for pbond in pbg.pseudobonds:
                            if target in pbond.atoms and all(atom.structure is model for atom in pbond.atoms):
                                other_atom = pbond.other_atom(target)
                                if other_atom.residue not in conv_res:
                                    conv_res.append(other_atom.residue)
            
            rescol = ResidueCollection(model, convert_residues=conv_res)
            for res in models[model]:
                residue = [resi for resi in rescol.residues if resi.chix_residue is res][0]
                
                for target in models[model][res]:
                    targ = rescol.find_exact(AtomSpec(target.atomspec))[0]
                    adjust_hydrogens = vsepr
                    if vsepr is not False:
                        cur_bonds = len(targ.connected)
                        change_Hs = goal - cur_bonds
                        adjust_hydrogens = (change_Hs, vsepr)

                    residue.change_element(targ, 
                                           element, 
                                           adjust_bonds=adjust_bonds, 
                                           adjust_hydrogens=adjust_hydrogens,
                    )
                
                residue.update_chix(res)    
    
    def do_new_ring(self):
        rings = self.ringname.text()
        
        for ring in rings.split(","):
            ring = ring.strip()
            
            rescol = ResidueCollection(Ring(ring))
        
            model = self.ring_model_selector.currentData()
            if model is None:
                chix = rescol.get_chimera(self.session)
                self.session.models.add([chix])
                apply_seqcrow_preset(chix, fallback="Ball-Stick-Endcap")
                self.ring_model_selector.setCurrentIndex(self.ring_model_selector.count()-1)
    
            else:
                res = model.new_residue("new", "a", len(model.residues)+1)
                rescol.residues[0].update_chix(res)
                run(self.session, "select add %s" % " ".join([atom.atomspec for atom in res.atoms]))
    
    def do_new_lig(self):
        ligands = self.ligname.text()
        
        for lig in ligands.split(","):
            lig = lig.strip()
            
            rescol = ResidueCollection(Component(lig))
        
            model = self.lig_model_selector.currentData()
            if model is None:
                chix = rescol.get_chimera(self.session)
                self.session.models.add([chix])
                apply_seqcrow_preset(chix, fallback="Ball-Stick-Endcap")
                self.lig_model_selector.setCurrentIndex(self.lig_model_selector.count()-1)
    
            else:
                res = model.new_residue("new", "a", len(model.residues)+1)
                rescol.residues[0].update_chix(res)
                run(self.session, "select add %s" % " ".join([atom.atomspec for atom in res.atoms]))

    def do_new_atom(self):
        element = self.element.text()
        adjust_bonds = self.change_bonds.isChecked()
        self.settings.change_bonds = adjust_bonds
        vsepr = self.vsepr.currentText()
        
        if vsepr == "do not change":
            vsepr = False
        elif vsepr == "linear (1 bond)":
            vsepr = "linear 1"
        elif vsepr == "linear (2 bonds)":
            vsepr = "linear 2"
        elif vsepr == "trigonal planar (2 bonds)":
            vsepr = "bent 2 planar"
        elif vsepr == "tetrahedral (2 bonds)":
            vsepr = "bent 2 tetrahedral"
        elif vsepr == "tetrahedral (3 bonds)":
            vsepr = "bent 3 tetrahedral"
        
        if vsepr:
            atoms = Atom.get_shape(vsepr)
            atoms[0].element = element
            for atom in atoms[1:]:
                atom.element = "H"
            if adjust_bonds:
                # this works b/c all atoms are 1 angstrom from the center
                # just multiply by the distance we want
                expected_dist = RADII[element] + RADII["H"]
                for atom in atoms[1:]:
                    atom.coords *= expected_dist
            for atom in atoms[1:]:
                atoms[0].connected.add(atom)
                atom.connected.add(atoms[0])
        else:
            atoms = [Atom(element=element, coords=np.zeros(3))]

        rescol = ResidueCollection(atoms, name="new", refresh_connected=False)

        model = self.model_selector.currentData()
        if model is None:
            chix = rescol.get_chimera(self.session)
            self.session.models.add([chix])
            apply_seqcrow_preset(chix, fallback="Ball-Stick-Endcap")
            self.model_selector.setCurrentIndex(self.model_selector.count()-1)

        else:
            res = model.new_residue("new", "a", len(model.residues)+1)
            rescol.residues[0].update_chix(res)
            run(self.session, "select add %s" % " ".join([atom.atomspec for atom in res.atoms]))
    
    def delete_atoms(self, *args):
        atoms = selected_atoms(self.session)
        for atom in atoms:
            atom.delete()
    
    def delete(self):
        self.ring_model_selector.deleteLater()
        self.lig_model_selector.deleteLater()
        self.model_selector.deleteLater()

        return super().delete()    
    
    def close(self):
        self.ring_model_selector.deleteLater()
        self.lig_model_selector.deleteLater()
        self.model_selector.deleteLater()

        return super().close()
Пример #14
0
class Thermochem(ToolInstance):
    """tool for calculating free energy corrections based on frequencies and energies 
    associated with FileReaders
    there are two tabs: absolute and relative
    
    the absolute tab can be used to combine the thermo corrections from one
    FileReader with the energy of another
    
    the relative tab can do the same, but it prints the energies relative to
    those of another FileReader
    multiple molecule groups can be added (i.e. for reactions with multiple
    reactants and products)
    each molecule group can have multiple conformers
    the energies of these conformers are boltzmann weighted, and the boltzmann-weighted
    energy is used to calculate the energy of either the reference group or the 'other' group"""

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

    theory_helper = {
        "Grimme's Quasi-RRHO": "https://doi.org/10.1002/chem.201200497",
        "Truhlar's Quasi-Harmonic": "https://doi.org/10.1021/jp205508z"
    }

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

        self.display_name = "Thermochemistry"

        self.tool_window = MainToolWindow(self)

        self.settings = _ComputeThermoSettings(self.session, name)

        self.nrg_fr = {}
        self.thermo_fr = {}
        self.thermo_co = {}
        self._headers = []
        self._data = []

        self._build_ui()

        self.set_sp()
        self.set_thermo_mdl()

        self.calc_relative_thermo()

    def _build_ui(self):
        #each group has an empty widget at the bottom so they resize the way I want while also having the
        #labels where I want them
        layout = QGridLayout()

        self.tab_widget = QTabWidget()
        layout.addWidget(self.tab_widget)

        #layout for absolute thermo stuff
        absolute_widget = QWidget()
        absolute_layout = QGridLayout(absolute_widget)

        #box for sp
        sp_area_widget = QGroupBox("Single-point")
        sp_layout = QFormLayout(sp_area_widget)

        self.sp_selector = FilereaderComboBox(self.session,
                                              otherItems=['energy'])
        self.sp_selector.currentIndexChanged.connect(self.set_sp)
        sp_layout.addRow(self.sp_selector)

        self.sp_table = QTableWidget()
        self.sp_table.setColumnCount(3)
        self.sp_table.setShowGrid(False)
        self.sp_table.horizontalHeader().hide()
        self.sp_table.verticalHeader().hide()
        self.sp_table.setFrameShape(QTableWidget.NoFrame)
        self.sp_table.setSelectionMode(QTableWidget.NoSelection)
        self.sp_table.insertRow(0)
        sp_layout.addRow(self.sp_table)

        #box for thermo
        therm_area_widget = QGroupBox("Thermal corrections")
        thermo_layout = QFormLayout(therm_area_widget)

        self.thermo_selector = FilereaderComboBox(self.session,
                                                  otherItems=['frequency'])
        self.thermo_selector.currentIndexChanged.connect(self.set_thermo_mdl)
        thermo_layout.addRow(self.thermo_selector)

        self.temperature_line = QDoubleSpinBox()
        self.temperature_line.setMaximum(2**31 - 1)
        self.temperature_line.setValue(298.15)
        self.temperature_line.setSingleStep(10)
        self.temperature_line.setSuffix(" K")
        self.temperature_line.setMinimum(0)
        self.temperature_line.valueChanged.connect(self.set_thermo)
        thermo_layout.addRow("T =", self.temperature_line)

        self.v0_edit = QDoubleSpinBox()
        self.v0_edit.setMaximum(4000)
        self.v0_edit.setValue(self.settings.w0)
        self.v0_edit.setSingleStep(25)
        self.v0_edit.setSuffix(" cm\u207b\u00b9")
        self.v0_edit.valueChanged.connect(self.set_thermo)
        self.v0_edit.setMinimum(0)
        self.v0_edit.setToolTip(
            "frequency parameter for quasi treatments of entropy")
        thermo_layout.addRow("𝜔<sub>0</sub> =", self.v0_edit)

        self.thermo_table = QTableWidget()
        self.thermo_table.setColumnCount(3)
        self.thermo_table.setShowGrid(False)
        self.thermo_table.horizontalHeader().hide()
        self.thermo_table.verticalHeader().hide()
        self.thermo_table.setFrameShape(QTableWidget.NoFrame)
        self.thermo_table.setSelectionMode(QTableWidget.NoSelection)
        thermo_layout.addRow(self.thermo_table)

        # for for total
        sum_area_widget = QGroupBox("Thermochemistry")
        sum_layout = QFormLayout(sum_area_widget)

        self.sum_table = QTableWidget()
        self.sum_table.setColumnCount(3)
        self.sum_table.setShowGrid(False)
        self.sum_table.horizontalHeader().hide()
        self.sum_table.verticalHeader().hide()
        self.sum_table.setFrameShape(QTableWidget.NoFrame)
        self.sum_table.setSelectionMode(QTableWidget.NoSelection)
        sum_layout.addRow(self.sum_table)

        splitter = QSplitter(Qt.Horizontal)
        splitter.setChildrenCollapsible(False)
        splitter.addWidget(sp_area_widget)
        splitter.addWidget(therm_area_widget)
        splitter.addWidget(sum_area_widget)

        absolute_layout.addWidget(splitter)

        self.status = QStatusBar()
        self.status.setSizeGripEnabled(False)
        self.status.setStyleSheet("color: red")
        absolute_layout.addWidget(self.status, 1, 0, 1, 1, Qt.AlignTop)

        self.tab_widget.addTab(absolute_widget, "absolute")

        relative_widget = QWidget()
        relative_layout = QGridLayout(relative_widget)

        size = [self.settings.ref_col_1, self.settings.ref_col_2]
        self.ref_group = ThermoGroup("reference group", self.session,
                                     self.nrg_fr, self.thermo_co, size)
        self.ref_group.changes.connect(self.calc_relative_thermo)
        relative_layout.addWidget(self.ref_group, 0, 0, 1, 3, Qt.AlignTop)

        size = [self.settings.other_col_1, self.settings.other_col_2]
        self.other_group = ThermoGroup("other group", self.session,
                                       self.nrg_fr, self.thermo_co, size)
        self.other_group.changes.connect(self.calc_relative_thermo)
        relative_layout.addWidget(self.other_group, 0, 3, 1, 3, Qt.AlignTop)

        self.relative_temperature = QDoubleSpinBox()
        self.relative_temperature.setMaximum(2**31 - 1)
        self.relative_temperature.setValue(self.settings.rel_temp)
        self.relative_temperature.setSingleStep(10)
        self.relative_temperature.setSuffix(" K")
        self.relative_temperature.setMinimum(0)
        self.relative_temperature.valueChanged.connect(
            self.calc_relative_thermo)
        relative_layout.addWidget(QLabel("T ="), 1, 0, 1, 1,
                                  Qt.AlignRight | Qt.AlignVCenter)
        relative_layout.addWidget(self.relative_temperature, 1, 1, 1, 5,
                                  Qt.AlignLeft | Qt.AlignVCenter)

        self.relative_v0 = QDoubleSpinBox()
        self.relative_v0.setMaximum(2**31 - 1)
        self.relative_v0.setValue(self.settings.w0)
        self.relative_v0.setSingleStep(25)
        self.relative_v0.setSuffix(" cm\u207b\u00b9")
        self.relative_v0.setMinimum(0)
        self.relative_v0.setToolTip(
            "frequency parameter for quasi treatments of entropy")
        self.relative_v0.valueChanged.connect(self.calc_relative_thermo)
        relative_layout.addWidget(QLabel("𝜔<sub>0</sub> ="), 2, 0, 1, 1,
                                  Qt.AlignRight | Qt.AlignVCenter)

        relative_layout.addWidget(self.relative_v0, 2, 1, 1, 5,
                                  Qt.AlignLeft | Qt.AlignVCenter)

        relative_layout.addWidget(
            QLabel("Boltzmann-weighted relative energies in kcal/mol:"), 3, 0,
            1, 6, Qt.AlignVCenter | Qt.AlignLeft)

        self.relative_table = QTextBrowser()
        self.relative_table.setMaximumHeight(
            4 * self.relative_table.fontMetrics().boundingRect("Q").height())
        relative_layout.addWidget(self.relative_table, 4, 0, 1, 6, Qt.AlignTop)

        relative_layout.setRowStretch(0, 1)
        relative_layout.setRowStretch(1, 0)
        relative_layout.setRowStretch(2, 0)
        relative_layout.setRowStretch(3, 0)
        relative_layout.setRowStretch(4, 0)

        self.tab_widget.addTab(relative_widget, "relative")

        #menu stuff
        menu = QMenuBar()

        export = menu.addMenu("&Export")
        copy = QAction("&Copy CSV to clipboard", self.tool_window.ui_area)
        copy.triggered.connect(self.copy_csv)
        shortcut = QKeySequence(Qt.CTRL + Qt.Key_C)
        copy.setShortcut(shortcut)
        export.addAction(copy)
        self.copy = copy

        save = QAction("&Save CSV...", self.tool_window.ui_area)
        save.triggered.connect(self.save_csv)
        #this shortcut interferes with main window's save shortcut
        #I've tried different shortcut contexts to no avail
        #thanks Qt...
        #shortcut = QKeySequence(Qt.CTRL + Qt.Key_S)
        #save.setShortcut(shortcut)
        #save.setShortcutContext(Qt.WidgetShortcut)
        export.addAction(save)

        delimiter = export.addMenu("Delimiter")

        comma = QAction("comma", self.tool_window.ui_area, checkable=True)
        comma.setChecked(self.settings.delimiter == "comma")
        comma.triggered.connect(lambda *args, delim="comma": self.settings.
                                __setattr__("delimiter", delim))
        delimiter.addAction(comma)

        tab = QAction("tab", self.tool_window.ui_area, checkable=True)
        tab.setChecked(self.settings.delimiter == "tab")
        tab.triggered.connect(lambda *args, delim="tab": self.settings.
                              __setattr__("delimiter", delim))
        delimiter.addAction(tab)

        space = QAction("space", self.tool_window.ui_area, checkable=True)
        space.setChecked(self.settings.delimiter == "space")
        space.triggered.connect(lambda *args, delim="space": self.settings.
                                __setattr__("delimiter", delim))
        delimiter.addAction(space)

        semicolon = QAction("semicolon",
                            self.tool_window.ui_area,
                            checkable=True)
        semicolon.setChecked(self.settings.delimiter == "semicolon")
        semicolon.triggered.connect(lambda *args, delim="semicolon": self.
                                    settings.__setattr__("delimiter", delim))
        delimiter.addAction(semicolon)

        add_header = QAction("&Include CSV header",
                             self.tool_window.ui_area,
                             checkable=True)
        add_header.setChecked(self.settings.include_header)
        add_header.triggered.connect(self.header_check)
        export.addAction(add_header)

        comma.triggered.connect(
            lambda *args, action=tab: action.setChecked(False))
        comma.triggered.connect(
            lambda *args, action=space: action.setChecked(False))
        comma.triggered.connect(
            lambda *args, action=semicolon: action.setChecked(False))

        tab.triggered.connect(
            lambda *args, action=comma: action.setChecked(False))
        tab.triggered.connect(
            lambda *args, action=space: action.setChecked(False))
        tab.triggered.connect(
            lambda *args, action=semicolon: action.setChecked(False))

        space.triggered.connect(
            lambda *args, action=comma: action.setChecked(False))
        space.triggered.connect(
            lambda *args, action=tab: action.setChecked(False))
        space.triggered.connect(
            lambda *args, action=semicolon: action.setChecked(False))

        semicolon.triggered.connect(
            lambda *args, action=comma: action.setChecked(False))
        semicolon.triggered.connect(
            lambda *args, action=tab: action.setChecked(False))
        semicolon.triggered.connect(
            lambda *args, action=space: action.setChecked(False))

        menu.setNativeMenuBar(False)
        self._menu = menu
        layout.setMenuBar(menu)
        menu.setVisible(True)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def calc_relative_thermo(self, *args):
        """changes the values on the 'relative' tab
        called when the tool is opened and whenever something changes on the relative tab"""
        def calc_free_energies(nrg_list, co_list, T, w0):
            """returns lists for ZPVE, H, RRHO G, QRRHO G, and QHARM G
            for the items in nrg_list and co_list at temperature T
            and frequency parameter w0"""
            ZPVEs = []
            if all(co.frequency.anharm_data for co_group in co_list
                   for co in co_group):
                anharm_ZVPEs = []
            else:
                anharm_ZVPEs = None
            Hs = []
            Gs = []
            RRHOG = []
            QHARMG = []
            for i in range(0, len(nrg_list)):
                ZPVEs.append([])
                if anharm_ZVPEs is not None:
                    anharm_ZVPEs.append([])
                Hs.append([])
                Gs.append([])
                RRHOG.append([])
                QHARMG.append([])
                for nrg, co in zip(nrg_list[i], co_list[i]):
                    ZPVE = co.ZPVE
                    ZPVEs[-1].append(nrg + ZPVE)

                    if anharm_ZVPEs is not None:
                        zpve_anharm = co.calc_zpe(anharmonic=True)
                        anharm_ZVPEs[-1].append(nrg + zpve_anharm)

                    dH = co.therm_corr(T, w0, CompOutput.RRHO)[1]
                    Hs[-1].append(nrg + dH)

                    dG = co.calc_G_corr(temperature=T,
                                        v0=w0,
                                        method=CompOutput.RRHO)
                    Gs[-1].append(nrg + dG)

                    dQRRHOG = co.calc_G_corr(temperature=T,
                                             v0=w0,
                                             method=CompOutput.QUASI_RRHO)
                    RRHOG[-1].append(nrg + dQRRHOG)

                    dQHARMG = co.calc_G_corr(temperature=T,
                                             v0=w0,
                                             method=CompOutput.QUASI_HARMONIC)
                    QHARMG[-1].append(nrg + dQHARMG)

            return ZPVEs, anharm_ZVPEs, Hs, Gs, RRHOG, QHARMG

        def boltzmann_weight(energies1, energies2, T):
            """
            energies - list of lists
                       list axis 0 - molecule groups
                            axis 1 - energies of conformers
            boltzmann weight energies for conformers 
            combine energies for molecule groups 
            return the difference"""
            totals1 = []
            totals2 = []

            beta = UNIT.HART_TO_JOULE / (PHYSICAL.KB * T)

            for energy_group in energies1:
                if len(energy_group) == 0:
                    continue

                rezero = min(energy_group)
                rel_nrgs = [(x - rezero) for x in energy_group]
                weights = [np.exp(-beta * nrg) for nrg in rel_nrgs]

                totals1.append(-PHYSICAL.BOLTZMANN * T * np.log(sum(weights)) +
                               rezero * UNIT.HART_TO_KCAL)

            for energy_group in energies2:
                if len(energy_group) == 0:
                    continue

                rezero = min(energy_group)
                rel_nrgs = [(x - rezero) for x in energy_group]
                weights = [np.exp(-beta * nrg) for nrg in rel_nrgs]

                totals2.append(-PHYSICAL.BOLTZMANN * T * np.log(sum(weights)) +
                               rezero * UNIT.HART_TO_KCAL)

            return sum(totals2) - sum(totals1)

        ref_Es = self.ref_group.energies()
        ref_cos = self.ref_group.compOutputs()

        other_Es = self.other_group.energies()
        other_cos = self.other_group.compOutputs()

        if any(
                len(x) == 0 or all(len(y) == 0 for y in x)
                for x in [ref_Es, other_Es]):
            self.relative_table.setText("not enough data")
            return

        T = self.relative_temperature.value()
        self.settings.rel_temp = T
        w0 = self.relative_v0.value()

        if w0 != self.settings.w0:
            self.settings.w0 = w0

        rel_E = boltzmann_weight(ref_Es, other_Es, T)
        headers = ["ΔE"]
        data = [rel_E]

        empty_groups = []
        for i, group in enumerate(ref_cos):
            if len(group) == 0:
                empty_groups.append(i)
        for ndx in empty_groups[::-1]:
            ref_Es.pop(ndx)
            ref_cos.pop(ndx)

        empty_groups = []
        for i, group in enumerate(other_cos):
            if len(group) == 0:
                empty_groups.append(i)

        for ndx in empty_groups[::-1]:
            other_Es.pop(ndx)
            other_cos.pop(ndx)

        if not any(
                len(x) == 0 or all(len(y) == 0 for y in x)
                for x in [ref_cos, other_cos]):
            rel_E = boltzmann_weight(ref_Es, other_Es, T)
            data = [rel_E]

            ref_ZPVEs, ref_anharm_zpve, ref_Hs, ref_Gs, ref_QRRHOGs, ref_QHARMGs = calc_free_energies(
                ref_Es, ref_cos, T, w0)
            other_ZPVEs, other_anharm_zpve, other_Hs, other_Gs, other_QRRHOGs, other_QHARMGs = calc_free_energies(
                other_Es, other_cos, T, w0)

            rel_ZPVE = boltzmann_weight(ref_ZPVEs, other_ZPVEs, T)
            rel_H = boltzmann_weight(ref_Hs, other_Hs, T)
            rel_G = boltzmann_weight(ref_Gs, other_Gs, T)
            rel_QRRHOG = boltzmann_weight(ref_QRRHOGs, other_QRRHOGs, T)
            rel_QHARMG = boltzmann_weight(ref_QHARMGs, other_QHARMGs, T)

            if ref_anharm_zpve and other_anharm_zpve:
                rel_anharm_ZPVE = boltzmann_weight(ref_anharm_zpve,
                                                   other_anharm_zpve, T)
                headers.extend([
                    "ΔZPE",
                    "ΔZPE<sub>anh</sub>",
                    "ΔH<sub>RRHO</sub>",
                    "ΔG<sub>RRHO</sub>",
                    "ΔG<sub>Quasi-RRHO</sub>",
                    "ΔG<sub>Quasi-Harmonic</sub>",
                ])
                data.extend([
                    rel_ZPVE,
                    rel_anharm_ZPVE,
                    rel_H,
                    rel_G,
                    rel_QRRHOG,
                    rel_QHARMG,
                ])
            else:
                headers.extend([
                    "ΔZPE",
                    "ΔH<sub>RRHO</sub>",
                    "ΔG<sub>RRHO</sub>",
                    "ΔG<sub>Quasi-RRHO</sub>",
                    "ΔG<sub>Quasi-Harmonic</sub>",
                ])
                data.extend([
                    rel_ZPVE,
                    rel_H,
                    rel_G,
                    rel_QRRHOG,
                    rel_QHARMG,
                ])

        self._headers = headers
        self._data = data

        s = "<table style=\"border: 1px solid ghostwhite;\">\n"
        s += "\t<tr>\n"
        for header in headers:
            s += "\t\t<th style=\"text-align: center; border: 1px solid ghostwhite; padding-left: 5px;  padding-right: 5px\">%s</th>\n" % header
        s += "\t</tr>\n"
        s += "\t<tr>\n"
        for val in data:
            s += "\t\t<td style=\"text-align: center; border: 1px solid ghostwhite; padding-left: 5px;  padding-right: 5px\">%.1f</td>\n" % val
        s += "\t</tr>\n"
        s += "</table>"

        self.relative_table.setText(s)

    def open_link(self, theory):
        """open the oft-cited QRRHO or QHARM reference"""
        link = self.theory_helper[theory]
        run(self.session, "open %s" % link)

    def save_csv(self):
        """save data on current tab to CSV file"""
        filename, _ = QFileDialog.getSaveFileName(filter="CSV Files (*.csv)")
        if filename:
            s = self.get_csv()

            with open(filename, 'w') as f:
                f.write(s.strip())

            self.session.logger.status("saved to %s" % filename)

    def copy_csv(self):
        """put CSV data for current tab on the clipboard"""
        app = QApplication.instance()
        clipboard = app.clipboard()
        csv = self.get_csv()
        clipboard.setText(csv)
        self.session.logger.status("copied to clipboard")

    def get_csv(self):
        """get CSV data for the current tab"""
        if self.settings.delimiter == "comma":
            delim = ","
        elif self.settings.delimiter == "space":
            delim = " "
        elif self.settings.delimiter == "tab":
            delim = "\t"
        elif self.settings.delimiter == "semicolon":
            delim = ";"

        if self.tab_widget.currentIndex() == 0:
            s = ""
            if self.settings.include_header:
                link_re = QRegularExpression("<a[\d\D]*>(𝛿?Δ?[\d\D]*)</a>")
                # get header labels from the tables
                for table in [
                        self.sp_table, self.thermo_table, self.sum_table
                ]:
                    for row in range(0, table.rowCount()):
                        item = table.cellWidget(row, 0)
                        text = item.text()
                        text = text.replace(" =", "")
                        text = text.replace("<sub>", "(")
                        text = text.replace("</sub>", ")")
                        # if there's a link like for QRRHO G, remove the link
                        if "href=" in text:
                            match = link_re.match(text)
                            text = match.captured(1)
                        text = text.replace("𝛿", "d")
                        s += "%s%s" % (text, delim)

                s += "SP_File%sThermo_File\n" % delim

            # get values from tables
            for table in [self.sp_table, self.thermo_table, self.sum_table]:
                for row in range(0, table.rowCount()):
                    item = table.cellWidget(row, 1)
                    s += "%s%s" % (item.text(), delim)

            sp_mdl = self.sp_selector.currentData()
            sp_name = sp_mdl.name
            therm_mdl = self.thermo_selector.currentData()

            s += "%s" % sp_name
            if therm_mdl:
                therm_name = therm_mdl.name
                s += "%s%s" % (delim, therm_name)
            s += "\n"

        elif self.tab_widget.currentIndex() == 1:
            s = ""
            if self.settings.include_header:
                link_re = QRegularExpression("<a[\d\D]*>(𝛿?Δ?[\d\D]*)</a>")
                # get header labels from the tables
                for text in self._headers:
                    text = text.replace(" =", "")
                    text = text.replace("<sub>", "(")
                    text = text.replace("</sub>", ")")
                    # if there's a link like for QRRHO G, remove the link
                    if "href=" in text:
                        match = link_re.match(text)
                        text = match.captured(1)
                    text = text.replace("𝛿", "d")
                    s += "%s%s" % (text, delim)

            s = s.rstrip(delim)
            s += "\n"
            s += delim.join(["%.1f" % val for val in self._data])

        return s

    def header_check(self, state):
        """user has [un]checked the 'include header' option on the menu"""
        if state:
            self.settings.include_header = True
        else:
            self.settings.include_header = False

    def set_sp(self):
        """set energy entry for when sp model changes"""
        self.sp_table.setRowCount(0)
        if self.sp_selector.currentIndex() >= 0:
            fr = self.sp_selector.currentData()

            self.check_geometry_rmsd("SP")

            self.sp_table.insertRow(0)

            nrg_label = QLabel("E =")
            nrg_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
            self.sp_table.setCellWidget(0, 0, nrg_label)

            unit_label = ReadOnlyTableItem()
            unit_label.setData(Qt.DisplayRole, "E\u2095")
            unit_label.setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter)
            self.sp_table.setItem(0, 2, unit_label)

            sp_nrg = SmallLineEdit("%.6f" % fr.other['energy'])
            sp_nrg.setReadOnly(True)
            sp_nrg.setFrame(False)
            self.sp_table.setCellWidget(0, 1, sp_nrg)

            self.sp_table.resizeRowToContents(0)
            self.sp_table.resizeColumnToContents(0)
            self.sp_table.resizeColumnToContents(1)
            self.sp_table.resizeColumnToContents(2)

        self.update_sum()

    def set_thermo_mdl(self):
        """
        frequencies filereader has changed on the absolute tab
        also changes the temperature option (on the absolute tab only)
        """
        if self.thermo_selector.currentIndex() >= 0:
            fr = self.thermo_selector.currentData()

            self.check_geometry_rmsd("THERM")

            if 'temperature' in fr.other:
                self.temperature_line.setValue(fr.other['temperature'])

        self.set_thermo()

    def check_geometry_rmsd(self, *args):
        """check RMSD between energy and frequency filereader on the absolute tab
        if the RMSD is > 10^-5 or the number of atoms is different, put a warning in the
        status bar"""
        if self.thermo_selector.currentIndex(
        ) >= 0 and self.sp_selector.currentIndex() >= 0:
            fr = self.sp_selector.currentData()
            fr2 = self.thermo_selector.currentData()

            if len(fr.atoms) != len(fr2.atoms):
                self.status.showMessage(
                    "structures are not the same: different number of atoms")
                return

            geom = Geometry(fr)
            geom2 = Geometry(fr2)
            rmsd = geom.RMSD(geom2)
            if not isclose(rmsd, 0, atol=10**-5):
                rmsd = geom.RMSD(geom2, sort=True)

            if not isclose(rmsd, 0, atol=10**-5):
                self.status.showMessage(
                    "structures might not be the same - RMSD = %.4f" % rmsd)
            else:
                self.status.showMessage("")

    def set_thermo(self):
        """computes thermo corrections and sets thermo entries for when thermo model changes"""
        #index of combobox is -1 when combobox has no entries
        self.thermo_table.setRowCount(0)
        if self.thermo_selector.currentIndex() >= 0:
            fr = self.thermo_selector.currentData()
            if fr not in self.thermo_co:
                self.thermo_co[fr] = CompOutput(fr)
            co = self.thermo_co[fr]

            v0 = self.v0_edit.value()

            if v0 != self.settings.w0:
                self.settings.w0 = v0

            T = self.temperature_line.value()
            if not T:
                return

            dZPE = co.ZPVE
            #compute enthalpy and entropy at this temperature
            #AaronTools uses Grimme's Quasi-RRHO, but this is the same as RRHO when w0=0
            dE, dH, s = co.therm_corr(temperature=T, v0=0, method="RRHO")
            rrho_dg = dH - T * s
            #compute G with quasi entropy treatments
            qrrho_dg = co.calc_G_corr(v0=v0, temperature=T, method="QRRHO")
            qharm_dg = co.calc_G_corr(v0=v0, temperature=T, method="QHARM")

            items = [(
                "𝛿ZPE =",
                dZPE,
                None,
                "lowest energy the molecule can have\n"
                "no rotational or vibrational modes populated\n"
                "equal to enthalpy at 0 K",
            )]

            if fr.other["frequency"].anharm_data:
                dZPE_anh = co.calc_zpe(anharmonic=True)
                items.append((
                    "𝛿ZPE<sub>anh</sub> =",
                    dZPE_anh,
                    None,
                    "lowest energy the molecule can have\n"
                    "no rotational or vibrational modes populated\n"
                    "includes corrections for anharmonic vibrations",
                ))

            items.extend([
                (
                    "𝛿H<sub>RRHO</sub> =",
                    dH,
                    None,
                    "enthalpy of formation",
                ),
                (
                    "𝛿G<sub>RRHO</sub> =",
                    rrho_dg,
                    None,
                    "energy after taking into account the average\n"
                    "population of vibrational, rotational, and translational\n"
                    "degrees of freedom",
                ),
                (
                    "𝛿G<sub>Quasi-RRHO</sub> =",
                    qrrho_dg,
                    "Grimme's Quasi-RRHO",
                    "vibrational entropy of each real mode is damped and complemented\n"
                    "with rotational entropy, with the damping function being stronger for\n"
                    "frequencies < 𝜔\u2080\n"
                    "can mitigate error from inaccuracies in the harmonic oscillator\n"
                    "approximation for low-frequency vibrations",
                ),
                (
                    "𝛿G<sub>Quasi-Harmonic</sub> =",
                    qharm_dg,
                    "Truhlar's Quasi-Harmonic",
                    "real vibrational frequencies below 𝜔\u2080 are treated as 𝜔\u2080\n"
                    "can mitigate error from inaccuracies in the harmonic oscillator\n"
                    "approximation for low-frequency vibrations",
                ),
            ])

            for i, (label_text, val, link, tooltip) in enumerate(items):
                self.thermo_table.insertRow(i)

                label = QLabel(label_text)
                if link:
                    label = QLabel()
                    label.setText(
                        "<a href=\"%s\" style=\"text-decoration: none;\">%s</a>"
                        % (link, label_text))
                    label.setTextFormat(Qt.RichText)
                    label.setTextInteractionFlags(Qt.TextBrowserInteraction)
                    label.linkActivated.connect(self.open_link)

                label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
                label.setToolTip(tooltip)

                self.thermo_table.setCellWidget(i, 0, label)

                unit_label = ReadOnlyTableItem()
                unit_label.setData(Qt.DisplayRole, "E\u2095")
                unit_label.setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter)
                unit_label.setToolTip(tooltip)
                self.thermo_table.setItem(i, 2, unit_label)

                d_nrg = SmallLineEdit("%.6f" % val)
                d_nrg.setReadOnly(True)
                d_nrg.setFrame(False)
                d_nrg.setToolTip(tooltip)
                self.thermo_table.setCellWidget(i, 1, d_nrg)

                self.thermo_table.resizeRowToContents(i)

            self.thermo_table.resizeColumnToContents(0)
            self.thermo_table.resizeColumnToContents(1)
            self.thermo_table.resizeColumnToContents(2)

        self.update_sum()

    def update_sum(self):
        """updates the sum of energy and thermo corrections"""
        self.sum_table.setRowCount(0)

        if not self.sp_table.rowCount():
            return

        sp_nrg = float(self.sp_table.cellWidget(0, 1).text())

        for row in range(0, self.thermo_table.rowCount()):
            self.sum_table.insertRow(row)

            label = self.thermo_table.cellWidget(row, 0)
            tooltip = label.toolTip()
            text = label.text().replace("𝛿", "")
            sum_label = QLabel(text)
            if "href=" in text:
                sum_label = QLabel()
                sum_label.setText(text)
                sum_label.setTextFormat(Qt.RichText)
                sum_label.setTextInteractionFlags(Qt.TextBrowserInteraction)
                sum_label.linkActivated.connect(self.open_link)

            sum_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
            sum_label.setToolTip(tooltip)

            self.sum_table.setCellWidget(row, 0, sum_label)

            thermo = float(self.thermo_table.cellWidget(row, 1).text())

            total = sp_nrg + thermo
            total_nrg = SmallLineEdit("%.6f" % total)
            total_nrg.setFrame(False)
            total_nrg.setReadOnly(True)
            total_nrg.setToolTip(tooltip)
            self.sum_table.setCellWidget(row, 1, total_nrg)

            unit_label = ReadOnlyTableItem()
            unit_label.setData(Qt.DisplayRole, "E\u2095")
            unit_label.setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter)
            unit_label.setToolTip(tooltip)
            self.sum_table.setItem(row, 2, unit_label)

            self.sum_table.resizeRowToContents(row)

        self.sum_table.resizeColumnToContents(0)
        self.sum_table.resizeColumnToContents(1)
        self.sum_table.resizeColumnToContents(2)

    def display_help(self):
        """Show the help for this tool in the help viewer."""
        from chimerax.core.commands import run
        run(self.session,
            'open %s' % self.help if self.help is not None else "")

    def delete(self):
        #overload because closing a tool window doesn't destroy any widgets on it
        self.settings.ref_col_1 = self.ref_group.tree.columnWidth(0)
        self.settings.ref_col_2 = self.ref_group.tree.columnWidth(1)

        self.settings.other_col_1 = self.other_group.tree.columnWidth(0)
        self.settings.other_col_2 = self.other_group.tree.columnWidth(1)

        self.sp_selector.deleteLater()
        self.thermo_selector.deleteLater()
        self.ref_group.deleteLater()
        self.other_group.deleteLater()

        return super().delete()

    def close(self):
        #overload because closing a tool window doesn't destroy any widgets on it
        self.settings.ref_col_1 = self.ref_group.tree.columnWidth(0)
        self.settings.ref_col_2 = self.ref_group.tree.columnWidth(1)

        self.settings.other_col_1 = self.other_group.tree.columnWidth(0)
        self.settings.other_col_2 = self.other_group.tree.columnWidth(1)

        self.sp_selector.deleteLater()
        self.thermo_selector.deleteLater()
        self.ref_group.deleteLater()
        self.other_group.deleteLater()

        return super().close()
Пример #15
0
class _ModelSelector(ToolInstance):
    def __init__(self, session, name, coords):
        super().__init__(session, name)
        self._coords = coords
        self.tool_window = MainToolWindow(self)
        self._build_ui()

    def _build_ui(self):
        layout = QFormLayout()

        self.model_selector = ModelComboBox(self.session, addNew=True)
        layout.addRow("model:", self.model_selector)

        do_it = QPushButton(
            "add %s %s to selected model" %
            (ChangeElementMouseMode.vsepr, ChangeElementMouseMode.element))
        do_it.clicked.connect(self.new_atom)
        layout.addRow(do_it)

        open_ele_sel = QPushButton("open element picker")
        open_ele_sel.clicked.connect(self.show_ele_sel)
        layout.addRow(open_ele_sel)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def show_ele_sel(self, *args):
        _ElementPicker(self.session, "pick element")
        self.delete()

    def new_atom(self):
        element = ChangeElementMouseMode.element
        adjust_bonds = True
        vsepr = ChangeElementMouseMode.vsepr

        if vsepr == "do not change":
            vsepr = False
        elif vsepr == "linear (1 bond)":
            vsepr = "linear 1"
        elif vsepr == "linear (2 bonds)":
            vsepr = "linear 2"
        elif vsepr == "trigonal planar (2 bonds)":
            vsepr = "bent 2 planar"
        elif vsepr == "tetrahedral (2 bonds)":
            vsepr = "bent 2 tetrahedral"
        elif vsepr == "tetrahedral (3 bonds)":
            vsepr = "bent 3 tetrahedral"

        if vsepr:
            atoms = Atom.get_shape(vsepr)
            atoms[0].element = element
            for atom in atoms[1:]:
                atom.element = "H"
            if adjust_bonds:
                # this works b/c all atoms are 1 angstrom from the center
                # just multiply by the distance we want
                expected_dist = RADII[element] + RADII["H"]
                for atom in atoms[1:]:
                    atom.coords *= expected_dist
            for atom in atoms[1:]:
                atoms[0].connected.add(atom)
                atom.connected.add(atoms[0])
        else:
            atoms = [Atom(element=element, coords=np.zeros(3))]

        rescol = ResidueCollection(atoms, name="new", refresh_connected=False)
        rescol.coord_shift(self._coords)

        model = self.model_selector.currentData()
        if model is None:
            chix = rescol.get_chimera(self.session)
            self.session.models.add([chix])
            apply_seqcrow_preset(chix, fallback="Ball-Stick-Endcap")

        else:
            res = model.new_residue("new", "a", len(model.residues) + 1)
            rescol.residues[0].update_chix(res)
            run(
                self.session, "select add %s" %
                " ".join([atom.atomspec for atom in res.atoms]))

        self.delete()

    def delete(self):
        self.model_selector.deleteLater()
        return super().delete()

    def close(self):
        self.model_selector.deleteLater()
        return super().close()
Пример #16
0
class BondEditor(ToolInstance):

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

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

        self.tool_window = MainToolWindow(self)

        self.settings = _BondEditorSettings(session, name)

        self._build_ui()

    def _build_ui(self):
        layout = QGridLayout()

        tabs = QTabWidget()
        layout.addWidget(tabs)

        ts_bond_tab = QWidget()
        ts_options = QFormLayout(ts_bond_tab)

        self.tsbond_color = ColorButton(has_alpha_channel=False,
                                        max_size=(16, 16))
        self.tsbond_color.set_color(self.settings.tsbond_color)
        ts_options.addRow("color:", self.tsbond_color)

        self.tsbond_transparency = QSpinBox()
        self.tsbond_transparency.setRange(1, 99)
        self.tsbond_transparency.setValue(self.settings.tsbond_transparency)
        self.tsbond_transparency.setSuffix("%")
        ts_options.addRow("transparency:", self.tsbond_transparency)

        self.tsbond_radius = QDoubleSpinBox()
        self.tsbond_radius.setRange(0.01, 1)
        self.tsbond_radius.setDecimals(3)
        self.tsbond_radius.setSingleStep(0.005)
        self.tsbond_radius.setSuffix(" \u212B")
        self.tsbond_radius.setValue(self.settings.tsbond_radius)
        ts_options.addRow("radius:", self.tsbond_radius)

        draw_tsbonds = QPushButton("draw TS bonds on selected atoms/bonds")
        draw_tsbonds.clicked.connect(self.run_tsbond)
        ts_options.addRow(draw_tsbonds)
        self.draw_tsbonds = draw_tsbonds

        erase_tsbonds = QPushButton("erase selected TS bonds")
        erase_tsbonds.clicked.connect(self.run_erase_tsbond)
        ts_options.addRow(erase_tsbonds)
        self.erase_tsbonds = erase_tsbonds

        bond_tab = QWidget()
        bond_options = QFormLayout(bond_tab)

        self.bond_halfbond = QCheckBox()
        self.bond_halfbond.setChecked(self.settings.bond_halfbond)
        self.bond_halfbond.setToolTip(
            "each half of the bond will be colored according to the atom's color"
        )
        bond_options.addRow("half-bond:", self.bond_halfbond)

        self.bond_color = ColorButton(has_alpha_channel=True,
                                      max_size=(16, 16))
        self.bond_color.set_color(self.settings.bond_color)
        self.bond_color.setEnabled(
            self.bond_halfbond.checkState() != Qt.Checked)
        self.bond_halfbond.stateChanged.connect(
            lambda state, widget=self.bond_color: self.bond_color.setEnabled(
                state != Qt.Checked))
        bond_options.addRow("color:", self.bond_color)

        self.bond_radius = QDoubleSpinBox()
        self.bond_radius.setRange(0.01, 1)
        self.bond_radius.setDecimals(3)
        self.bond_radius.setSingleStep(0.005)
        self.bond_radius.setSuffix(" \u212B")
        self.bond_radius.setValue(self.settings.bond_radius)
        bond_options.addRow("radius:", self.bond_radius)

        draw_tsbonds = QPushButton("draw bond between selected atoms")
        draw_tsbonds.clicked.connect(self.run_bond)
        bond_options.addRow(draw_tsbonds)
        self.draw_tsbonds = draw_tsbonds

        erase_bonds = QPushButton("erase selected bonds")
        erase_bonds.clicked.connect(
            lambda *, ses=self.session: run(ses, "delete bonds sel"))
        bond_options.addRow(erase_bonds)
        self.erase_bonds = erase_bonds

        hbond_tab = QWidget()
        hbond_options = QFormLayout(hbond_tab)

        self.hbond_color = ColorButton(has_alpha_channel=True,
                                       max_size=(16, 16))
        self.hbond_color.set_color(self.settings.hbond_color)
        hbond_options.addRow("color:", self.hbond_color)

        self.hbond_radius = QDoubleSpinBox()
        self.hbond_radius.setDecimals(3)
        self.hbond_radius.setSuffix(" \u212B")
        self.hbond_radius.setValue(self.settings.hbond_radius)
        hbond_options.addRow("radius:", self.hbond_radius)

        self.hbond_dashes = QSpinBox()
        self.hbond_dashes.setRange(0, 28)
        self.hbond_dashes.setSingleStep(2)
        self.hbond_radius.setSingleStep(0.005)
        self.hbond_dashes.setValue(self.settings.hbond_dashes)
        hbond_options.addRow("dashes:", self.hbond_dashes)

        draw_hbonds = QPushButton("draw H-bonds")
        draw_hbonds.clicked.connect(self.run_hbond)
        hbond_options.addRow(draw_hbonds)
        self.draw_hbonds = draw_hbonds

        erase_hbonds = QPushButton("erase all H-bonds")
        erase_hbonds.clicked.connect(
            lambda *, ses=self.session: run(ses, "~hbonds"))
        hbond_options.addRow(erase_hbonds)
        self.erase_hbonds = erase_hbonds

        tm_bond_tab = QWidget()
        tm_bond_options = QFormLayout(tm_bond_tab)

        self.tm_bond_color = ColorButton(has_alpha_channel=True,
                                         max_size=(16, 16))
        self.tm_bond_color.set_color(self.settings.tm_bond_color)
        tm_bond_options.addRow("color:", self.tm_bond_color)

        self.tm_bond_radius = QDoubleSpinBox()
        self.tm_bond_radius.setDecimals(3)
        self.tm_bond_radius.setSuffix(" \u212B")
        self.tm_bond_radius.setValue(self.settings.tm_bond_radius)
        tm_bond_options.addRow("radius:", self.tm_bond_radius)

        self.tm_bond_dashes = QSpinBox()
        self.tm_bond_dashes.setRange(0, 28)
        self.tm_bond_dashes.setSingleStep(2)
        self.tm_bond_radius.setSingleStep(0.005)
        self.tm_bond_dashes.setValue(self.settings.tm_bond_dashes)
        tm_bond_options.addRow("dashes:", self.tm_bond_dashes)

        draw_tm_bonds = QPushButton("draw metal coordination bonds")
        draw_tm_bonds.clicked.connect(self.run_tm_bond)
        tm_bond_options.addRow(draw_tm_bonds)
        self.draw_tm_bonds = draw_tm_bonds

        erase_tm_bonds = QPushButton("erase all metal coordination bonds")
        erase_tm_bonds.clicked.connect(self.del_tm_bond)
        tm_bond_options.addRow(erase_tm_bonds)
        self.erase_tm_bonds = erase_tm_bonds

        bond_length_tab = QWidget()
        bond_length_layout = QFormLayout(bond_length_tab)

        self.bond_distance = QDoubleSpinBox()
        self.bond_distance.setRange(0.5, 10.0)
        self.bond_distance.setSingleStep(0.05)
        self.bond_distance.setValue(1.51)
        self.bond_distance.setSuffix(" \u212B")
        bond_length_layout.addRow("bond length:", self.bond_distance)

        self.move_fragment = QComboBox()
        self.move_fragment.addItems(["both", "smaller", "larger"])
        bond_length_layout.addRow("move side:", self.move_fragment)

        bond_lookup = QGroupBox("bond length lookup:")
        bond_lookup_layout = QGridLayout(bond_lookup)

        bond_lookup_layout.addWidget(
            QLabel("elements:"),
            0,
            0,
        )

        self.ele1 = ElementButton("C", single_state=True)
        self.ele1.clicked.connect(
            lambda *args, button=self.ele1: self.open_ptable(button))
        bond_lookup_layout.addWidget(self.ele1, 0, 1,
                                     Qt.AlignRight | Qt.AlignTop)

        bond_lookup_layout.addWidget(QLabel("-"), 0, 2,
                                     Qt.AlignHCenter | Qt.AlignVCenter)

        self.ele2 = ElementButton("C", single_state=True)
        self.ele2.clicked.connect(
            lambda *args, button=self.ele2: self.open_ptable(button))
        bond_lookup_layout.addWidget(self.ele2, 0, 3,
                                     Qt.AlignLeft | Qt.AlignTop)

        bond_lookup_layout.addWidget(QLabel("bond order:"), 1, 0)

        self.bond_order = BondOrderSpinBox()
        self.bond_order.setRange(1., 3.)
        self.bond_order.setValue(1)
        self.bond_order.setSingleStep(0.5)
        self.bond_order.setDecimals(1)
        self.bond_order.valueChanged.connect(self.check_bond_lengths)
        bond_lookup_layout.addWidget(self.bond_order, 1, 1, 1, 3)

        bond_lookup_layout.setColumnStretch(0, 0)
        bond_lookup_layout.setColumnStretch(1, 0)
        bond_lookup_layout.setColumnStretch(2, 0)
        bond_lookup_layout.setColumnStretch(3, 1)

        bond_length_layout.addRow(bond_lookup)

        self.status = QStatusBar()
        self.status.setSizeGripEnabled(False)
        bond_lookup_layout.addWidget(self.status, 2, 0, 1, 4)

        self.do_bond_change = QPushButton("change selected bond lengths")
        self.do_bond_change.clicked.connect(self.change_bond_length)
        bond_length_layout.addRow(self.do_bond_change)

        tabs.addTab(bond_tab, "covalent bonds")
        tabs.addTab(ts_bond_tab, "TS bonds")
        tabs.addTab(hbond_tab, "H-bonds")
        tabs.addTab(tm_bond_tab, "coordination bonds")
        tabs.addTab(bond_length_tab, "bond length")

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def run_tsbond(self, *args):
        args = ["tsbond", "sel"]

        color = self.tsbond_color.get_color()
        args.extend(["color", "rgb(%i, %i, %i)" % tuple(color[:-1])])
        self.settings.tsbond_color = tuple([c / 255. for c in color])

        radius = self.tsbond_radius.value()
        args.extend(["radius", "%.3f" % radius])
        self.settings.tsbond_radius = radius

        transparency = self.tsbond_transparency.value()
        args.extend(["transparency", "%i" % transparency])
        self.settings.tsbond_transparency = transparency

        run(self.session, " ".join(args))

    def run_erase_tsbond(self, *args):
        run(self.session, "~tsbond sel")

    def run_bond(self, *args):
        # TODO: switch to `bond sel` in 1.2
        sel = selected_atoms(self.session)
        halfbond = self.bond_halfbond.checkState() == Qt.Checked
        self.settings.bond_halfbond = halfbond

        if not halfbond:
            color = self.bond_color.get_color()
            color = tuple(x / 255. for x in color)
            self.settings.bond_color = color

        radius = self.bond_radius.value()
        self.settings.bond_radius = radius

        for b in selected_bonds(self.session):
            b.halfbond = halfbond
            if not halfbond:
                b.color = np.array([int(x * 255) for x in color])

            b.radius = radius

        for i, a1 in enumerate(sel):
            for a2 in sel[:i]:
                if a1.structure is a2.structure and a2 not in a1.neighbors:
                    new_bond = a1.structure.new_bond(a1, a2)
                    new_bond.halfbond = halfbond

                    if not halfbond:
                        new_bond.color = np.array(
                            [int(x * 255) for x in color])

                    new_bond.radius = radius

    def run_hbond(self, *args):
        args = ["hbonds", "reveal", "true"]

        color = self.hbond_color.get_color()
        args.extend(["color", "rgb(%i, %i, %i)" % tuple(color[:-1])])
        self.settings.hbond_color = tuple([c / 255. for c in color])

        radius = self.hbond_radius.value()
        args.extend(["radius", "%.3f" % radius])
        self.settings.hbond_radius = radius

        dashes = self.hbond_dashes.value()
        args.extend(["dashes", "%i" % dashes])
        self.settings.hbond_dashes = dashes

        run(self.session, " ".join(args))

    def run_tm_bond(self, *args):
        color = self.tm_bond_color.get_color()
        self.settings.tm_bond_color = tuple([c / 255. for c in color])

        radius = self.tm_bond_radius.value()
        self.settings.tm_bond_radius = radius

        dashes = self.tm_bond_dashes.value()
        self.settings.tm_bond_dashes = dashes

        models = self.session.models.list(type=AtomicStructure)
        for model in models:
            rescol = ResidueCollection(model, bonds_matter=False)
            try:
                tm_list = rescol.find([
                    AnyTransitionMetal(), "Na", "K", "Rb", "Cs", "Fr", "Mg",
                    "Ca", "Sr", "Ba", "Ra"
                ])
                for tm in tm_list:
                    for atom in rescol.atoms:
                        if atom is tm:
                            continue

                        if atom.is_connected(tm):
                            pbg = model.pseudobond_group(
                                model.PBG_METAL_COORDINATION,
                                create_type="normal")
                            pbg.new_pseudobond(tm.chix_atom, atom.chix_atom)
                            pbg.dashes = dashes
                            pbg.color = color
                            pbg.radius = radius

            except LookupError:
                pass

    def del_tm_bond(self, *args):
        models = self.session.models.list(type=AtomicStructure)
        for model in models:
            pbg = model.pseudobond_group(model.PBG_METAL_COORDINATION,
                                         create_type=None)
            if pbg is not None:
                pbg.delete()

    def open_ptable(self, button):
        self.tool_window.create_child_window("select element",
                                             window_class=PTable2,
                                             button=button,
                                             callback=self.check_bond_lengths)

    def check_bond_lengths(self, *args):
        ele1 = self.ele1.text()
        ele2 = self.ele2.text()
        key = ORDER_BOND_ORDER.key(ele1, ele2)

        order = "%.1f" % self.bond_order.value()
        if key in ORDER_BOND_ORDER.bonds and order in ORDER_BOND_ORDER.bonds[
                key]:
            self.bond_distance.setValue(ORDER_BOND_ORDER.bonds[key][order])
            self.status.showMessage("")
        else:
            self.status.showMessage("no bond data for %s-%s %sx bonds" %
                                    (ele1, ele2, order))

    def change_bond_length(self, *args):
        dist = self.bond_distance.value()

        atom_pairs = []

        sel = selected_atoms(self.session)
        if len(sel) == 2 and sel[0].structure is sel[1].structure:
            atom_pairs.append(sel)

        for bond in selected_bonds(self.session):
            if not all(atom in sel for atom in bond.atoms):
                atom_pairs.append(bond.atoms)

        for bond in selected_pseudobonds(self.session):
            if not all(atom in sel for atom in bond.atoms):
                atom_pairs.append(bond.atoms)

        for pair in atom_pairs:
            atom1, atom2 = pair
            frag1 = get_fragment(atom1,
                                 stop=atom2,
                                 max_len=atom1.structure.num_atoms)
            frag2 = get_fragment(atom2,
                                 stop=atom1,
                                 max_len=atom1.structure.num_atoms)

            v = atom2.coord - atom1.coord

            cur_dist = np.linalg.norm(v)
            change = dist - cur_dist

            if self.move_fragment.currentText() == "both":
                change = 0.5 * change
                frag1.coords -= change * v / cur_dist
                frag2.coords += change * v / cur_dist
            elif self.move_fragment.currentText() == "smaller":
                if len(frag1) < len(frag2) or (len(frag1) == len(frag2)
                                               and sum(frag1.elements.masses) <
                                               sum(frag2.elements.masses)):
                    frag1.coords -= change * v / cur_dist
                else:
                    frag2.coords += change * v / cur_dist
            elif self.move_fragment.currentText() == "larger":
                if len(frag1) > len(frag2) or (len(frag1) == len(frag2)
                                               and sum(frag1.elements.masses) >
                                               sum(frag2.elements.masses)):
                    frag1.coords -= change * v / cur_dist
                else:
                    frag2.coords += change * v / cur_dist
Пример #17
0
class _SubstituentSelector(ToolInstance):
    def __init__(self, session, name):
        super().__init__(session, name)
        self.tool_window = MainToolWindow(self)
        self._build_ui()

    def _build_ui(self):
        layout = QFormLayout()

        self.substituent_table = SubstituentTable(singleSelect=True)
        layout.addRow(self.substituent_table)

        self.new_residue = QCheckBox()
        self.new_residue.setCheckState(
            Qt.Checked if SubstituteMouseMode.newRes else Qt.Unchecked)
        layout.addRow("new residue:", self.new_residue)

        self.res_name = QLineEdit()
        self.res_name.setPlaceholderText("leave blank to keep current")
        layout.addRow("set residue name:", self.res_name)

        self.distance_names = QCheckBox()
        self.distance_names.setCheckState(
            Qt.Checked if SubstituteMouseMode.useRemoteness else Qt.Unchecked)
        layout.addRow("distance atom names:", self.distance_names)

        self.keep_open = QCheckBox()
        layout.addRow("keep list open:", self.keep_open)

        do_it = QPushButton("set substituent")
        do_it.clicked.connect(self.set_sub)
        layout.addRow(do_it)

        self.keep_open.stateChanged.connect(
            lambda state: do_it.setVisible(state != Qt.Checked))
        self.keep_open.stateChanged.connect(self.sub_changed)

        self.substituent_table.table.itemSelectionChanged.connect(
            self.sub_changed)
        self.new_residue.stateChanged.connect(self.sub_changed)
        self.res_name.textChanged.connect(self.sub_changed)
        self.distance_names.stateChanged.connect(self.sub_changed)

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def sub_changed(self):
        if self.keep_open.checkState() == Qt.Checked:
            self.set_sub()

    def set_sub(self):
        sub_names = []
        for row in self.substituent_table.table.selectionModel().selectedRows(
        ):
            if self.substituent_table.table.isRowHidden(row.row()):
                continue

            sub_names.append(row.data())

        if sub_names:
            SubstituteMouseMode.substituent = sub_names[0]
            SubstituteMouseMode.newRes = self.new_residue.checkState(
            ) == Qt.Checked
            SubstituteMouseMode.useRemoteness = self.distance_names.checkState(
            ) == Qt.Checked
            sub_name = self.res_name.text()
            SubstituteMouseMode.resName = sub_name

        if self.keep_open.checkState() != Qt.Checked:
            self.delete()

    def close(self):
        sub_names = []
        for row in self.substituent_table.table.selectionModel().selectedRows(
        ):
            if self.substituent_table.table.isRowHidden(row.row()):
                continue

            sub_names.append(row.data())

        if sub_names:
            SubstituteMouseMode.substituent = sub_names[0]
            SubstituteMouseMode.newRes = self.new_residue.checkState(
            ) == Qt.Checked
            SubstituteMouseMode.useRemoteness = self.distance_names.checkState(
            ) == Qt.Checked
            sub_name = self.res_name.text()
            SubstituteMouseMode.resName = sub_name

        super().close()
Пример #18
0
class ISOLDE_ToolUI(ToolInstance):

    SESSION_ENDURING = True
    # if SESSION_ENDURING is True, tool instance not deleted at session closure

    help = _find_help()

    def __init__(self, session, tool_name):
        ToolInstance.__init__(self, session, tool_name)
        self.display_name = "ISOLDE"
        from chimerax.ui.gui import MainToolWindow
        self.tool_window = MainToolWindow(self)
        self.tool_window.manage(placement=None)
        parent = self.tool_window.ui_area
        pp = parent.parent().parent()
        pp.resize(540, 850)

        import os
        basedir = os.path.dirname(os.path.abspath(__file__))
        from PyQt5 import uic
        uifile = os.path.join(basedir, 'ui', 'IsoldeFrame.ui')
        from Qt import QtWidgets, QtGui
        #from . import isoldewidget
        from .resources import resources_rc
        mw = self.mainwin = QtWidgets.QFrame(parent=parent)
        iw = self.iw = uic.loadUi(uifile, mw)
        # iw = self.iw = isoldewidget.Ui_isolde_widget()
        #iw = w.Ui_isolde_widget()
        # iw.setupUi(self.mainwin)

        import os
        icon_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                 'resources/logo_small.png')
        isolde_icon = QtGui.QPixmap(icon_file)
        iw._isolde_icon.setPixmap(isolde_icon)

        layout = QtWidgets.QHBoxLayout()
        layout.addWidget(self.mainwin)
        layout.setStretchFactor(self.mainwin, 1)
        layout.setContentsMargins(1, 1, 1, 3)
        parent.setLayout(layout)
        self.tool_window.manage(placement=None)
        # Should load saved state here

        combo_boxes = (
            iw._tutorials_combo_box,
            iw._master_model_combo_box,
            iw._real_space_map_volume_combo_box,
            iw._sim_basic_xtal_settings_map_combo_box,
            iw._sim_force_field_combo_box,
            iw._sim_platform_combo_box,
            iw._experience_level_combo_box,
            iw._rebuild_sel_res_rot_backrub_map_combo_box,
            iw._validate_rama_case_combo_box,
            iw._validate_rama_sel_combo_box,
        )
        from .ui.util import win_auto_resize_combo_box_hack
        for cb in combo_boxes:
            win_auto_resize_combo_box_hack(cb)

        ###
        # Selection manipulation buttons
        ###

        # Grow selection at N-terminus
        self._sel_grow_n_terminal_buttons = (iw._sel_extend_N_button, )

        # Shrink selection at N-terminus
        self._sel_shrink_n_terminal_buttons = (iw._sel_shrink_N_button, )

        # Shrink selection at C-terminus
        self._sel_shrink_c_terminal_buttons = (iw._sel_shrink_C_button, )

        # Grow selection at C-terminus
        self._sel_grow_c_terminal_buttons = (iw._sel_extend_C_button, )

        # Define intermediate and expert frames
        self._intermediate_frames = [
            iw._sim_platform_frame,
            iw._sim_basic_load_cif_templates_button,
        ]
        self._expert_frames = [
            iw._force_field_selection_frame,
        ]

        # Any other frames/widgets that should be hidden at the start
        self._hidden_at_start = [
            iw._validate_rama_main_frame,
            iw._validate_pep_main_frame,
            iw._validate_rota_main_frame,
            iw._validate_unparameterised_residues_main_frame,
            iw._sim_basic_xtal_init_open_button,
            iw._sim_basic_xtal_init_main_frame,
            iw._sim_basic_xtal_settings_live_recalc_checkbox,
            iw._sim_basic_xtal_map_settings_frame,
            iw._real_space_map_from_volume_frame,
            iw._live_map_control_frame,
            iw._sim_status_indicator,
        ]

        for f in self._hidden_at_start:
            f.hide()

        # Any frames/widgets that should be disabled at the start
        self._disabled_at_start = [
            #iw._sim_basic_xtal_map_settings_frame,
            iw._map_masking_frame,
            iw._rebuild_sel_res_pep_flip_button,
            iw._rebuild_sel_res_cis_trans_flip_button,
            iw._rebuild_cycle_rotamer_frame,
            #iw._rebuild_sel_res_last_rotamer_button,
            #iw._rebuild_sel_res_next_rotamer_button,
            iw._rebuild_sel_res_rot_commit_button,
            iw._rebuild_sel_res_rot_target_button,
            iw._rebuild_sel_res_rot_discard_button,
            iw._rebuild_sel_res_rot_release_button,
            #~ iw._rebuild_pos_restraint_one_atom_frame,
            #iw._rebuild_pin_atom_container,
            iw._rebuild_pin_atom_to_current_pos_button,
            iw._rebuild_pin_atom_to_pivot_button,
            iw._rebuild_pos_restraint_clear_button,
            iw._rebuild_2ry_struct_restr_container,
            iw._rebuild_2ry_struct_restr_clear_button,
            iw._rebuild_register_shift_container,
            iw._rebuild_dist_restraint_container,
            iw._rebuild_dist_restraint_apply_button,
            iw._rebuild_grow_shrink_sel_frame,
            iw._right_mouse_modes_frame,
            iw._live_map_recalc_button,
            iw._live_map_update_sim_button,
        ]
        for f in self._disabled_at_start:
            f.setEnabled(False)

        # Apply custom palettes to intermediate and expert frames
        from . import palettes
        self._pi = palettes.IntermediatePalette()
        self._pe = palettes.ExpertPalette()

        for f in self._intermediate_frames:
            f.setPalette(self._pi.palette)
            f.setAutoFillBackground(True)

        for f in self._expert_frames:
            f.setPalette(self._pe.palette)
            f.setAutoFillBackground(True)

        from . import isolde
        self.isolde = isolde.Isolde(self)

    def delete(self):
        self.isolde._on_close()
        super().delete()

    def _change_experience_level(self):
        iw = self.iw
        exp_index = iw._experience_level_combo_box.currentIndex()
        if (exp_index == 0):
            # Easy. Just hide everything intermediate or expert, and
            # everything belonging to other sim modes
            for f in self._intermediate_frames:
                f.hide()
            for f in self._expert_frames:
                f.hide()
        elif (exp_index == 1):
            for f in self._intermediate_frames:
                f.show()
            for f in self._expert_frames:
                f.hide()
        else:
            for f in self._intermediate_frames:
                f.show()
            for f in self._expert_frames:
                f.show()

        return
Пример #19
0
class Linter(ToolInstance):
    """
    tool to run python linters on files
    the UI has a list of files, an option to choose
    the linter, and a button to run the linter
    results are printed to the log
    """
    def __init__(self, session, name):
        super().__init__(session, name)

        self.tool_window = MainToolWindow(self)
        self.settings = _LinterSettings(self.session, name)

        self._build_ui()

    def _build_ui(self):
        layout = QFormLayout()

        self.table = QTableWidget()
        self.table.setColumnCount(2)
        self.table.setHorizontalHeaderLabels(["file", "remove"])
        self.table.horizontalHeader().setStretchLastSection(False)
        self.table.horizontalHeader().setSectionResizeMode(
            0, QHeaderView.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(
            1, QHeaderView.Fixed)
        self.table.cellClicked.connect(self.table_clicked)
        layout.addRow(self.table)

        self.linters = QComboBox()
        self.linters.addItems([
            "pyflakes",
            "flake8",
            "mypy",
            "pydocstyle",
            "pylint",
        ])
        ndx = self.linters.findText(self.settings.linter, Qt.MatchExactly)
        self.linters.setCurrentIndex(ndx)
        layout.addRow(self.linters)

        lint = QPushButton("run linter")
        lint.clicked.connect(self.run_linter)
        layout.addRow(lint)

        self.add_files(loads(self.settings.files))

        self.tool_window.ui_area.setLayout(layout)

        self.tool_window.manage(None)

    def table_clicked(self, row, column):
        """
        if the last row is clicked, open a file browser and add
        the files to the list
        otherwise, if the 2nd column is clicked (the trash can),
        remove that row
        """
        if row == self.table.rowCount() - 1 or self.table.rowCount() == 1:
            filenames = QFileDialog.getOpenFileNames(
                filter="Python Files (*.py)")
            if filenames[0]:
                self.table.setRowCount(self.table.rowCount() - 1)
                self.add_files(filenames[0])
        elif column == 1:
            self.table.removeRow(row)

    def add_files(self, filenames):
        """add filenames (list(str)) to the table"""
        for f in filenames:
            row = self.table.rowCount()
            self.table.insertRow(row)

            file_item = QTableWidgetItem()
            file_item.setData(Qt.DisplayRole, f)
            self.table.setItem(row, 0, file_item)

            widget_that_lets_me_horizontally_align_an_icon = QWidget()
            widget_layout = QHBoxLayout(
                widget_that_lets_me_horizontally_align_an_icon)
            section_remove = QLabel()
            dim = int(1.5 *
                      section_remove.fontMetrics().boundingRect("Z").height())
            section_remove.setPixmap(
                QIcon(section_remove.style().standardIcon(
                    QStyle.SP_DialogDiscardButton)).pixmap(dim, dim))
            widget_layout.addWidget(section_remove, 0, Qt.AlignHCenter)
            widget_layout.setContentsMargins(0, 0, 0, 0)
            self.table.setCellWidget(
                row, 1, widget_that_lets_me_horizontally_align_an_icon)

        self.add_last_row()

    def add_last_row(self):
        """add the "add files" button to the bottom of the table"""
        row = self.table.rowCount()
        self.table.insertRow(row)

        add_row = QTableWidgetItem()
        add_row.setData(Qt.DisplayRole, "add files")
        self.table.setItem(row, 0, add_row)

    def run_linter(self):
        """execute linter"""
        previous_files = []
        linter = self.linters.currentText()
        for row in range(0, self.table.rowCount() - 1):
            fname = self.table.item(row, 0).text()
            previous_files.append(fname)

            run(self.session, "linter \"%s\" linter %s" % (fname, linter))

        self.settings.files = dumps(previous_files)
        self.settings.linter = linter