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 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 PrecisionRotate(ToolInstance): help = "https://github.com/QChASM/SEQCROW/wiki/Rotate-Tool" SESSION_ENDURING = False SESSION_SAVE = False def __init__(self, session, name): super().__init__(session, name) self.tool_window = MainToolWindow(self) self.settings = _PrecisionRotateSettings(session, name) self.bonds = {} self.bond_centers = {} self.groups = {} self.perpendiculars = {} self.perp_centers = {} self.manual_center = {} self._build_ui() self._show_rot_vec = self.session.triggers.add_handler( SELECTION_CHANGED, self.show_rot_vec) global_triggers = get_triggers() self._changes = global_triggers.add_handler("changes done", self.show_rot_vec) self.show_rot_vec() def _build_ui(self): layout = QGridLayout() layout.addWidget(QLabel("center of rotation:"), 0, 0, 1, 1, Qt.AlignLeft | Qt.AlignVCenter) self.cor_button = QComboBox() self.cor_button.addItems( ["automatic", "select atoms", "view's center of rotation"]) layout.addWidget(self.cor_button, 0, 1, 1, 1, Qt.AlignTop) self.set_cor_selection = QPushButton("set selection") self.cor_button.currentTextChanged.connect( lambda t, widget=self.set_cor_selection: widget.setEnabled( t == "select atoms")) self.set_cor_selection.clicked.connect(self.manual_cor) layout.addWidget(self.set_cor_selection, 0, 2, 1, 1, Qt.AlignTop) self.set_cor_selection.setEnabled(False) layout.addWidget(QLabel("rotation vector:"), 1, 0, 1, 1, Qt.AlignLeft | Qt.AlignVCenter) self.vector_option = QComboBox() self.vector_option.addItems([ "axis", "view axis", "bond", "perpendicular to plane", "centroid of atoms", "custom" ]) layout.addWidget(self.vector_option, 1, 1, 1, 1, Qt.AlignVCenter) vector = QWidget() vector.setToolTip("vector will be normalized before rotating") vector_layout = QHBoxLayout(vector) vector_layout.setContentsMargins(0, 0, 0, 0) self.vector_x = QDoubleSpinBox() self.vector_y = QDoubleSpinBox() self.vector_z = QDoubleSpinBox() self.vector_z.setValue(1.0) for c, t in zip([self.vector_x, self.vector_y, self.vector_z], [" x", " y", " z"]): c.setSingleStep(0.01) c.setRange(-100, 100) # c.setSuffix(t) c.valueChanged.connect(self.show_rot_vec) vector_layout.addWidget(c) layout.addWidget(vector, 1, 2, 1, 1, Qt.AlignTop) vector.setVisible(self.vector_option.currentText() == "custom") self.vector_option.currentTextChanged.connect( lambda text, widget=vector: widget.setVisible(text == "custom")) self.view_axis = QComboBox() self.view_axis.addItems(["z", "y", "x"]) layout.addWidget(self.view_axis, 1, 2, 1, 1, Qt.AlignTop) self.view_axis.setVisible( self.vector_option.currentText() == "view axis") self.vector_option.currentTextChanged.connect( lambda text, widget=self.view_axis: widget.setVisible(text == "view axis")) self.axis = QComboBox() self.axis.addItems(["z", "y", "x"]) layout.addWidget(self.axis, 1, 2, 1, 1, Qt.AlignTop) self.axis.setVisible(self.vector_option.currentText() == "axis") self.vector_option.currentTextChanged.connect( lambda text, widget=self.axis: widget.setVisible(text == "axis")) self.bond_button = QPushButton("set selected bond") self.bond_button.clicked.connect(self.set_bonds) layout.addWidget(self.bond_button, 1, 2, 1, 1, Qt.AlignTop) self.bond_button.setVisible(self.vector_option.currentText() == "bond") self.vector_option.currentTextChanged.connect( lambda text, widget=self.bond_button: widget.setVisible(text == "bond")) self.perp_button = QPushButton("set selected atoms") self.perp_button.clicked.connect(self.set_perpendicular) layout.addWidget(self.perp_button, 1, 2, 1, 1, Qt.AlignTop) self.perp_button.setVisible( self.vector_option.currentText() == "perpendicular to plane") self.vector_option.currentTextChanged.connect( lambda text, widget=self.perp_button: widget.setVisible( text == "perpendicular to plane")) self.group_button = QPushButton("set selected atoms") self.group_button.clicked.connect(self.set_group) layout.addWidget(self.group_button, 1, 2, 1, 1, Qt.AlignTop) self.group_button.setVisible( self.vector_option.currentText() == "centroid of atoms") self.vector_option.currentTextChanged.connect( lambda text, widget=self.group_button: widget.setVisible( text == "centroid of atoms")) layout.addWidget(QLabel("angle:"), 2, 0, 1, 1, Qt.AlignLeft | Qt.AlignVCenter) self.angle = QDoubleSpinBox() self.angle.setRange(-360, 360) self.angle.setSingleStep(5) self.angle.setSuffix("°") layout.addWidget(self.angle, 2, 1, 1, 1, Qt.AlignLeft | Qt.AlignVCenter) layout.addWidget(QLabel("preview rotation axis:"), 3, 0, 1, 1, Qt.AlignLeft | Qt.AlignVCenter) self.display_rot_vec = QCheckBox() self.display_rot_vec.setCheckState(Qt.Checked) self.display_rot_vec.stateChanged.connect(self.show_rot_vec) layout.addWidget(self.display_rot_vec, 3, 1, 1, 1, Qt.AlignLeft | Qt.AlignVCenter) rotate_button = QPushButton("rotate selected atoms") rotate_button.clicked.connect(self.do_rotate) layout.addWidget(rotate_button, 4, 0, 1, 3, Qt.AlignTop) self.rotate_button = rotate_button self.status_bar = QStatusBar() self.status_bar.setSizeGripEnabled(False) layout.addWidget(self.status_bar, 5, 0, 1, 3, Qt.AlignTop) self.vector_option.currentTextChanged.connect(self.show_auto_status) self.cor_button.currentIndexChanged.connect( lambda *args: self.show_auto_status("select atoms")) self.cor_button.currentIndexChanged.connect(self.show_rot_vec) self.set_cor_selection.clicked.connect(self.show_rot_vec) self.vector_option.currentIndexChanged.connect(self.show_rot_vec) self.axis.currentIndexChanged.connect(self.show_rot_vec) self.view_axis.currentIndexChanged.connect(self.show_rot_vec) self.bond_button.clicked.connect(self.show_rot_vec) self.perp_button.clicked.connect(self.show_rot_vec) self.group_button.clicked.connect(self.show_rot_vec) layout.setRowStretch(0, 0) layout.setRowStretch(1, 0) layout.setRowStretch(2, 0) layout.setRowStretch(3, 0) layout.setRowStretch(4, 0) layout.setRowStretch(5, 1) layout.setColumnStretch(0, 0) layout.setColumnStretch(1, 1) layout.setColumnStretch(2, 1) self.tool_window.ui_area.setLayout(layout) self.tool_window.manage(None) def manual_cor(self, *args): selection = selected_atoms(self.session) models = {} for atom in selection: if atom.structure not in models: models[atom.structure] = [atom] else: models[atom.structure].append(atom) self.manual_center = {} for model in models: atoms = models[model] coords = np.array([atom.coord for atom in atoms]) self.manual_center[model] = np.mean(coords, axis=0) def show_auto_status(self, text): if self.cor_button.currentText() == "automatic": if text == "bond": self.status_bar.showMessage( "center set to one of the bonded atoms") elif text == "perpendicular to plane": self.status_bar.showMessage("center set to centroid of atoms") else: self.status_bar.showMessage( "center set to centroid of rotating atoms") elif self.cor_button.currentText() == "select atoms": self.status_bar.showMessage( "center set to centroid of specified atoms") else: self.status_bar.showMessage( "center set to view's center of rotation") def set_bonds(self, *args): bonds = selected_bonds(self.session) if len(bonds) == 0: self.session.logger.error("no bonds selected") return models = [bond.structure for bond in bonds] if any(models.count(m) > 1 for m in models): self.session.logger.error( "multiple bonds selected on the same structure") return self.bonds = { model: (bond.atoms[0].coord - bond.atoms[1].coord) for model, bond in zip(models, bonds) } self.bond_centers = { model: bond.atoms[1].coord for model, bond in zip(models, bonds) } def set_perpendicular(self, *args): atoms = selected_atoms(self.session) if len(atoms) == 0: self.session.logger.error("no atoms selected") return self.perpendiculars = {} self.perp_centers = {} models = set(atom.structure for atom in atoms) for model in models: atom_coords = [] for atom in atoms: if atom.structure is model: atom_coords.append(atom.coord) if len(atom_coords) < 3: self.session.logger.error("fewer than 3 atoms selected on %s" % model.atomspec) continue xyz = np.array(atom_coords) xyz -= np.mean(atom_coords, axis=0) R = np.dot(xyz.T, xyz) u, s, vh = np.linalg.svd(R, compute_uv=True) vector = u[:, -1] self.perpendiculars[model] = vector self.perp_centers[model] = np.mean(atom_coords, axis=0) def set_group(self, *args): atoms = selected_atoms(self.session) if len(atoms) == 0: self.session.logger.error("no atoms selected") return self.groups = {} models = set(atom.structure for atom in atoms) for model in models: atom_coords = [] for atom in atoms: if atom.structure is model: atom_coords.append(atom.coord) self.groups[model] = np.mean(atom_coords, axis=0) def do_rotate(self, *args): selection = selected_atoms(self.session) models = {} for atom in selection: if atom.structure not in models: models[atom.structure] = [atom] else: models[atom.structure].append(atom) if len(models.keys()) == 0: return if self.vector_option.currentText() == "axis": if self.axis.currentText() == "z": vector = np.array([0., 0., 1.]) elif self.axis.currentText() == "y": vector = np.array([0., 1., 0.]) elif self.axis.currentText() == "x": vector = np.array([1., 0., 0.]) elif self.vector_option.currentText() == "view axis": if self.view_axis.currentText() == "z": vector = self.session.view.camera.get_position().axes()[2] elif self.view_axis.currentText() == "y": vector = self.session.view.camera.get_position().axes()[1] elif self.view_axis.currentText() == "x": vector = self.session.view.camera.get_position().axes()[0] elif self.vector_option.currentText() == "bond": vector = self.bonds elif self.vector_option.currentText() == "perpendicular to plane": vector = self.perpendiculars elif self.vector_option.currentText() == "centroid of atoms": vector = self.groups elif self.vector_option.currentText() == "custom": x = self.vector_x.value() y = self.vector_y.value() z = self.vector_z.value() vector = np.array([x, y, z]) angle = np.deg2rad(self.angle.value()) center = {} for model in models: atoms = models[model] coords = np.array([atom.coord for atom in atoms]) center[model] = np.mean(coords, axis=0) if self.cor_button.currentText() == "automatic": if self.vector_option.currentText() == "perpendicular to plane": center = self.perp_centers elif self.vector_option.currentText() == "bond": center = self.bond_centers elif self.cor_button.currentText() == "select atoms": center = self.manual_center else: center = self.session.main_view.center_of_rotation for model in models: if isinstance(vector, dict): if model not in vector.keys(): continue else: v = vector[model] else: v = vector if isinstance(center, dict): if model not in center.keys(): continue else: c = center[model] else: c = center if self.vector_option.currentText( ) == "centroid of atoms" and self.cor_button.currentText( ) != "automatic": v = v - c v = v / np.linalg.norm(v) q = np.hstack(([np.cos(angle / 2)], v * np.sin(angle / 2))) q /= np.linalg.norm(q) qs = q[0] qv = q[1:] xyz = np.array([a.coord for a in models[model]]) xyz -= c xprod = np.cross(qv, xyz) qs_xprod = 2 * qs * xprod qv_xprod = 2 * np.cross(qv, xprod) xyz += qs_xprod + qv_xprod + c for t, coord in zip(models[model], xyz): t.coord = coord def show_rot_vec(self, *args): for model in self.session.models.list(type=Generic3DModel): if model.name == "rotation vector": model.delete() if self.display_rot_vec.checkState() == Qt.Unchecked: return selection = selected_atoms(self.session) if len(selection) == 0: return models = {} for atom in selection: if atom.structure not in models: models[atom.structure] = [atom] else: models[atom.structure].append(atom) if len(models.keys()) == 0: return if self.vector_option.currentText() == "axis": if self.axis.currentText() == "z": vector = np.array([0., 0., 1.]) elif self.axis.currentText() == "y": vector = np.array([0., 1., 0.]) elif self.axis.currentText() == "x": vector = np.array([1., 0., 0.]) elif self.vector_option.currentText() == "view axis": if self.view_axis.currentText() == "z": vector = self.session.view.camera.get_position().axes()[2] elif self.view_axis.currentText() == "y": vector = self.session.view.camera.get_position().axes()[1] elif self.view_axis.currentText() == "x": vector = self.session.view.camera.get_position().axes()[0] elif self.vector_option.currentText() == "bond": vector = self.bonds elif self.vector_option.currentText() == "perpendicular to plane": vector = self.perpendiculars elif self.vector_option.currentText() == "centroid of atoms": vector = self.groups elif self.vector_option.currentText() == "custom": x = self.vector_x.value() y = self.vector_y.value() z = self.vector_z.value() vector = np.array([x, y, z]) center = {} for model in models: atoms = models[model] coords = np.array([atom.coord for atom in atoms]) center[model] = np.mean(coords, axis=0) if self.cor_button.currentText() == "automatic": if self.vector_option.currentText() == "perpendicular to plane": center = self.perp_centers elif self.vector_option.currentText() == "bond": center = self.bond_centers elif self.cor_button.currentText() == "select atoms": center = self.manual_center else: center = self.session.main_view.center_of_rotation for model in models: if isinstance(vector, dict): if model not in vector.keys(): continue else: v = vector[model] else: v = vector if isinstance(center, dict): if model not in center.keys(): continue else: c = center[model] else: c = center if self.vector_option.currentText( ) == "centroid of atoms" and self.cor_button.currentText( ) != "automatic": v = v - c if np.linalg.norm(v) == 0: continue residues = [] for atom in models[model]: if atom.residue not in residues: residues.append(atom.residue) v_c = c + v s = ".color red\n" s += ".arrow %10.6f %10.6f %10.6f %10.6f %10.6f %10.6f 0.2 0.4 0.7\n" % ( *c, *v_c) stream = BytesIO(bytes(s, 'utf-8')) bild_obj, status = read_bild(self.session, stream, "rotation vector") self.session.models.add(bild_obj, parent=model) def delete(self): self.session.triggers.remove_handler(self._show_rot_vec) global_triggers = get_triggers() global_triggers.remove_handler(self._changes) for model in self.session.models.list(type=Generic3DModel): if model.name == "rotation vector": model.delete() return super().delete() def close(self): self.session.triggers.remove_handler(self._show_rot_vec) global_triggers = get_triggers() global_triggers.remove_handler(self._changes) for model in self.session.models.list(type=Generic3DModel): if model.name == "rotation vector": model.delete() return super().close()