Ejemplo n.º 1
0
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 = "&nbsp;&nbsp;&nbsp;<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([
                "&nbsp;&nbsp;&nbsp;<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([
                "&nbsp;&nbsp;&nbsp;<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)
Ejemplo n.º 2
0
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()
Ejemplo n.º 3
0
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')