class SubstituentTable(QWidget): def __init__(self, parent=None, singleSelect=False): super().__init__(parent) layout = QGridLayout(self) self.table = QTableWidget() self.table.setColumnCount(3) self.table.setHorizontalHeaderLabels( ['name', 'conformers', 'conf. angle']) self.add_subs() for i in range(0, 3): self.table.resizeColumnToContents(i) self.table.horizontalHeader().setStretchLastSection(False) self.table.horizontalHeader().setSectionResizeMode( 0, QHeaderView.Fixed) self.table.horizontalHeader().setSectionResizeMode( 1, QHeaderView.Fixed) self.table.horizontalHeader().setSectionResizeMode( 2, QHeaderView.Stretch) self.table.setSortingEnabled(True) self.table.setSelectionBehavior(QTableWidget.SelectRows) if singleSelect: self.table.setSelectionMode(QTableWidget.SingleSelection) self.table.setEditTriggers(QTableWidget.NoEditTriggers) self.filterEdit = QLineEdit() self.filterEdit.textChanged.connect(self.apply_filter) self.filterEdit.setClearButtonEnabled(True) self.filter_columns = QComboBox() self.filter_columns.addItem("name") self.filter_columns.addItem("conformers") self.filter_columns.addItem("conf. angle") self.filter_columns.currentTextChanged.connect( self.change_filter_method) self.name_regex_option = QComboBox() self.name_regex_option.addItem("case-insensitive") self.name_regex_option.addItem("case-sensitive") self.name_regex_option.currentTextChanged.connect(self.apply_filter) self.name_regex_option.setVisible( self.filter_columns.currentText() == "name") layout.addWidget(self.table, 0, 0, 1, 4) layout.addWidget(QLabel("filter based on"), 1, 0) layout.addWidget(self.filter_columns, 1, 1) layout.addWidget(self.name_regex_option, 1, 2) layout.addWidget(self.filterEdit, 1, 3) self.change_filter_method("name") def change_filter_method(self, text): if text == "name": self.filterEdit.setToolTip("name regex") self.name_regex_option.setVisible(True) elif text == "conformers": self.filterEdit.setToolTip("number of conformers") self.name_regex_option.setVisible(False) elif text == "conf. angle": self.filterEdit.setToolTip("angle between conformers") self.name_regex_option.setVisible(False) self.apply_filter() def apply_filter(self, *args): text = self.filterEdit.text() if text: if self.filter_columns.currentText() == "name": m = QRegularExpression(text) if m.isValid(): if self.name_regex_option.currentText( ) == "case-insensitive": m.setPatternOptions( QRegularExpression.CaseInsensitiveOption) m.optimize() filter = lambda row_num: m.match( self.table.item(row_num, 0).text()).hasMatch() else: return elif self.filter_columns.currentText() == "conformers": if text.isdigit(): filter = lambda row_num: int( self.table.item(row_num, 1).text()) == int(text) else: filter = lambda row: True elif self.filter_columns.currentText() == "conf. angle": if text.isdigit(): filter = lambda row_num: int( self.table.item(row_num, 2).text()) == int(text) else: filter = lambda row: True else: filter = lambda row: True for i in range(0, self.table.rowCount()): self.table.setRowHidden(i, not filter(i)) def add_subs(self): from AaronTools.substituent import Substituent names = [] for lib in [Substituent.AARON_LIBS, Substituent.BUILTIN]: if not os.path.exists(lib): continue for ring in os.listdir(lib): name, ext = os.path.splitext(ring) if not any(".%s" % x == ext for x in read_types): continue if name in names: continue names.append(name) geom = Geometry( os.path.join(lib, ring), refresh_connected=False, refresh_ranks=False, ) conf_info = re.search(r"CF:(\d+),(\d+)", geom.comment) row = self.table.rowCount() self.table.insertRow(row) self.table.setItem(row, 0, QTableWidgetItem(name)) #the next two items are integers - need to initialize then setData so they sort and display correctly conf_num = QTableWidgetItem() conf_num.setData(Qt.DisplayRole, conf_info.group(1)) self.table.setItem(row, 1, conf_num) conf_angle = QTableWidgetItem() conf_angle.setData(Qt.DisplayRole, conf_info.group(2)) self.table.setItem(row, 2, conf_angle) self.substituent_list = names
class RingTable(QWidget): def __init__(self, parent=None): super().__init__(parent) layout = QGridLayout(self) self.table = QTableWidget() self.table.setColumnCount(1) self.table.setHorizontalHeaderLabels(['name']) self.add_rings() for i in range(0, 1): self.table.resizeColumnToContents(i) self.table.horizontalHeader().setSectionResizeMode( 0, QHeaderView.Stretch) self.table.setSortingEnabled(True) self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setEditTriggers(QTableWidget.NoEditTriggers) self.filterEdit = QLineEdit() self.filterEdit.textChanged.connect(self.apply_filter) self.filterEdit.setClearButtonEnabled(True) self.filter_columns = QComboBox() self.filter_columns.addItem("name") self.filter_columns.currentTextChanged.connect( self.change_filter_method) self.name_regex_option = QComboBox() self.name_regex_option.addItem("case-insensitive") self.name_regex_option.addItem("case-sensitive") self.name_regex_option.currentTextChanged.connect(self.apply_filter) self.name_regex_option.setVisible( self.filter_columns.currentText() == "name") layout.addWidget(self.table, 0, 0, 1, 4) layout.addWidget(QLabel("filter based on"), 1, 0) layout.addWidget(self.filter_columns, 1, 1) layout.addWidget(self.name_regex_option, 1, 2) layout.addWidget(self.filterEdit, 1, 3) self.change_filter_method("name") def change_filter_method(self, text): if text == "name": self.filterEdit.setToolTip("name regex") self.name_regex_option.setVisible(True) self.apply_filter() def apply_filter(self, *args): text = self.filterEdit.text() if text: if self.filter_columns.currentText() == "name": m = QRegularExpression(text) if m.isValid(): if self.name_regex_option.currentText( ) == "case-insensitive": m.setPatternOptions( QRegularExpression.CaseInsensitiveOption) m.optimize() filter = lambda row_num: m.match( self.table.item(row_num, 0).text()).hasMatch() else: return else: filter = lambda row: True for i in range(0, self.table.rowCount()): self.table.setRowHidden(i, not filter(i)) def add_rings(self): from AaronTools.ring import Ring names = [] for lib in [Ring.AARON_LIBS, Ring.BUILTIN]: if not os.path.exists(lib): continue for ring in os.listdir(lib): name, ext = os.path.splitext(ring) if not any(".%s" % x == ext for x in read_types): continue if name in names: continue names.append(name) row = self.table.rowCount() self.table.insertRow(row) self.table.setItem(row, 0, QTableWidgetItem(name)) self.ring_list = names
class LigandTable(QWidget): def __init__( self, parent=None, singleSelect=False, maxDenticity=None, include_substituents=False, ): super().__init__(parent) self._include_substituents = include_substituents layout = QGridLayout(self) self.table = QTableWidget() self.table.setColumnCount(3) self.table.setHorizontalHeaderLabels( ['name', 'denticity', 'coordinating elements']) self.add_ligands(maxDenticity) for i in range(0, 3): self.table.resizeColumnToContents(i) self.table.horizontalHeader().setStretchLastSection(False) self.table.horizontalHeader().setSectionResizeMode( 0, QHeaderView.Fixed) self.table.horizontalHeader().setSectionResizeMode( 1, QHeaderView.Fixed) self.table.horizontalHeader().setSectionResizeMode( 2, QHeaderView.Stretch) self.table.setSortingEnabled(True) self.table.setSelectionBehavior(QTableWidget.SelectRows) if singleSelect: self.table.setSelectionMode(QTableWidget.SingleSelection) self.table.setEditTriggers(QTableWidget.NoEditTriggers) self.filterEdit = QLineEdit() self.filterEdit.textChanged.connect(self.apply_filter) self.filterEdit.setClearButtonEnabled(True) self.filter_columns = QComboBox() self.filter_columns.addItem("name") self.filter_columns.addItem("denticity") self.filter_columns.addItem("coordinating elements") self.filter_columns.currentTextChanged.connect( self.change_filter_method) self.name_regex_option = QComboBox() self.name_regex_option.addItem("case-insensitive") self.name_regex_option.addItem("case-sensitive") self.name_regex_option.currentTextChanged.connect(self.apply_filter) self.name_regex_option.setVisible( self.filter_columns.currentText() == "name") self.coordinating_elements_method = QComboBox() self.coordinating_elements_method.addItem("exactly") self.coordinating_elements_method.addItem("at least") self.coordinating_elements_method.currentTextChanged.connect( self.apply_filter) self.coordinating_elements_method.setVisible( self.filter_columns.currentText() == "coordinating elements") layout.addWidget(self.table, 0, 0, 1, 4) layout.addWidget(QLabel("filter based on"), 1, 0) layout.addWidget(self.filter_columns, 1, 1) layout.addWidget(self.coordinating_elements_method, 1, 2) layout.addWidget(self.name_regex_option, 1, 2) layout.addWidget(self.filterEdit, 1, 3) self.change_filter_method("name") def add_ligands(self, maxDenticity=None): from AaronTools.component import Component names = [] for lib in [Component.AARON_LIBS, Component.BUILTIN]: if not os.path.exists(lib): continue for lig in os.listdir(lib): name, ext = os.path.splitext(lig) if not any(".%s" % x == ext for x in read_types): continue if name in names: continue names.append(name) geom = Geometry( os.path.join(lib, lig), refresh_connected=False, refresh_ranks=False, ) key_atoms = [geom.atoms[i] for i in geom.other["key_atoms"]] if maxDenticity and len(key_atoms) > maxDenticity: continue row = self.table.rowCount() self.table.insertRow(row) self.table.setItem(row, 0, QTableWidgetItem(name)) #this is an integer, so I need to initialize it then set the data denticity = QTableWidgetItem() denticity.setData(Qt.DisplayRole, len(key_atoms)) self.table.setItem(row, 1, denticity) self.table.setItem( row, 2, QTableWidgetItem(", ".join( sorted([atom.element for atom in key_atoms])))) if self._include_substituents: from AaronTools.substituent import Substituent for lib in [Substituent.AARON_LIBS, Substituent.BUILTIN]: if not os.path.exists(lib): continue for sub in os.listdir(lib): name, ext = os.path.splitext(sub) if not any(".%s" % x == ext for x in read_types): continue if name in names: continue names.append(name) geom = Geometry( os.path.join(lib, sub), refresh_connected=False, refresh_ranks=False, ) key_atoms = [geom.atoms[0]] if maxDenticity and len(key_atoms) > maxDenticity: continue row = self.table.rowCount() self.table.insertRow(row) self.table.setItem(row, 0, QTableWidgetItem(name)) #this is an integer, so I need to initialize it then set the data denticity = QTableWidgetItem() denticity.setData(Qt.DisplayRole, len(key_atoms)) self.table.setItem(row, 1, denticity) self.table.setItem( row, 2, QTableWidgetItem(", ".join( sorted([atom.element for atom in key_atoms])))) self.ligand_list = names def change_filter_method(self, text): if text == "coordinating elements": self.filterEdit.setToolTip("comma and/or space delimited elements") self.coordinating_elements_method.setVisible(True) self.name_regex_option.setVisible(False) elif text == "name": self.filterEdit.setToolTip("name regex") self.coordinating_elements_method.setVisible(False) self.name_regex_option.setVisible(True) elif text == "denticity": self.filterEdit.setToolTip("number of key atoms") self.coordinating_elements_method.setVisible(False) self.name_regex_option.setVisible(False) self.apply_filter() def apply_filter(self, *args): text = self.filterEdit.text() if text: if self.filter_columns.currentText() == "name": m = QRegularExpression(text) if m.isValid(): if self.name_regex_option.currentText( ) == "case-insensitive": m.setPatternOptions( QRegularExpression.CaseInsensitiveOption) m.optimize() filter = lambda row_num: m.match( self.table.item(row_num, 0).text()).hasMatch() else: return elif self.filter_columns.currentText() == "denticity": if text.isdigit(): filter = lambda row_num: int( self.table.item(row_num, 1).text()) == int(text) else: filter = lambda row: True elif self.filter_columns.currentText() == "coordinating elements": method = self.coordinating_elements_method.currentText() def filter(row_num): row_key_atoms = [ item.strip() for item in self.table.item( row_num, 2).text().split(',') ] search_atoms = [] for item in text.split(): for ele in item.split(','): if ele.strip() != "": search_atoms.append(ele) if method == "exactly": if all([row_key_atoms.count(element) == search_atoms.count(element) for element in set(search_atoms)]) and \ all([row_key_atoms.count(element) == search_atoms.count(element) for element in set(row_key_atoms)]): return True else: return False elif method == "at least": if all([ row_key_atoms.count(element) >= search_atoms.count(element) for element in set(search_atoms) ]): return True else: return False else: filter = lambda row: True for i in range(0, self.table.rowCount()): self.table.setRowHidden(i, not filter(i))
class ConeAngle(ToolInstance): help = "https://github.com/QChASM/SEQCROW/wiki/Cone-Angle-Tool" def __init__(self, session, name): super().__init__(session, name) from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) self.settings = _ConeAngleSettings(self.session, name) self.ligands = dict() self._build_ui() def _build_ui(self): layout = QFormLayout() self.cone_option = QComboBox() self.cone_option.addItems(["Tolman (Unsymmetrical)", "Exact"]) ndx = self.cone_option.findText(self.settings.cone_option, Qt.MatchExactly) self.cone_option.setCurrentIndex(ndx) layout.addRow("method:", self.cone_option) 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) layout.addRow("radii:", self.radii_option) self.display_cone = QCheckBox() self.display_cone.setChecked(self.settings.display_cone) layout.addRow("show cone:", self.display_cone) self.display_radii = QCheckBox() self.display_radii.setChecked(self.settings.display_radii) layout.addRow("show radii:", self.display_radii) set_ligand_button = QPushButton("set ligand to current selection") set_ligand_button.clicked.connect(self.set_ligand) layout.addRow(set_ligand_button) self.set_ligand_button = set_ligand_button calc_cone_button = QPushButton( "calculate cone angle for ligand on selected center") calc_cone_button.clicked.connect(self.calc_cone) layout.addRow(calc_cone_button) self.calc_cone_button = calc_cone_button remove_cone_button = QPushButton("remove cone visualizations") remove_cone_button.clicked.connect(self.del_cone) layout.addRow(remove_cone_button) self.remove_cone_button = remove_cone_button self.table = QTableWidget() self.table.setColumnCount(3) self.table.setHorizontalHeaderLabels([ 'model', 'center', 'cone angle (°)', ]) 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) layout.addRow(self.table) menu = QMenuBar() export = menu.addMenu("&Export") clear = QAction("Clear data table", self.tool_window.ui_area) clear.triggered.connect(self.clear_table) export.addAction(clear) 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) 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 clear_table(self): are_you_sure = QMessageBox.question( None, "Clear table?", "Are you sure you want to clear the data table?", ) if are_you_sure != QMessageBox.Yes: return self.table.setRowCount(0) 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_atom", "cone_angle"]) 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_cone(self): for model in self.session.models.list(type=Generic3DModel): if model.name.startswith("Cone angle"): model.delete() def set_ligand(self, *args): self.ligands = {} for atom in selected_atoms(self.session): if atom.structure not in self.ligands: self.ligands[atom.structure] = [] self.ligands[atom.structure].append(atom) self.session.logger.status("set ligand to current selection") def calc_cone(self, *args): self.settings.cone_option = self.cone_option.currentText() self.settings.radii = self.radii_option.currentText() self.settings.display_radii = self.display_radii.checkState( ) == Qt.Checked self.settings.display_cone = self.display_cone.checkState( ) == Qt.Checked if self.cone_option.currentText() == "Tolman (Unsymmetrical)": method = "tolman" else: method = self.cone_option.currentText() radii = self.radii_option.currentText() return_cones = self.display_cone.checkState() == Qt.Checked display_radii = self.display_radii.checkState() == Qt.Checked # self.table.setRowCount(0) for center_atom in selected_atoms(self.session): rescol = ResidueCollection(center_atom.structure) at_center = rescol.find_exact(AtomSpec(center_atom.atomspec))[0] if center_atom.structure in self.ligands: comp = Component( rescol.find([ AtomSpec(atom.atomspec) for atom in self.ligands[center_atom.structure] ]), to_center=rescol.find_exact(AtomSpec( center_atom.atomspec)), key_atoms=rescol.find(BondedTo(at_center)), ) else: comp = Component( rescol.find(NotAny(at_center)), to_center=rescol.find_exact(AtomSpec( center_atom.atomspec)), key_atoms=rescol.find(BondedTo(at_center)), ) cone_angle = comp.cone_angle( center=rescol.find(AtomSpec(center_atom.atomspec)), method=method, radii=radii, return_cones=return_cones, ) if return_cones: cone_angle, cones = cone_angle s = ".transparency 0.5\n" for cone in cones: apex, base, radius = cone s += ".cone %6.3f %6.3f %6.3f %6.3f %6.3f %6.3f %.3f open\n" % ( *apex, *base, radius) stream = BytesIO(bytes(s, "utf-8")) bild_obj, status = read_bild(self.session, stream, "Cone angle %s" % center_atom) self.session.models.add(bild_obj, parent=center_atom.structure) if display_radii: s = ".note radii\n" s += ".transparency 75\n" color = None for atom in comp.atoms: chix_atom = atom.chix_atom if radii.lower() == "umn": r = VDW_RADII[chix_atom.element.name] elif radii.lower() == "bondi": r = BONDI_RADII[chix_atom.element.name] if color is None or chix_atom.color != color: color = chix_atom.color rgb = [x / 255. for x in chix_atom.color] rgb.pop(-1) s += ".color %f %f %f\n" % tuple(rgb) s += ".sphere %f %f %f %f\n" % (*chix_atom.coord, r) stream = BytesIO(bytes(s, "utf-8")) bild_obj, status = read_bild(self.session, stream, "Cone angle radii") self.session.models.add(bild_obj, parent=center_atom.structure) row = self.table.rowCount() self.table.insertRow(row) name = QTableWidgetItem() name.setData(Qt.DisplayRole, center_atom.structure.name) self.table.setItem(row, 0, name) center = QTableWidgetItem() center.setData(Qt.DisplayRole, center_atom.atomspec) self.table.setItem(row, 1, center) ca = QTableWidgetItem() ca.setData(Qt.DisplayRole, "%.2f" % cone_angle) self.table.setItem(row, 2, ca) self.table.resizeColumnToContents(0) self.table.resizeColumnToContents(1) self.table.resizeColumnToContents(2)
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 LigandSterimol(ToolInstance): help = "https://github.com/QChASM/SEQCROW/wiki/Ligand-Sterimol-Tool" SESSION_ENDURING = False SESSION_SAVE = False def __init__(self, session, name): super().__init__(session, name) from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) self.settings = _SterimolSettings(self.session, name) self._build_ui() def _build_ui(self): layout = QFormLayout() 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) layout.addRow("radii:", self.radii_option) self.L_option = QComboBox() self.L_option.addItems([ "to centroid of coordinating atoms", "bisect angle between coordinating atoms" ]) ndx = self.L_option.findText(self.settings.L_option, Qt.MatchExactly) self.L_option.setCurrentIndex(ndx) layout.addRow("L axis:", self.L_option) self.sterimol2vec = QGroupBox("Sterimol2Vec") sterimol2vec_layout = QFormLayout(self.sterimol2vec) self.at_L = QDoubleSpinBox() self.at_L.setRange(-10, 30) self.at_L.setDecimals(2) self.at_L.setSingleStep(0.25) self.at_L.setValue(self.settings.at_L) sterimol2vec_layout.addRow("L value:", self.at_L) layout.addRow(self.sterimol2vec) self.sterimol2vec.setCheckable(True) self.sterimol2vec.toggled.connect(lambda x: self.at_L.setEnabled(x)) self.sterimol2vec.setChecked(self.settings.sterimol2vec) self.display_vectors = QCheckBox() self.display_vectors.setChecked(self.settings.display_vectors) layout.addRow("show vectors:", self.display_vectors) self.display_radii = QCheckBox() self.display_radii.setChecked(self.settings.display_radii) layout.addRow("show radii:", self.display_radii) calc_sterimol_button = QPushButton( "calculate parameters for selected ligands") calc_sterimol_button.clicked.connect(self.calc_sterimol) layout.addRow(calc_sterimol_button) self.calc_sterimol_button = calc_sterimol_button remove_sterimol_button = QPushButton("remove Sterimol visualizations") remove_sterimol_button.clicked.connect(self.del_sterimol) layout.addRow(remove_sterimol_button) self.remove_sterimol_button = remove_sterimol_button self.table = QTableWidget() self.table.setColumnCount(8) self.table.setHorizontalHeaderLabels([ 'model', 'coord. atoms', 'B\u2081', 'B\u2082', 'B\u2083', 'B\u2084', 'B\u2085', 'L', ]) self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setEditTriggers(QTableWidget.NoEditTriggers) self.table.resizeColumnToContents(0) self.table.resizeColumnToContents(1) self.table.resizeColumnToContents(2) self.table.resizeColumnToContents(3) self.table.resizeColumnToContents(4) self.table.resizeColumnToContents(5) self.table.resizeColumnToContents(6) self.table.resizeColumnToContents(7) self.table.horizontalHeader().setSectionResizeMode( 2, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode( 3, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode( 4, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode( 5, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode( 6, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode( 7, QHeaderView.Stretch) layout.addRow(self.table) menu = QMenuBar() export = menu.addMenu("&Export") clear = QAction("Clear data table", self.tool_window.ui_area) clear.triggered.connect(self.clear_table) export.addAction(clear) 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) 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 clear_table(self): are_you_sure = QMessageBox.question( None, "Clear table?", "Are you sure you want to clear the data table?", ) if are_you_sure != QMessageBox.Yes: return self.table.setRowCount(0) def calc_sterimol(self, *args): self.settings.radii = self.radii_option.currentText() self.settings.display_radii = self.display_radii.checkState( ) == Qt.Checked self.settings.display_vectors = self.display_vectors.checkState( ) == Qt.Checked self.settings.at_L = self.at_L.value() self.settings.sterimol2vec = self.sterimol2vec.isChecked() self.settings.L_option = self.L_option.currentText() targets, neighbors, datas = sterimol_cmd( self.session, selected_atoms(self.session), radii=self.radii_option.currentText(), showVectors=self.display_vectors.checkState() == Qt.Checked, showRadii=self.display_radii.checkState() == Qt.Checked, return_values=True, at_L=self.at_L.value() if self.sterimol2vec.isChecked() else None, bisect_L=self.L_option.currentText() == "bisect angle between coordinating atoms", ) if len(targets) == 0: return 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 = ";" # self.table.setRowCount(0) for t, b, data in zip(targets, neighbors, datas): row = self.table.rowCount() self.table.insertRow(row) targ = QTableWidgetItem() targ.setData(Qt.DisplayRole, t) self.table.setItem(row, 0, targ) neigh = QTableWidgetItem() neigh.setData(Qt.DisplayRole, delim.join(b)) self.table.setItem(row, 1, neigh) l = np.linalg.norm(data["L"][1] - data["L"][0]) b1 = np.linalg.norm(data["B1"][1] - data["B1"][0]) b2 = np.linalg.norm(data["B2"][1] - data["B2"][0]) b3 = np.linalg.norm(data["B3"][1] - data["B3"][0]) b4 = np.linalg.norm(data["B4"][1] - data["B4"][0]) b5 = np.linalg.norm(data["B5"][1] - data["B5"][0]) li = QTableWidgetItem() li.setData(Qt.DisplayRole, "%.2f" % l) self.table.setItem(row, 7, li) b1i = QTableWidgetItem() b1i.setData(Qt.DisplayRole, "%.2f" % b1) self.table.setItem(row, 2, b1i) b2i = QTableWidgetItem() b2i.setData(Qt.DisplayRole, "%.2f" % b2) self.table.setItem(row, 3, b2i) b3i = QTableWidgetItem() b3i.setData(Qt.DisplayRole, "%.2f" % b3) self.table.setItem(row, 4, b3i) b4i = QTableWidgetItem() b4i.setData(Qt.DisplayRole, "%.2f" % b4) self.table.setItem(row, 5, b4i) b5i = QTableWidgetItem() b5i.setData(Qt.DisplayRole, "%.2f" % b5) self.table.setItem(row, 6, b5i) for i in range(0, 7): if i == 1: continue self.table.resizeColumnToContents(i) 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([ "substituent_atom", "bonded_atom", "B1", "B2", "B3", "B4", "B5", "L" ]) 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, 8)] ]) 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_sterimol(self): for model in self.session.models.list(type=Generic3DModel): if model.name.startswith("Sterimol"): 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 PercentVolumeBuried(ToolInstance): help = "https://github.com/QChASM/SEQCROW/wiki/Buried-Volume-Tool" SESSION_ENDURING = False SESSION_SAVE = False 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) settings_widget = QWidget() settings_layout = QFormLayout(settings_widget) steric_map_widget = QWidget() steric_layout = QFormLayout(steric_map_widget) cutout_widget = QWidget() vol_cutout_layout = QFormLayout(cutout_widget) layout.addWidget(tabs) tabs.addTab(calc_widget, "calculation") tabs.addTab(settings_widget, "settings") tabs.addTab(steric_map_widget, "steric map") tabs.addTab(cutout_widget, "volume cutout") 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) settings_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) settings_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.) settings_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) settings_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) settings_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) settings_layout.addRow(mc_widget) if self.settings.method == "Lebedev": mc_widget.setVisible(False) elif self.settings.method == "Monte-Carlo": leb_widget.setVisible(False) self.report_component = QComboBox() self.report_component.addItems(["total", "quadrants", "octants"]) ndx = self.report_component.findText(self.settings.report_component, Qt.MatchExactly) self.report_component.setCurrentIndex(ndx) settings_layout.addRow("report volume:", self.report_component) self.use_scene = QCheckBox() self.use_scene.setChecked(self.settings.use_scene) self.use_scene.setToolTip( "quadrants/octants will use the orientation the molecule is displayed in" ) settings_layout.addRow("use display orientation:", self.use_scene) 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) 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_layout.addRow("create steric map:", self.steric_map) 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_layout.addRow("number of points:", self.num_pts) self.include_vbur = QCheckBox() self.include_vbur.setChecked(self.settings.include_vbur) steric_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_layout.addRow("map shape:", self.map_shape) self.auto_minmax = QCheckBox() self.auto_minmax.setChecked(self.settings.auto_minmax) steric_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_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_layout.addRow("maximum value:", self.map_max) 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())) 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") vol_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" ) vol_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) vol_cutout_layout.addRow("intersection density:", self.intersection_scale) self.cutout_labels = QComboBox() self.cutout_labels.addItems(["none", "quadrants", "octants"]) ndx = self.cutout_labels.findText(self.settings.cutout_labels, Qt.MatchExactly) self.cutout_labels.setCurrentIndex(ndx) vol_cutout_layout.addRow("label sections:", self.cutout_labels) self.point_spacing.setEnabled(self.settings.display_cutout != "no") self.intersection_scale.setEnabled( self.settings.display_cutout != "no") self.cutout_labels.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")) self.display_cutout.currentTextChanged.connect( lambda text, widget=self.cutout_labels: widget.setEnabled(text != "no")) 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) vol_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( 0, QHeaderView.Interactive) self.table.horizontalHeader().setSectionResizeMode( 1, QHeaderView.Interactive) self.table.horizontalHeader().setSectionResizeMode( 2, QHeaderView.Stretch) calc_layout.addRow(self.table) menu = QMenuBar() export = menu.addMenu("&Export") clear = QAction("Clear data table", self.tool_window.ui_area) clear.triggered.connect(self.clear_table) export.addAction(clear) 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 clear_table(self): are_you_sure = QMessageBox.question( None, "Clear table?", "Are you sure you want to clear the data table?", ) if are_you_sure != QMessageBox.Yes: return self.table.setRowCount(0) 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["useScene"] = 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 report_component = self.report_component.currentText() self.settings.report_component = report_component args["reportComponent"] = report_component 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 != "no": 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 cutout_labels = self.cutout_labels.currentText() self.settings.cutout_labels = cutout_labels args["labels"] = cutout_labels 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.name) self.table.setItem(row, 0, m) c = QTableWidgetItem() c.setData(Qt.DisplayRole, cent) self.table.setItem(row, 1, c) v = QTableWidgetItem() if report_component == "octants": v.setData(Qt.DisplayRole, ",".join(["%.1f" % x for x in vbur])) elif report_component == "quadrants": v.setData( Qt.DisplayRole, ",".join("%.1f" % x for x in [ vbur[0] + vbur[7], vbur[1] + vbur[6], vbur[2] + vbur[5], vbur[3] + vbur[4], ])) else: if hasattr(vbur, "__iter__"): v.setData(Qt.DisplayRole, "%.1f" % sum(vbur)) else: v.setData(Qt.DisplayRole, "%.1f" % 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.name, 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.name) self.table.setItem(row, 0, m) c = QTableWidgetItem() c.setData(Qt.DisplayRole, cent) self.table.setItem(row, 1, c) v = QTableWidgetItem() if report_component == "octants": v.setData(Qt.DisplayRole, ",".join(["%.1f" % x for x in vbur])) elif report_component == "quadrants": v.setData( Qt.DisplayRole, ",".join("%.1f" % x for x in [ vbur[0] + vbur[7], vbur[1] + vbur[6], vbur[2] + vbur[5], vbur[3] + vbur[4], ])) else: if hasattr(vbur, "__iter__"): v.setData(Qt.DisplayRole, "%.1f" % sum(vbur)) else: v.setData(Qt.DisplayRole, "%.1f" % vbur) v.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.table.setItem(row, 2, v) self.table.resizeColumnToContents(1) self.table.resizeColumnToContents(2) 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 "")