class PartitionEditor(QDialog, Ui_Partition_editor, object): ''' 局限性:三联密码子位置必须紧挨着,不能散布 ''' guiCloseSig = pyqtSignal(str) def __init__(self, mode="PF2", parent=None): self.factory = Factory() self.thisPath = self.factory.thisPath super(PartitionEditor, self).__init__(parent) # 开始装载样式表 with open(self.thisPath + os.sep + 'style.qss', encoding="utf-8", errors='ignore') as f: self.qss_file = f.read() self.setStyleSheet(self.qss_file) self.resize(400, 600) self.setWindowFlags(self.windowFlags() | Qt.WindowMinMaxButtonsHint) self.setupUi(self) self.header = ["Name", "", "Start", "", "Stop"] self.pushButton_2.clicked.connect(self.addRow) self.pushButton.clicked.connect(self.deleteRow) self.pushButton_recog.clicked.connect(self.recogFromText) self.pushButton_codon.clicked.connect(self.ToCodonMode) self.pushButton_nocodon.clicked.connect(self.CancelCodonMode) self.data_type = "NUC" self.mode = mode ## brief demo country = self.factory.path_settings.value("country", "UK") url = "http://phylosuite.jushengwu.com/dongzhang0725.github.io/documentation/#5-10-2-Brief-tutorial-for-partition-editor" if \ country == "China" else "https://dongzhang0725.github.io/dongzhang0725.github.io/documentation/#5-10-2-Brief-tutorial-for-partition-editor" self.label_4.clicked.connect( lambda: QDesktopServices.openUrl(QUrl(url))) def readPartition(self, partition_content, mode="PF2"): ''' :param partition_content: 串联生成的分区文件 :return: tableview的矩阵 ''' rgx = re.compile(r"(.+) *= *(\d+) *\- *(\d+)(\\3)?;") if mode == "PF2" \ else re.compile(r"charset (.+) *= *(\d+) *\- *(\d+)(\\3)?;") list_contents = partition_content.split("\n") arraydata = [] for line in list_contents: if rgx.search(line): geneName, start, stop, codonSite = rgx.findall(line)[0] geneName_new = self.factory.refineName(geneName.strip(), remain_words="-") arraydata.append( [geneName_new, "=", start, "-", stop + codonSite]) return arraydata def readPartitionFromText(self, partition_content): ''' 从文本识别分区,先识别PF2格式的,如果没有再识别nex格式的 :param partition_content: 分区文本 :return: tableview的矩阵 ''' rgx1 = re.compile(r"(.+) *= *(\d+) *\- *(\d+)(\\3)?;") rgx2 = re.compile(r"charset (.+) *= *(\d+) *\- *(\d+)(\\3)?;") list_contents = partition_content.split("\n") arraydata = [] for line in list_contents: if rgx1.search(line): geneName, start, stop, codonSite = rgx1.findall(line)[0] geneName_new = self.factory.refineName(geneName.strip(), remain_words="-") arraydata.append( [geneName_new, "=", start, "-", stop + codonSite]) elif rgx2.search(line): geneName, start, stop, codonSite = rgx2.findall(line)[0] geneName_new = self.factory.refineName(geneName.strip(), remain_words="-") arraydata.append( [geneName_new, "=", start, "-", stop + codonSite]) return arraydata def partition2text(self, arraydata, mode="PF2"): ''' :param arraydata: :return: partition文本,用于界面展示 ''' array = self.trimArray(arraydata) if array: self.GeneIndexIsOK(array) if mode == "PF2": return ";\n".join(["".join(i) for i in array]) + ";" else: return "#nexus\nbegin sets;\n%send;\n" % ( ";\n".join(["charset " + "".join(i) for i in array]) + ";\n") else: return "" def addRow(self): """ add row """ currentModel = self.tableView_partition.model() # print(currentModel.CDS_rows) if currentModel: currentData = currentModel.arraydata currentModel.layoutAboutToBeChanged.emit() list_data = [] for i in currentData[-1]: if i in ["=", "-", " "]: list_data.append(i) else: list_data.append("") currentData.append(list_data) currentModel.layoutChanged.emit() # self.partitionDataSig.emit(currentData) self.tableView_partition.scrollToBottom() def deleteRow(self): """ delete row """ indices = self.tableView_partition.selectedIndexes() if indices: rows = sorted(set(index.row() for index in indices), reverse=True) self.deleteFromRows(rows) def deleteFromRows(self, rows): currentModel = self.tableView_partition.model() if currentModel: currentData = currentModel.arraydata for row in rows: currentModel.layoutAboutToBeChanged.emit() currentData.pop(row) currentModel.layoutChanged.emit() self.tableView_partition.clearSelection() def refreshTableView(self, array, header): model = MyPartEditorTableModel(array, header, parent=self) model.dataChanged.connect(self.sortGenes) self.tableView_partition.setModel(model) def ctrlResizedColumn(self): for j in range(0, 5): self.tableView_partition.horizontalHeader().setSectionResizeMode( j, QHeaderView.ResizeToContents) def recogFromText(self): text = self.textEdit.toPlainText() array = self.readPartitionFromText(text) if array: reply = QMessageBox.question( self, "Partition editor", "This will overwrite the partitions in the above table and cannot be undone, continue?", QMessageBox.Yes, QMessageBox.Cancel) if reply == QMessageBox.Yes: model = MyPartEditorTableModel(array, self.header, parent=self) model.dataChanged.connect(self.sortGenes) self.tableView_partition.setModel(model) self.ctrlResizedColumn() # 先执行一次改变列的宽度 else: QMessageBox.information( self, "Partition editor", "<p style='line-height:25px; height:25px'>The format of the partition text cannot be recognized! " "Hover mouse over the text box to see example.</p>") def closeEvent(self, e): currentModel = self.tableView_partition.model() text = self.partition2text(currentModel.arraydata, mode=self.mode) self.guiCloseSig.emit(text) def trimArray(self, arraydata): ''' 只保留有值的array :param arraydata: :return: ''' array = [] for i in arraydata: if i[0] and i[2] and i[4]: array.append(i) return array def ToCodonMode(self): indices = self.tableView_partition.selectedIndexes() currentModel = self.tableView_partition.model() if currentModel and indices: currentData = currentModel.arraydata rows = sorted(set(index.row() for index in indices), reverse=True) # 排除/3的行 CDS_rows = self.fetchCDSrow(currentData) intersect_rows = list(set(rows).intersection(set(CDS_rows))) if intersect_rows: new_array = [] for row, line in enumerate(currentData): if row in intersect_rows: name, x1, start, x2, stop = line new_array.append( [name + "_codon1", "=", start, "-", stop + "\\3"]) new_array.append([ name + "_codon2", "=", str(int(start) + 1), "-", stop + "\\3" ]) new_array.append([ name + "_codon3", "=", str(int(start) + 2), "-", stop + "\\3" ]) else: new_array.append(line) model = MyPartEditorTableModel(new_array, self.header, parent=self) model.dataChanged.connect(self.sortGenes) self.tableView_partition.setModel(model) self.ctrlResizedColumn() else: QMessageBox.information( self, "Partition editor", "<p style='line-height:25px; height:25px'>The length of the selected genes is not a multiple of 3 or " "the data are already in the codon mode!</p>") def CancelCodonMode(self): ''' 3个位点都选中的时候,才能取消,如果没有选中要说明一下 :return: ''' indices = self.tableView_partition.selectedIndexes() currentModel = self.tableView_partition.model() if currentModel and indices: currentData = currentModel.arraydata rows = sorted(set(index.row() for index in indices), reverse=True) dict_ = {} # {789: [1, 2, 3]} #stop位置及其所有起始位置 for row in rows: if "\\3" in currentData[row][4]: start = int(currentData[row][2]) stop = int(currentData[row][4].replace("\\3", "")) dict_.setdefault(stop, ["NA", "NA", "NA"]) # 不管是哪一位,先把三联的起始位置找到 if (stop - start + 1) % 3 == 0: dict_[stop][0] = row #第一位 elif (stop - start + 1) % 3 == 2: dict_[stop][1] = row #第二位 elif (stop - start + 1) % 3 == 1: dict_[stop][2] = row # 第三位 #先按三联行的位置排序,然后从大开始删和修改 list_values = sorted([i for i in dict_.values() if "NA" not in i], key=lambda x: max(x), reverse=True) for i in list_values: delete_rows = sorted(i, reverse=True) if delete_rows: # 先修改第一个 currentModel.layoutAboutToBeChanged.emit() currentData[delete_rows[-1]][0] = re.sub( r"_codon\d", "", currentData[delete_rows[0]][0]) currentData[delete_rows[-1]][4] = currentData[ delete_rows[0]][4].replace("\\3", "") currentModel.layoutChanged.emit() self.deleteFromRows(delete_rows[:-1]) #只删除2、3行 self.ctrlResizedColumn() def fetchCDSrow(self, array): CDS_rows = [] for row, line in enumerate(array): if "\\3" in line[4]: continue start = int(line[2]) stop = int(line[4].replace("\\3", "")) if (stop - start + 1) % 3 == 0: if row not in CDS_rows: CDS_rows.append(row) return CDS_rows def showEvent(self, event): self.textEdit.clear() def sortGenes(self): self.tableView_partition.sortByColumn(2, Qt.AscendingOrder) def GeneIndexIsOK(self, array): # 基因多的时候只展示前2个 list_ = [] # [[cox1, 1, 867]] for row in array: geneName, start, stop = row[0], int(row[2]), int(row[4].replace( "\\3", "")) if "\\3" in row[4]: if ((stop - start + 1) % 3 == 0): list_.append([geneName, start, stop]) else: list_.append([geneName, start, stop]) overlap = [] # [[cox1, cox2]] space = [] # [[cox1, cox2]] startError = "" last = None for i in list_: if last: diff = i[1] - last[2] - 1 if diff < 0: overlap.append([last[0], i[0]]) elif diff > 0: space.append([last[0], i[0]]) else: # 起始位置 if i[1] != 1: # 起始必须是1开始 startError = " <span style='font-weight:600'>*</span> <span style='font-weight:600; color:#ff0000;'>%s</span> must start with 1.<br>" % i[ 0] last = i error = "" if startError: error += startError if overlap: if len(overlap) > 2: overlap_ = overlap[:2] extra_text = "And %d more overlapping region(s)...<br><br>" % ( len(overlap) - 2) else: overlap_ = overlap extra_text = "" text = "".join([ " <span style='font-weight:600'>*</span> Positions overlapped between <span style='font-weight:600; color:#ff0000;'>%s</span> and <span style='font-weight:600; color:#ff0000;'>%s</span><br>" % (j[0], j[1]) for j in overlap_ ]) error += text + extra_text if space: if len(overlap) > 2: space_ = space[:2] extra_text = "And %d more unassigned region(s)...<br><br>" % ( len(space) - 2) else: space_ = space extra_text = "" text = "".join([ " <span style='font-weight:600'>*</span> Unassigned positions found between <span style='font-weight:600; color:#ff0000;'>%s</span> and <span style='font-weight:600; color:#ff0000;'>%s</span><br>" % (j[0], j[1]) for j in space_ ]) error += text + extra_text error = "Gene positions setting failed:<br>" + error if error else "" if error: QMessageBox.warning( self, "Warning", "<p style='line-height:25px; height:25px'>%s</p>" % error)
class ModelFinder(QDialog, Ui_ModelFinder, object): exception_signal = pyqtSignal(str) # 定义所有类都可以使用的信号 progressSig = pyqtSignal(int) # 控制进度条 startButtonStatusSig = pyqtSignal(list) logGuiSig = pyqtSignal(str) search_algorithm = pyqtSignal(str) # 定义所有类都可以使用的信号 workflow_progress = pyqtSignal(int) workflow_finished = pyqtSignal(str) iq_tree_exception = pyqtSignal(str) # 定义所有类都可以使用的信号 # 用于flowchart自动popup combobox等操作 showSig = pyqtSignal(QDialog) closeSig = pyqtSignal(str, str) # 用于输入文件后判断用 ui_closeSig = pyqtSignal(str) ##弹出识别输入文件的信号 auto_popSig = pyqtSignal(QDialog) def __init__( self, autoMFPath=None, partFile=None, workPath=None, focusSig=None, IQ_exe=None, workflow=False, parent=None): super(ModelFinder, self).__init__(parent) self.parent = parent self.function_name = "ModelFinder" self.factory = Factory() self.thisPath = self.factory.thisPath self.workPath = workPath self.focusSig = focusSig if focusSig else pyqtSignal( str) # 为了方便workflow self.workflow = workflow self.modelfinder_exe = '"' + IQ_exe + '"' self.autoMFPath = autoMFPath[0] if type(autoMFPath) == list else autoMFPath self.partFile = partFile self.interrupt = False self.setupUi(self) # 保存设置 if not workflow: self.modelfinder_settings = QSettings( self.thisPath + '/settings/modelfinder_settings.ini', QSettings.IniFormat) else: self.modelfinder_settings = QSettings( self.thisPath + '/settings/workflow_settings.ini', QSettings.IniFormat) self.modelfinder_settings.beginGroup("Workflow") self.modelfinder_settings.beginGroup("temporary") self.modelfinder_settings.beginGroup('ModelFinder') # File only, no fallback to registry or or. self.modelfinder_settings.setFallbacksEnabled(False) # 开始装载样式表 with open(self.thisPath + os.sep + 'style.qss', encoding="utf-8", errors='ignore') as f: self.qss_file = f.read() self.setStyleSheet(self.qss_file) # 恢复用户的设置 self.guiRestore() self.exception_signal.connect(self.popupException) self.iq_tree_exception.connect(self.popup_IQTREE_exception) self.startButtonStatusSig.connect(self.factory.ctrl_startButton_status) self.progressSig.connect(self.runProgress) self.logGuiSig.connect(self.addText2Log) self.comboBox_4.installEventFilter(self) self.log_gui = self.gui4Log() self.lineEdit.installEventFilter(self) self.lineEdit.autoDetectSig.connect(self.popupAutoDec) self.lineEdit_2.installEventFilter(self) self.lineEdit_3.installEventFilter(self) self.comboBox_3.activated[str].connect(self.controlCodonTable) self.comboBox_5.activated[str].connect(self.ctrl_ratehet) self.ctrl_ratehet(self.comboBox_5.currentText()) self.lineEdit_2.setLineEditNoChange(True) self.lineEdit_3.setLineEditNoChange(True) self.lineEdit.deleteFile.clicked.connect( self.clear_lineEdit) # 删除了内容,也要把tooltip删掉 self.lineEdit_2.deleteFile.clicked.connect( self.clear_lineEdit) # 删除了内容,也要把tooltip删掉 self.lineEdit_3.deleteFile.clicked.connect( self.clear_lineEdit) # 删除了内容,也要把tooltip删掉 # 初始化codon table的选择 self.controlCodonTable(self.comboBox_3.currentText()) self.checkBox.stateChanged.connect(self.switchPart) self.switchPart(self.checkBox.isChecked()) # 给开始按钮添加菜单 menu = QMenu(self) menu.setToolTipsVisible(True) action = QAction(QIcon(":/picture/resourses/terminal-512.png"), "View | Edit command", menu, triggered=self.showCMD) self.work_action = QAction(QIcon(":/picture/resourses/work.png"), "", menu) self.work_action.triggered.connect(lambda: self.factory.swithWorkPath(self.work_action, parent=self)) self.dir_action = QAction(QIcon(":/picture/resourses/folder.png"), "Output Dir: ", menu) self.dir_action.triggered.connect(lambda: self.factory.set_direct_dir(self.dir_action, self)) menu.addAction(action) menu.addAction(self.work_action) menu.addAction(self.dir_action) self.pushButton.toolButton.setMenu(menu) self.pushButton.toolButton.menu().installEventFilter(self) self.factory.swithWorkPath(self.work_action, init=True, parent=self) # 初始化一下 ## brief demo self.label_2.clicked.connect(lambda: QDesktopServices.openUrl(QUrl( "https://dongzhang0725.github.io/dongzhang0725.github.io/documentation/#5-9-1-Brief-example"))) ##自动弹出识别文件窗口 self.auto_popSig.connect(self.popupAutoDecSub) @pyqtSlot() def on_pushButton_clicked(self): """ execute program """ try: self.command = self.getCMD() if self.command: self.MF_popen = self.factory.init_popen(self.command) self.factory.emitCommands(self.logGuiSig, self.command) self.worker = WorkThread(self.run_command, parent=self) self.worker.start() except: self.exceptionInfo = ''.join( traceback.format_exception( *sys.exc_info())) # 捕获报错内容,只能在这里捕获,没有报错的地方无法捕获 self.exception_signal.emit(self.exceptionInfo) # 激发这个信号 @pyqtSlot() def on_pushButton_3_clicked(self): """ alignment file """ fileName = QFileDialog.getOpenFileName( self, "Input alignment file", filter="Phylip Format(*.phy *.phylip);;Fasta Format(*.fas *.fasta);;Nexus Format(*.nex *.nexus *.nxs);;Clustal Format(*.aln)") if fileName[0]: base = os.path.basename(fileName[0]) self.lineEdit.setText(base) self.lineEdit.setToolTip(fileName[0]) @pyqtSlot() def on_pushButton_4_clicked(self): """ tree file """ fileName = QFileDialog.getOpenFileName( self, "Input tree file", filter="Newick Format(*.nwk *.newick);;") if fileName[0]: base = os.path.basename(fileName[0]) self.lineEdit_2.setText(base) self.lineEdit_2.setToolTip(fileName[0]) @pyqtSlot() def on_pushButton_22_clicked(self): """ partition file """ fileName = QFileDialog.getOpenFileName( self, "Input partition file", filter="All(*);;") if fileName[0]: base = os.path.basename(fileName[0]) self.lineEdit_3.setText(base) self.lineEdit_3.setToolTip(fileName[0]) @pyqtSlot() def on_pushButton_9_clicked(self): """ show log """ self.log_gui.show() @pyqtSlot() def on_pushButton_2_clicked(self): """ Stop """ if self.isRunning(): if not self.workflow: reply = QMessageBox.question( self, "Confirmation", "<p style='line-height:25px; height:25px'>ModelFinder is still running, terminate it?</p>", QMessageBox.Yes, QMessageBox.Cancel) else: reply = QMessageBox.Yes if reply == QMessageBox.Yes: try: self.worker.stopWork() if platform.system().lower() in ["windows", "darwin"]: self.MF_popen.kill() else: os.killpg(os.getpgid(self.MF_popen.pid), signal.SIGTERM) self.MF_popen = None self.interrupt = True except: self.MF_popen = None self.interrupt = True if not self.workflow: QMessageBox.information( self, "ModelFinder", "<p style='line-height:25px; height:25px'>Program has been terminated!</p>") self.startButtonStatusSig.emit( [ self.pushButton, self.progressBar, "except", self.exportPath, self.qss_file, self]) def run_command(self): try: time_start = datetime.datetime.now() self.startButtonStatusSig.emit( [ self.pushButton, self.progressBar, "start", self.exportPath, self.qss_file, self]) self.run_MF() time_end = datetime.datetime.now() self.time_used = str(time_end - time_start) self.time_used_des = "Start at: %s\nFinish at: %s\nTotal time used: %s\n\n" % (str(time_start), str(time_end), self.time_used) softWare = self.comboBox_5.currentText() if softWare in ["BEAST1 (NUC)", "BEAST2 (NUC)", "BEAST (AA)"]: str1 = self.description + " " + self.parseResults() +\ "\n\nIf you use PhyloSuite, please cite:\nZhang, D., F. Gao, I. Jakovlić, H. Zou, J. Zhang, W.X. Li, and G.T. Wang, PhyloSuite: An integrated and scalable desktop platform for streamlined molecular sequence data management and evolutionary phylogenetics studies. Molecular Ecology Resources, 2020. 20(1): p. 348–355. DOI: 10.1111/1755-0998.13096.\n" \ "If you use ModelFinder, please cite:\n" + self.reference + \ "\n\nhttps://justinbagley.rbind.io/2016/10/11/setting-dna-substitution-models-beast/\nDetails for setting substitution models in %s\n" % softWare array = [[i] for i in str1.split( "\n")] + self.model2beast_des() + [[j] for j in ("\n\n" + self.time_used_des).split("\n")] self.factory.write_csv_file( self.exportPath + os.sep + "summary.csv", array, self, silence=True) else: with open(self.exportPath + os.sep + "summary.txt", "w", encoding="utf-8") as f: f.write(self.description + " " + self.parseResults() + "\n\nIf you use PhyloSuite, please cite:\nZhang, D., F. Gao, I. Jakovlić, H. Zou, J. Zhang, W.X. Li, and G.T. Wang, PhyloSuite: An integrated and scalable desktop platform for streamlined molecular sequence data management and evolutionary phylogenetics studies. Molecular Ecology Resources, 2020. 20(1): p. 348–355. DOI: 10.1111/1755-0998.13096.\n" "If you use ModelFinder, please cite:\n" + self.reference + "\n\n" + self.time_used_des) if not self.interrupt: if self.workflow: # work flow跑的 self.startButtonStatusSig.emit( [ self.pushButton, self.progressBar, "workflow stop", self.exportPath, self.qss_file, self]) self.workflow_finished.emit("finished") return self.startButtonStatusSig.emit( [ self.pushButton, self.progressBar, "stop", self.exportPath, self.qss_file, self]) else: self.startButtonStatusSig.emit( [ self.pushButton, self.progressBar, "except", self.exportPath, self.qss_file, self]) if not self.workflow: self.focusSig.emit(self.exportPath) except BaseException: self.exceptionInfo = ''.join( traceback.format_exception( *sys.exc_info())) # 捕获报错内容,只能在这里捕获,没有报错的地方无法捕获 self.exception_signal.emit(self.exceptionInfo) # 激发这个信号 self.startButtonStatusSig.emit( [ self.pushButton, self.progressBar, "except", self.exportPath, self.qss_file, self]) def guiSave(self): # Save geometry self.modelfinder_settings.setValue('size', self.size()) # self.modelfinder_settings.setValue('pos', self.pos()) for name, obj in inspect.getmembers(self): # if type(obj) is QComboBox: # this works similar to isinstance, but # missed some field... not sure why? if isinstance(obj, QComboBox): # save combobox selection to registry index = obj.currentIndex() self.modelfinder_settings.setValue(name, index) # if isinstance(obj, QLineEdit): # text = obj.text() # tooltip = obj.toolTip() # self.modelfinder_settings.setValue(name, [text, tooltip]) if isinstance(obj, QCheckBox): state = obj.isChecked() self.modelfinder_settings.setValue(name, state) if isinstance(obj, QRadioButton): state = obj.isChecked() self.modelfinder_settings.setValue(name, state) def guiRestore(self): # Restore geometry self.resize(self.modelfinder_settings.value('size', QSize(500, 500))) self.factory.centerWindow(self) # self.move(self.modelfinder_settings.value('pos', QPoint(875, 254))) for name, obj in inspect.getmembers(self): if isinstance(obj, QComboBox): if name == "comboBox_6": cpu_num = multiprocessing.cpu_count() list_cpu = ["AUTO"] + [str(i + 1) for i in range(cpu_num)] index = self.modelfinder_settings.value(name, "0") model = obj.model() obj.clear() for num, i in enumerate(list_cpu): item = QStandardItem(i) # 背景颜色 if num % 2 == 0: item.setBackground(QColor(255, 255, 255)) else: item.setBackground(QColor(237, 243, 254)) model.appendRow(item) obj.setCurrentIndex(int(index)) else: allItems = [obj.itemText(i) for i in range(obj.count())] index = self.modelfinder_settings.value(name, "0") model = obj.model() obj.clear() for num, i in enumerate(allItems): item = QStandardItem(i) # 背景颜色 if num % 2 == 0: item.setBackground(QColor(255, 255, 255)) else: item.setBackground(QColor(237, 243, 254)) model.appendRow(item) obj.setCurrentIndex(int(index)) if isinstance(obj, QLineEdit): # text, tooltip = self.modelfinder_settings.value(name, ["", ""]) # if os.path.exists(tooltip): # if os.path.exists(tooltip): # obj.setText(text) # obj.setToolTip(tooltip) if self.autoMFPath and name == "lineEdit": if os.path.exists(self.autoMFPath): obj.setText(os.path.basename(self.autoMFPath)) obj.setToolTip(self.autoMFPath) if self.partFile and name == "lineEdit_3": self.inputPartition(self.partFile) if isinstance(obj, QCheckBox): value = self.modelfinder_settings.value( name, "true") # get stored value from registry obj.setChecked( self.factory.str2bool(value)) # restore checkbox if isinstance(obj, QRadioButton): value = self.modelfinder_settings.value( name, "true") # get stored value from registry obj.setChecked( self.factory.str2bool(value)) # restore checkbox def runProgress(self, num): oldValue = self.progressBar.value() done_int = int(num) if done_int > oldValue: self.progressBar.setProperty("value", done_int) QCoreApplication.processEvents() def popupException(self, exception): print(exception) rgx = re.compile(r'Permission.+?[\'\"](.+\.csv)[\'\"]') if rgx.search(exception): csvfile = rgx.search(exception).group(1) reply = QMessageBox.critical( self, "Extract sequence", "<p style='line-height:25px; height:25px'>Please close '%s' file first!</p>" % os.path.basename( csvfile), QMessageBox.Yes, QMessageBox.Cancel) if reply == QMessageBox.Yes and platform.system().lower() == "windows": os.startfile(csvfile) else: msg = QMessageBox(self) msg.setIcon(QMessageBox.Critical) msg.setText( 'The program encountered an unforeseen problem, please report the bug at <a href="https://github.com/dongzhang0725/PhyloSuite/issues">https://github.com/dongzhang0725/PhyloSuite/issues</a> or send an email with the detailed traceback to [email protected]') msg.setWindowTitle("Error") msg.setDetailedText(exception) msg.setStandardButtons(QMessageBox.Ok) msg.exec_() def popup_IQTREE_exception(self, text): if text.endswith("redo"): reply = QMessageBox.question( self, "ModelFinder", "<p style='line-height:25px; height:25px'>%s. Do you want to rerun the analysis and overwrite all output files?</p>" % text.replace( "redo", ""), QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: self.interrupt = False self.run_with_CMD(self.command + " -redo") else: QMessageBox.information( self, "ModelFinder", "<p style='line-height:25px; height:25px'>%s</p>" % text) if "Show log" in text: self.on_pushButton_9_clicked() def closeEvent(self, event): self.guiSave() self.log_gui.close() # 关闭子窗口 self.closeSig.emit("ModelFinder", self.fetchWorkflowSetting()) # 断开showSig和closeSig的槽函数连接 try: self.showSig.disconnect() except: pass try: self.closeSig.disconnect() except: pass if self.workflow: self.ui_closeSig.emit("ModelFinder") # 自动跑的时候不杀掉程序 return if self.isRunning(): # print(self.isRunning()) reply = QMessageBox.question( self, "ModelFinder", "<p style='line-height:25px; height:25px'>ModelFinder is still running, terminate it?</p>", QMessageBox.Yes, QMessageBox.Cancel) if reply == QMessageBox.Yes: try: self.worker.stopWork() if platform.system().lower() in ["windows", "darwin"]: self.MF_popen.kill() else: os.killpg(os.getpgid(self.MF_popen.pid), signal.SIGTERM) self.MF_popen = None self.interrupt = True except: self.MF_popen = None self.interrupt = True else: event.ignore() def eventFilter(self, obj, event): # modifiers = QApplication.keyboardModifiers() name = obj.objectName() if isinstance( obj, QLineEdit): if event.type() == QEvent.DragEnter: if event.mimeData().hasUrls(): # must accept the dragEnterEvent or else the dropEvent # can't occur !!! event.accept() return True if event.type() == QEvent.Drop: files = [u.toLocalFile() for u in event.mimeData().urls()] if name == "lineEdit": if os.path.splitext(files[0])[1].upper() in [".PHY", ".PHYLIP", ".FAS", ".FASTA", ".NEX", ".NEXUS", ".NXS", ".ALN"]: base = os.path.basename(files[0]) self.lineEdit.setText(base) self.lineEdit.setToolTip(files[0]) else: QMessageBox.information( self, "ModelFinder", "<p style='line-height:25px; height:25px'>Unsupported file!</p>", QMessageBox.Ok) elif name == "lineEdit_2" and os.path.splitext(files[0])[1].upper() in [".NWK", ".NEWICK"]: base = os.path.basename(files[0]) self.lineEdit_2.setText(base) self.lineEdit_2.setToolTip(files[0]) elif name == "lineEdit_3": base = os.path.basename(files[0]) self.lineEdit_3.setText(base) self.lineEdit_3.setToolTip(files[0]) if (event.type() == QEvent.Show) and (obj == self.pushButton.toolButton.menu()): if re.search(r"\d+_\d+_\d+\-\d+_\d+_\d+", self.dir_action.text()) or self.dir_action.text() == "Output Dir: ": self.factory.sync_dir(self.dir_action) ##同步文件夹名字 menu_x_pos = self.pushButton.toolButton.menu().pos().x() menu_width = self.pushButton.toolButton.menu().size().width() button_width = self.pushButton.toolButton.size().width() pos = QPoint(menu_x_pos - menu_width + button_width, self.pushButton.toolButton.menu().pos().y()) self.pushButton.toolButton.menu().move(pos) return True # return QMainWindow.eventFilter(self, obj, event) # # 其他情况会返回系统默认的事件处理方法。 return super(ModelFinder, self).eventFilter(obj, event) # 0 def gui4Log(self): dialog = QDialog(self) dialog.resize(800, 500) dialog.setWindowTitle("Log") gridLayout = QGridLayout(dialog) horizontalLayout_2 = QHBoxLayout() label = QLabel(dialog) label.setText("Log of ModelFinder:") horizontalLayout_2.addWidget(label) spacerItem = QSpacerItem( 40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) horizontalLayout_2.addItem(spacerItem) toolButton = QToolButton(dialog) icon2 = QIcon() icon2.addPixmap( QPixmap(":/picture/resourses/interface-controls-text-wrap-512.png")) toolButton.setIcon(icon2) toolButton.setCheckable(True) toolButton.setToolTip("Use Wraps") toolButton.clicked.connect(self.setWordWrap) toolButton.setChecked(True) horizontalLayout_2.addWidget(toolButton) pushButton = QPushButton("Save to file", dialog) icon = QIcon() icon.addPixmap(QPixmap(":/picture/resourses/Save-icon.png")) pushButton.setIcon(icon) pushButton_2 = QPushButton("Close", dialog) icon = QIcon() icon.addPixmap(QPixmap(":/picture/resourses/if_Delete_1493279.png")) pushButton_2.setIcon(icon) self.textEdit_log = QTextEdit(dialog) self.textEdit_log.setReadOnly(True) gridLayout.addLayout(horizontalLayout_2, 0, 0, 1, 2) gridLayout.addWidget(self.textEdit_log, 1, 0, 1, 2) gridLayout.addWidget(pushButton, 2, 0, 1, 1) gridLayout.addWidget(pushButton_2, 2, 1, 1, 1) pushButton.clicked.connect(self.save_log_to_file) pushButton_2.clicked.connect(dialog.close) dialog.setWindowFlags( dialog.windowFlags() | Qt.WindowMinMaxButtonsHint) return dialog def addText2Log(self, text): if re.search(r"\w+", text): self.textEdit_log.append(text) with open(self.exportPath + os.sep + "PhyloSuite_ModelFinder.log", "a") as f: f.write(text + "\n") def save_log_to_file(self): content = self.textEdit_log.toPlainText() fileName = QFileDialog.getSaveFileName( self, "ModelFinder", "log", "text Format(*.txt)") if fileName[0]: with open(fileName[0], "w", encoding="utf-8") as f: f.write(content) def setWordWrap(self): button = self.sender() if button.isChecked(): button.setChecked(True) self.textEdit_log.setLineWrapMode(QTextEdit.WidgetWidth) else: button.setChecked(False) self.textEdit_log.setLineWrapMode(QTextEdit.NoWrap) def isRunning(self): '''判断程序是否运行,依赖进程是否存在来判断''' return hasattr(self, "MF_popen") and self.MF_popen and not self.interrupt def controlCodonTable(self, text): if text in ["Codon", "DNA-->AA"]: self.comboBox_9.setEnabled(True) else: self.comboBox_9.setEnabled(False) if text == "Codon": QMessageBox.information( self, "Information", "<p style='line-height:25px; height:25px'>If you choose \"Codon\" sequence type, " "only \"IQ-TREE\" is allowed as the \"Model for\" option!</p>") self.comboBox_5.setCurrentText("IQ-TREE") def run_MF(self): rgx_test_model = re.compile(r"^ModelFinder will test (\d+) \w+ models") rgx_part_model = re.compile(r"^Loading (\d+) partitions\.\.\.") rgx_finished = re.compile(r"^Date and Time:") self.totleModels = None self.totlePartitions_2 = None list_partition_names = [] # 存放partition的名字 num = 0 # partition出现的次数,当num等于2倍partition的个数的时候,就完成 is_error = False ##判断是否出了error while True: QApplication.processEvents() if self.isRunning(): try: out_line = self.MF_popen.stdout.readline().decode("utf-8", errors="ignore") except UnicodeDecodeError: out_line = self.MF_popen.stdout.readline().decode("gbk", errors="ignore") if out_line == "" and self.MF_popen.poll() is not None: break list_outline = out_line.strip().split() if rgx_test_model.search(out_line): self.totleModels = int( rgx_test_model.search(out_line).group(1)) self.progressSig.emit(5) self.workflow_progress.emit(5) elif rgx_part_model.search(out_line): self.totlePartitions_2 = 2 * \ int(rgx_part_model.search(out_line).group(1)) self.progressSig.emit(5) self.workflow_progress.emit(5) elif self.totleModels and (len(list_outline) == 7) and list_outline[0].isdigit() and list_outline[3].isdigit(): # 普通模式 model_num = int(list_outline[0]) self.progressSig.emit( 5 + model_num * 90 / self.totleModels) self.workflow_progress.emit( 5 + model_num * 90 / self.totleModels) # 下面2个是partition mode elif self.totlePartitions_2 and re.search(r"^\d", out_line) and len(out_line.strip().split("\t")) == 8: # partition模式, Subset Type Seqs Sites Infor # Invar Model Name list_partition_names.append( out_line.strip().split("\t")[-1]) elif list_partition_names and self.totlePartitions_2 and (num <= self.totlePartitions_2) and re.search(r"^ +\d+|^Optimizing", out_line): for i in list_partition_names: if i in out_line: num += 1 self.progressSig.emit( 5 + num * 90 / self.totlePartitions_2) self.workflow_progress.emit( 5 + num * 90 / self.totlePartitions_2) # print(num, self.totlePartitions_2) elif rgx_finished.search(out_line): self.progressSig.emit(100) self.workflow_progress.emit(100) text = out_line.strip() if re.search(r"\w", out_line) else "\n" self.logGuiSig.emit(text) if re.search(r"^ERROR", out_line): is_error = True # self.on_pushButton_9_clicked() # redo if re.search(r"(?m)(^Checkpoint.+?a previous run successfully finished)", out_line): self.interrupt = True text = re.search( r"(?m)(^Checkpoint.+?a previous run successfully finished)", out_line).group(1) self.iq_tree_exception.emit(text + "redo") else: break if is_error: self.interrupt = True self.iq_tree_exception.emit( "Error happened! Click <span style='font-weight:600; color:#ff0000;'>Show log</span> to see detail!") self.MF_popen = None def clear_lineEdit(self): sender = self.sender() lineEdit = sender.parent() lineEdit.setText("") lineEdit.setToolTip("") def ctrl_ratehet(self, text): if text == "IQ-TREE": self.checkBox_2.setEnabled(True) else: self.checkBox_2.setEnabled(False) if self.comboBox_3.currentText() == "Codon" and text != "IQ-TREE": QMessageBox.information( self, "Information", "<p style='line-height:25px; height:25px'>If you choose \"Codon\" sequence type, " "only \"IQ-TREE\" is allowed as the \"Model for\" option!</p>") self.comboBox_5.setCurrentText("IQ-TREE") def workflow_input(self, MSA=None, partition=None): self.lineEdit.setText("") self.lineEdit_2.setText("") self.lineEdit_3.setText("") if MSA: MSA = MSA[0] if type(MSA) == list else MSA self.lineEdit.setText(os.path.basename(MSA)) self.lineEdit.setToolTip(MSA) if partition: self.inputPartition(partition) def inputPartition(self, partition): if os.path.exists(partition): with open(partition, encoding="utf-8", errors='ignore') as f: partition_txt = f.read() iq_partition = re.search( r"(?s)\*\*\*IQ-TREE style\*\*\*(.+?)[\*|$]", partition_txt).group(1).strip() path = os.path.dirname(partition) + os.sep + "MF_partition.txt" with open(path, "w", encoding="utf-8") as f1: f1.write(iq_partition) self.lineEdit_3.setText(os.path.basename(path)) self.lineEdit_3.setToolTip(path) def popupAutoDec(self, init=False): self.init = init self.factory.popUpAutoDetect( "ModelFinder", self.workPath, self.auto_popSig, self) def popupAutoDecSub(self, popupUI): if not popupUI: if not self.init: QMessageBox.warning( self, "Warning", "<p style='line-height:25px; height:25px'>No available file detected!</p>") return if not self.init: popupUI.checkBox.setVisible(False) if popupUI.exec_() == QDialog.Accepted: widget = popupUI.listWidget_framless.itemWidget( popupUI.listWidget_framless.selectedItems()[0]) input_file, partition_file = widget.autoInputs self.workflow_input(input_file, partition_file) def fetchWorkflowSetting(self): '''* if partition mode, partition style: * seq type: * code table(if codon) * criterion * model for: ''' settings = '''<p class="title">***ModelFinder***</p>''' ifPartition = "Yes" if self.checkBox.isChecked() else "No" settings += '<p>Partition mode: <a href="self.ModelFinder_exe' \ ' factory.highlightWidgets(x.checkBox)">%s</a></p>' % ifPartition if ifPartition == "Yes": partStyle = "Edge-linked" if self.radioButton.isChecked() else "Edge-unlinked" settings += '<p>Partition style: <a href="self.ModelFinder_exe' \ ' factory.highlightWidgets(x.radioButton,x.radioButton_2)">%s</a></p>' % partStyle seq_type = self.comboBox_3.currentText() settings += '<p>Sequence type: <a href="self.ModelFinder_exe comboBox_3.showPopup()' \ ' factory.highlightWidgets(x.comboBox_3)">%s</a></p>' % seq_type threads = self.comboBox_6.currentText() settings += '<p>Threads: <a href="self.ModelFinder_exe comboBox_6.showPopup()' \ ' factory.highlightWidgets(x.comboBox_6)">%s</a></p>' % threads if seq_type == "Codon": code_table = self.comboBox_9.currentText() settings += '<p>Code table: <a href="self.ModelFinder_exe comboBox_9.showPopup()' \ ' factory.highlightWidgets(x.comboBox_9)">%s</a></p>' % code_table criterion = self.comboBox_4.currentText() settings += '<p>Criterion: <a href="self.ModelFinder_exe comboBox_4.showPopup()' \ ' factory.highlightWidgets(x.comboBox_4)">%s</a></p>' % criterion modelFor = self.comboBox_5.currentText() settings += '<p>Model used for: <a href="self.ModelFinder_exe comboBox_5.showPopup()' \ ' factory.highlightWidgets(x.comboBox_5)">%s</a></p>' % modelFor return settings def showEvent(self, event): QTimer.singleShot(100, lambda: self.showSig.emit(self)) # self.showSig.emit(self) def isFileIn(self): alignment = self.lineEdit.toolTip() if os.path.exists(alignment): return alignment else: return False def parseResults(self): # 非partition模式 list_input_model_file = [self.exportPath + os.sep + i for i in os.listdir(self.exportPath) if os.path.splitext(i)[1].upper() == ".IQTREE"] input_model_file = list_input_model_file[ 0] if list_input_model_file else "" if input_model_file: f = self.factory.read_file(input_model_file) # with open(input_model_file, encoding="utf-8", errors='ignore') as f: #"rb", model_content = f.read() # f.close() rgx_model = re.compile(r"(Best-fit model according to.+?\: )(.+)") # softWare = self.comboBox_5.currentText() # if softWare in ["BEAST1", "BEAST2"]: # prefix = rgx_model.search(model_content).group(1) # model = rgx_model.search(model_content).group(2) # version = softWare[-1] # model_split = model.split("+")[0] # model4beast, addSpecify = self.convertModel2beast(softWare, model_split) # best_model = model.replace(model_split, model4beast) # best_model = prefix + best_model + " [Additional specifications in BEAUti%s: %s]"%(version, addSpecify) \ # if addSpecify else prefix + best_model # else: best_model = rgx_model.search(model_content).group() else: best_model = "" return best_model + "." # def convertModel2beast(self, softWare, model): # '''作废''' # if softWare == "BEAST1": # model_replace = {"JC69": ["JC69", ""], # "TrN": ["TN93", ""], # "TrNef": ["TN93", 'base Frequencies set to "All Equal"'], # "K80": ["HKY", 'base Frequencies set to "All Equal"'], # "K2P": ["HKY", 'base Frequencies set to "All Equal"'], # "F81": ["GTR", 'turn off operators for all rate parameters'], # "HKY": ["HKY", ''], # "SYM": ["GTR", 'base Frequencies set to "All Equal"'], # "TIM": ["GTR", 'edit XML file by hand so that all other rates are equal to the AG rate'], # "TVM": ["GTR", 'unchecking AG rate operator'], # "TVMef": ["GTR", 'unchecking AG rate operator and setting base Frequencies to "All Equal"'], # "GTR": ["GTR", '']} # if model in model_replace: # return model_replace[model] # else: # return [model, ''] # else: # #BEAST2 # model_replace = {"JC69": ["JC69", ""], # "TrN": ["TN93", ""], # "TrNef": ["TN93", 'base Frequencies set to "All Equal"'], # "K80": ["HKY", 'base Frequencies set to "All Equal"'], # "K2P": ["HKY", 'base Frequencies set to "All Equal"'], # "F81": ["GTR", 'fix all rate parameters to 1.0 (uncheck the "estimate" box)'], # "HKY": ["HKY", ''], # "SYM": ["GTR", 'base Frequencies set to "All Equal"'], # "TIM": ["GTR", 'fix CT and AG rate parameters to 1.0 (uncheck the "estimate" box)'], # "TVM": ["GTR", 'fix the AG rate parameter to 1.0 (uncheck the "estimate" box)'], # "TVMef": ["GTR", 'fix the AG rate parameter to 1.0 (uncheck the "estimate" box), ' # 'and also set base Frequencies to "All Equal"'], # "GTR": ["GTR", '']} # if model in model_replace: # return model_replace[model] # else: # return [model, ''] def model2beast_des(self): softWare = self.comboBox_5.currentText() if softWare == "BEAST1 (NUC)": return [['Best-fit substitution model', '(Base) Model to select in BEAUti', 'Additional specifications in BEAUti'], ['JC69', 'JC69', 'None '], ['TrN', 'TN93', 'None '], ['TrNef', 'TN93', 'base Frequencies set to "All Equal"'], ['K80 (K2P)', 'HKY', 'base Frequencies set to "All Equal"'], ['F81', 'GTR', 'turn off operators for all rate parameters'], ['HKY', 'HKY', 'None'], ['SYM', 'GTR', 'base Frequencies set to "All Equal"'], ['TIM', 'GTR', 'edit XML file by hand so that all other rates are equal to the AG rate'], ['TVM', 'GTR', 'unchecking AG rate operator'], ['TVMef', 'GTR', 'unchecking AG rate operator and setting base Frequencies to "All Equal"'], ['GTR', 'GTR', 'None']] elif softWare == "BEAST2 (NUC)": return [['Best-fit substitution model', '(Base) Model to select in BEAUti 2', 'Additional specifications in BEAUti 2'], ['JC69', 'JC69', 'None '], ['TrN', 'TN93', 'None'], ['TrNef', 'TN93', 'base Frequencies set to "All Equal"'], ['K80 (K2P)', 'HKY', 'base Frequencies set to "All Equal"'], ['F81', 'GTR', 'fix all rate parameters to 1.0 (uncheck the "estimate" box)'], ['HKY', 'HKY', ' None'], ['SYM', 'GTR', 'base Frequencies set to "All Equal"'], ['TIM', 'GTR', 'fix CT and AG rate parameters to 1.0 (uncheck the "estimate" box)'], ['TVM', 'GTR', 'fix the AG rate parameter to 1.0 (uncheck the "estimate" box)'], ['TVMef', 'GTR', 'fix the AG rate parameter to 1.0 (uncheck the "estimate" box), and also set base Frequencies to "All Equal"'], ['GTR', 'GTR', ' None']] else: return [] def switchPart(self, state): if state: if not self.workflow: self.lineEdit_3.setEnabled(True) self.pushButton_22.setEnabled(True) else: if not self.workflow: self.lineEdit_3.setEnabled(False) self.pushButton_22.setEnabled(False) def getCMD(self): alignment = self.isFileIn() if alignment: # 有数据才执行 self.output_dir_name = self.factory.fetch_output_dir_name(self.dir_action) self.interrupt = False self.exportPath = self.factory.creat_dirs(self.workPath + os.sep + "ModelFinder_results" + os.sep + self.output_dir_name) ok = self.factory.remove_dir(self.exportPath, parent=self) if not ok: # 提醒是否删除旧结果,如果用户取消,就不执行 return os.chdir(self.exportPath) # 因为用了-pre,所以要先切换目录到该文件夹 inputFile = os.path.basename( shutil.copy(alignment, self.exportPath)) pre = " -pre \"%s\""%self.factory.refineName(inputFile + "." + self.comboBox_5.currentText().lower().replace("-", "_")) # use_model_for = " -m MF" if self.comboBox_5.currentText() == "IQ-TREE" else " -m TESTONLY -mset %s"%self.comboBox_5.currentText().lower() if self.comboBox_5.currentText() == "IQ-TREE": if self.checkBox_2.isChecked(): print(self.lineEdit_3.toolTip()) if self.checkBox.isChecked() and self.lineEdit_3.toolTip() and self.lineEdit_3.isEnabled(): # partition #确保勾选了partition以及输入了文件 use_model_for = " -m TESTNEWMERGEONLY" else: use_model_for = " -m TESTNEWONLY" else: if self.checkBox.isChecked() and self.lineEdit_3.toolTip() and self.lineEdit_3.isEnabled(): # partition use_model_for = " -m TESTMERGEONLY" else: use_model_for = " -m TESTONLY" elif self.comboBox_5.currentText() in ["BEAST1 (NUC)", "BEAST2 (NUC)"]: use_model_for = " -mset JC69,TrN,TrNef,K80,K2P,F81,HKY,SYM,TIM,TVM,TVMef,GTR -mrate E,G" elif self.comboBox_5.currentText() in ["BEAST (AA)"]: use_model_for = " -mset Blosum62,cpREV,JTT,mtREV,WAG,LG,Dayhoff -mrate E,G" else: use_model_for = " -m TESTONLY -mset %s" % self.comboBox_5.currentText().lower() \ if ((not self.checkBox.isChecked()) or (not self.lineEdit_3.toolTip())) else " -m TESTMERGEONLY -mset %s" % self.comboBox_5.currentText().lower() criterion_org = re.search( r"\((\w+)\)", self.comboBox_4.currentText()).group(1) criterion = " -%s" % criterion_org if criterion_org != "BIC" else "" dict_seq = {"DNA": "DNA", "Protein": "AA", "Codon": "CODON", "Binary": "BIN", "Morphology": "MORPH", "DNA-->AA": "NT2AA"} seqType = " -st %s" % dict_seq[ self.comboBox_3.currentText()] if self.comboBox_3.currentText() != "Auto detect" else "" codon_table_index = self.comboBox_9.currentText().split(" ")[0] seqType = seqType + \ codon_table_index if seqType in [ " -st CODON", " -st NT2AA"] else seqType treeFile = " -te %s" % shutil.copy(self.lineEdit_2.toolTip(), self.exportPath) if self.lineEdit_2.toolTip() else "" partitionCMD = "-spp" if self.radioButton.isChecked() else "-sp" partFile = " %s \"%s\"" % ( partitionCMD, os.path.basename(shutil.copy(self.lineEdit_3.toolTip(), self.exportPath))) if ( self.checkBox.isChecked() and self.lineEdit_3.toolTip()) else "" threads = " -nt %s" % self.comboBox_6.currentText() command = self.modelfinder_exe + " -s \"%s\"" % inputFile + \ use_model_for + criterion + seqType + \ treeFile + partFile + threads + pre # print(self.command) self.textEdit_log.clear() # 清空 # 描述 type = "Edge-linked" if self.radioButton.isChecked() else "Edge-unlinked" model = "best-fit model" if not partFile else "best-fit partition model (%s)" % type self.description = '''ModelFinder (Kalyaanamoorthy et al., 2017) was used to select the %s using %s criterion.''' % ( model, criterion_org) self.reference = "Kalyaanamoorthy, S., Minh, B.Q., Wong, T.K.F., von Haeseler, A., Jermiin, L.S., 2017. ModelFinder: fast model selection for accurate phylogenetic estimates. Nat. Methods 14, 587-589." return command else: QMessageBox.critical( self, "ModelFinder", "<p style='line-height:25px; height:25px'>Please input alignment file first!</p>") return None def showCMD(self): """ show command """ self.command = self.getCMD() if self.command: dialog = QDialog(self) dialog.resize(600, 200) dialog.setWindowTitle("Command") gridLayout = QGridLayout(dialog) label = QLabel(dialog) label.setText("Current Command:") pushButton = QPushButton("Save and run", dialog) icon = QIcon() icon.addPixmap(QPixmap(":/picture/resourses/Save-icon.png")) pushButton.setIcon(icon) pushButton_2 = QPushButton("Close", dialog) icon = QIcon() icon.addPixmap( QPixmap(":/picture/resourses/if_Delete_1493279.png")) pushButton_2.setIcon(icon) self.textEdit_cmd = QTextEdit(dialog) self.textEdit_cmd.setText(self.command) gridLayout.addWidget(label, 0, 0, 1, 2) gridLayout.addWidget(self.textEdit_cmd, 1, 0, 1, 2) gridLayout.addWidget(pushButton, 2, 0, 1, 1) gridLayout.addWidget(pushButton_2, 2, 1, 1, 1) pushButton.clicked.connect( lambda: [self.run_with_CMD(self.textEdit_cmd.toPlainText()), dialog.close()]) pushButton_2.clicked.connect(dialog.close) dialog.setWindowFlags( dialog.windowFlags() | Qt.WindowMinMaxButtonsHint) dialog.exec_() def run_with_CMD(self, cmd): self.command = cmd if self.command: self.MF_popen = self.factory.init_popen(self.command) self.factory.emitCommands(self.logGuiSig, self.command) self.worker = WorkThread(self.run_command, parent=self) self.worker.start()
class Seq_matrix(object): def __init__(self, **kwargs): self.factory = Factory() self.dict_args = kwargs self.outpath = self.dict_args["exportPath"] self.dict_species = {} self.dict_statistics = dict(prefix='taxon') self.partition_style = '***partitionfinder style***\n' self.partition_codon = "\n***codon style***\n" self.bayes_style = '\n***bayes style***\n' self.bayes_codon = "\n***bayes codon style***\n" self.iqtree_style = '\n***IQ-TREE style***\n#nexus\nbegin sets;\n' self.iqtree_codon = "\n***IQ-TREE codon style***\n#nexus\nbegin sets;\n" self.parent = self.dict_args["parent"] self.partition_name = '' self.partition_codname = "" self.gene_index = [] self.dist_warning_message = OrderedDict() self.error_message = "" self.warning_message = "" self.parsefmt = Parsefmt(self.error_message, self.warning_message) self.dict_genes_alignments = OrderedDict() self.unaligned = False self.unaligns = [] self.exportName = self.dict_args["export_name"] self.interrupt = False self.list_empty_files = [] for num, eachFile in enumerate(self.dict_args["files"]): if self.interrupt: break geneName = os.path.splitext(os.path.basename(eachFile))[0] geneName = self.factory.refineName(geneName) dict_taxon = self.parsefmt.readfile(eachFile) set_length = set([len(dict_taxon[i]) for i in dict_taxon]) if set_length == {0}: self.list_empty_files.append(os.path.basename(eachFile)) continue self.error_message += self.parsefmt.error_message self.warning_message += self.parsefmt.warning_message if self.factory.is_aligned(dict_taxon): self.dict_genes_alignments[geneName] = dict_taxon else: self.unaligned = True self.unaligns.append(geneName) if self.list_empty_files: if len(self.list_empty_files) == 1: text = "The empty file \"%s\" will be ignored" % self.list_empty_files[ 0] else: file_names = "\", \"".join( self.list_empty_files[:-1] ) + "\" and \"%s\"" % self.list_empty_files[-1] text = "The empty files \"%s will be ignored" % file_names QMetaObject.invokeMethod(self.parent, "popupEmptyFileWarning", Qt.BlockingQueuedConnection, Q_ARG(str, text)) if self.unaligned: self.dict_args["unaligned_signal"].emit(self.unaligns) self.ok = False return if self.dict_genes_alignments: self.supplement() self.concatenate() self.ok = True else: self.ok = False def concatenate(self): total = len(self.dict_args["files"]) self.dict_args["progressSig"].emit(10) self.dict_args["workflow_progress"].emit(10) if not self.unaligned: # 确认比对过 for num, self.genename in enumerate(self.dict_genes_alignments): if self.interrupt: break self.dict_taxon = self.dict_genes_alignments[self.genename] self.dict_statistics['prefix'] += '\t' + self.genename self.append() self.addPartition() self.dict_args["progressSig"].emit(10 + ((num + 1) * 80 / total)) self.dict_args["workflow_progress"].emit(10 + ( (num + 1) * 80 / total)) if self.interrupt: return self.complete() # 完善 self.save() # 存档 self.dict_args["progressSig"].emit(100) self.dict_args["workflow_progress"].emit(100) if self.error_message: self.dict_args["exception_signal"].emit(self.error_message) if self.warning_message: QMetaObject.invokeMethod(self.parent, "popupWarning", Qt.BlockingQueuedConnection, Q_ARG(list, [self.warning_message])) # else: # self.dict_args["unaligned_signal"].emit(self.unaligns) def supplement(self): # 补全缺失基因的物种 # dict_maxTaxa = max(*[self.dict_genes_alignments[i] # for i in self.dict_genes_alignments], key=len) # 物种数最多的基因 dict_maxTaxa = [] for k in self.dict_genes_alignments: if self.interrupt: break list_taxon = list(self.dict_genes_alignments[k].keys()) dict_maxTaxa.extend(list_taxon) list_set_dict_maxTaxa = list(set(dict_maxTaxa)) for i in list_set_dict_maxTaxa: if self.interrupt: break lossingGene = [] for j in self.dict_genes_alignments: if self.interrupt: break if i not in self.dict_genes_alignments[j]: keys = list(self.dict_genes_alignments[j].keys()) alignmentLenth = len( self.dict_genes_alignments[j][keys[0]]) self.dict_genes_alignments[j][i] = "?" * alignmentLenth lossingGene.append(j) if lossingGene: self.dist_warning_message[i] = lossingGene if self.dist_warning_message: ##报错 QMetaObject.invokeMethod(self.parent, "popupWarning", Qt.BlockingQueuedConnection, Q_ARG(list, [self.dist_warning_message])) def append(self): for self.spe_key, self.seq in self.dict_taxon.items(): # 因为读新的文件会 self.seq = self.seq.upper() # 全部改为大写 lenth = len(self.seq) indels = self.seq.count('-') if self.spe_key in self.dict_species: self.dict_species[self.spe_key] += self.seq if self.spe_key in self.dict_statistics: self.dict_statistics[self.spe_key] += '\t' + \ str(lenth) + ' (' + str(indels) + ' indels)' else: self.dict_statistics[self.spe_key] = '\t' + \ str(lenth) + ' (' + str(indels) + ' indels)' else: self.dict_species[self.spe_key] = self.seq self.dict_statistics[self.spe_key] = self.spe_key + \ '\t' + str(lenth) + ' (' + str(indels) + ' indels)' def addPartition(self): ##替换这些特殊符号,可以让序列更容易被识别 rgx = re.compile(r"([.^$*+?{}[\]\\|()])") matFlag = rgx.sub(r"[\1]", self.seq) + '$' span = re.search(matFlag, self.dict_species[self.spe_key]).span() self.gene_index.append(span) self.partition_style += self.genename + '=' + \ str(span[0] + 1) + '-' + str(span[1]) + ';\n' self.partition_codon += self.genename + "_codon1" + \ '=' + str(span[0] + 1) + '-' + str(span[1]) + '\\3;\n' self.partition_codon += self.genename + "_codon2" + \ '=' + str(span[0] + 2) + '-' + str(span[1]) + '\\3;\n' self.partition_codon += self.genename + "_codon3" + \ '=' + str(span[0] + 3) + '-' + str(span[1]) + '\\3;\n' self.bayes_style += 'charset ' + self.genename + \ '=' + str(span[0] + 1) + '-' + str(span[1]) + ';\n' self.bayes_codon += 'charset ' + self.genename + "_codon1" + \ '=' + str(span[0] + 1) + '-' + str(span[1]) + '\\3;\n' self.bayes_codon += 'charset ' + self.genename + "_codon2" + \ '=' + str(span[0] + 2) + '-' + str(span[1]) + '\\3;\n' self.bayes_codon += 'charset ' + self.genename + "_codon3" + \ '=' + str(span[0] + 3) + '-' + str(span[1]) + '\\3;\n' self.iqtree_style += '\tcharset ' + self.genename + \ '=' + str(span[0] + 1) + '-' + str(span[1]) + ';\n' self.iqtree_codon += '\tcharset ' + self.genename + "_codon1" + \ '=' + str(span[0] + 1) + '-' + str(span[1]) + '\\3;\n' self.iqtree_codon += '\tcharset ' + self.genename + "_codon2" + \ '=' + str(span[0] + 2) + '-' + str(span[1]) + '\\3;\n' self.iqtree_codon += '\tcharset ' + self.genename + "_codon3" + \ '=' + str(span[0] + 3) + '-' + str(span[1]) + '\\3;\n' self.partition_name += self.genename + ',' self.partition_codname += self.genename + "_codon1," + \ self.genename + "_codon2," + self.genename + "_codon3," def align(self, seq): list_seq = re.findall(r'(.{60})', seq) if not list_seq: return seq + "\n" remainder = len(seq) % 60 if remainder == 0: self.align_seq = '\n'.join(list_seq) + '\n' else: self.align_seq = '\n'.join( list_seq) + '\n' + seq[-remainder:] + '\n' def nxs_interleave(self): length = len(self.dict_species[self.list_keys[-1]]) # 总长 integer = length // 60 num = 1 if self.dict_args["export_nexi"]: while num <= integer: for i in self.list_keys: self.nxs_inter += i.ljust( self.name_longest) + ' ' + self.dict_species[i][ (num - 1) * 60:num * 60] + '\n' self.nxs_inter += "\n" num += 1 if length % 60 != 0: for i in self.list_keys: self.nxs_inter += i.ljust( self.name_longest) + ' ' + self.dict_species[i][ (num - 1) * 60:length] + '\n' if self.dict_args["export_nexig"]: for each_span in self.gene_index: # 以gene分界形式的nex for i in self.list_keys: self.nxs_gene += i.ljust( self.name_longest) + " " + self.dict_species[i][ each_span[0]:each_span[1]] + "\n" self.nxs_gene += "\n" def get_str(self): # 只有nex和phy格式需要改序列名字空格 for i in self.list_keys: self.dict_statistics[i] += '\t' + \ str(len(self.dict_species[i])) + '\t' + str(self.count) + '\n' self.file += '>' + i + '\n' + \ self.dict_species[i] + '\n' self.phy_file += i.ljust(self.name_longest) + \ ' ' + self.dict_species[i] + '\n' self.nxs_file += i.ljust(self.name_longest) + \ ' ' + self.dict_species[i] + '\n' self.align(self.dict_species[i]) self.paml_file += i + '\n' + \ self.align_seq + '\n' self.axt_file += self.dict_species[i] + '\n' self.statistics += self.dict_statistics[i] def complete(self): self.count = len(self.dict_genes_alignments) self.partition_name = 'partition Names = %s:' % str( self.count) + self.partition_name.strip( ',') + ';\nset partition=Names;\n' self.partition_codname = 'partition Names = %s:' % str( self.count * 3) + self.partition_codname.strip( ',') + ';\nset partition=Names;\n' self.dict_statistics['prefix'] += '\tTotal lenth\tNo of charsets\n' self.list_keys = sorted(list(self.dict_species.keys())) self.pattern = self.parsefmt.which_pattern( self.dict_species, "Concatenated file") # 得到pattern self.error_message += self.parsefmt.error_message self.warning_message += self.parsefmt.warning_message self.file = '' self.phy_file = ' ' + \ str(len(self.list_keys)) + ' ' + \ str(len(self.dict_species[self.list_keys[-1]])) + '\n' self.nxs_file = '#NEXUS\nBEGIN DATA;\ndimensions ntax=%s nchar=%s;\nformat missing=?\ndatatype=%s gap= -;\n\nmatrix\n' % ( str(len(self.list_keys)), str(len(self.dict_species[self.list_keys[-1]])), self.pattern) self.nxs_inter = '#NEXUS\nBEGIN DATA;\ndimensions ntax=%s nchar=%s;\nformat missing=?\ndatatype=%s gap= - interleave;\n\nmatrix\n' % ( str(len(self.list_keys)), str(len(self.dict_species[self.list_keys[-1]])), self.pattern) '''nex的interleave模式''' self.nxs_gene = '#NEXUS\nBEGIN DATA;\ndimensions ntax=%s nchar=%s;\nformat missing=?\ndatatype=%s gap= - interleave;\n\nmatrix\n' % ( str(len(self.list_keys)), str(len(self.dict_species[self.list_keys[-1]])), self.pattern) '''nex的interleave模式,以gene换行''' self.paml_file = str(len(self.list_keys)) + ' ' + \ str(len(self.dict_species[self.list_keys[-1]])) + '\n\n' self.axt_file = '-'.join(self.list_keys) + '\n' self.statistics = self.dict_statistics['prefix'] list_lenth = [len(i) for i in self.list_keys] self.name_longest = max(list_lenth) # 最长的序列名字 self.get_str() self.nxs_interleave() def save(self): self.partition_detail = self.outpath + os.sep + 'partition.txt' with open(self.partition_detail, 'w', encoding="utf-8") as f4: f4.write(self.partition_style + self.partition_codon + self.bayes_style + self.partition_name + self.bayes_codon + self.partition_codname + self.iqtree_style + "end;\n" + self.iqtree_codon + "end;") if self.dict_args["export_axt"]: with open(self.outpath + os.sep + '%s.axt' % self.exportName, 'w', encoding="utf-8") as f1: f1.write(self.axt_file) if self.dict_args["export_fas"]: with open(self.outpath + os.sep + '%s.fas' % self.exportName, 'w', encoding="utf-8") as f2: f2.write(self.file) if self.dict_args["export_stat"]: with open(self.outpath + os.sep + 'statistics.csv', 'w', encoding="utf-8") as f3: f3.write(self.statistics.replace('\t', ',')) if self.dict_args["export_phylip"]: with open(self.outpath + os.sep + '%s.phy' % self.exportName, 'w', encoding="utf-8") as f5: f5.write(self.phy_file) if self.dict_args["export_nex"]: with open(self.outpath + os.sep + '%s.nex' % self.exportName, 'w', encoding="utf-8") as f6: f6.write(self.nxs_file + ';\nEND;\n') if self.dict_args["export_paml"]: with open(self.outpath + os.sep + '%s.PML' % self.exportName, 'w', encoding="utf-8") as f7: f7.write(self.paml_file) if self.dict_args["export_nexi"]: with open(self.outpath + os.sep + '%s_interleave.nex' % self.exportName, 'w', encoding="utf-8") as f8: f8.write(self.nxs_inter + ';\nEND;\n') if self.dict_args["export_nexig"]: with open(self.outpath + os.sep + '%s_bygene.nex' % self.exportName, 'w', encoding="utf-8") as f9: f9.write(self.nxs_gene + ';\nEND;\n')