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")
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()
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)
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()
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()
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 "")
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()
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)
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()
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
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()
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()
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()
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()
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()
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
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()
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
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