diff --git a/STDF-Viewer.py b/STDF-Viewer.py index 5508e80..3fe4b88 100644 --- a/STDF-Viewer.py +++ b/STDF-Viewer.py @@ -4,7 +4,7 @@ # Author: noonchen - chennoon233@foxmail.com # Created Date: December 13th 2020 # ----- -# Last Modified: Sun Oct 19 2025 +# Last Modified: Sun Nov 02 2025 # Modified By: noonchen # ----- # Copyright (c) 2020 noonchen @@ -726,7 +726,9 @@ def updateDutSummaryTable(self): self.tmodel_dut.setQuery(QtSql.QSqlQuery(DUT_SUMMARY_QUERY, self.db_dut)) for column in range(0, header.count()): - if column in [2, 3, header.count()-1]: + if column in [DutTableColIndex.PartID, + DutTableColIndex.HeadSite, + DutTableColIndex.DutFlag]: # PartID, Head-Site and DUT Flag # column may be too long to display mode = QHeaderView.ResizeMode.ResizeToContents @@ -735,15 +737,15 @@ def updateDutSummaryTable(self): header.setSectionResizeMode(column, mode) # always hide dut index column - self.ui.dutInfoTable.hideColumn(0) - # hide file id column if 1 file is opened - if self.data_interface.num_files <= 1: - self.ui.dutInfoTable.hideColumn(1) - else: - self.ui.dutInfoTable.showColumn(1) - # # show all rows - # while self.tmodel_dut.canFetchMore(): - # self.tmodel_dut.fetchMore() + self.ui.dutInfoTable.hideColumn(DutTableColIndex.DutIndex) + # hide other columns under specific condition + for hideCond, col in [(self.data_interface.num_files <= 1, DutTableColIndex.FileID), + (self.data_interface.noWaferID, DutTableColIndex.WaferID), + (self.data_interface.noWaferXY, DutTableColIndex.XYCOORD)]: + if hideCond: + self.ui.dutInfoTable.hideColumn(col) + else: + self.ui.dutInfoTable.showColumn(col) def updateGDR_DTR_Table(self): diff --git a/deps/DataInterface.py b/deps/DataInterface.py index f989daa..a885ab5 100644 --- a/deps/DataInterface.py +++ b/deps/DataInterface.py @@ -4,7 +4,7 @@ # Author: noonchen - chennoon233@foxmail.com # Created Date: November 3rd 2022 # ----- -# Last Modified: Sun Oct 12 2025 +# Last Modified: Sun Nov 02 2025 # Modified By: noonchen # ----- # Copyright (c) 2022 noonchen @@ -50,7 +50,7 @@ def __init__(self): self.completeWaferList = [] # cache test pin list and names for MPR self.pinInfoDictCache = {} - + def loadDatabase(self): if not os.path.isfile(self.dbPath): @@ -80,6 +80,9 @@ def loadDatabase(self): # for UI display self.completeTestList = self.DatabaseFetcher.getTestItemsList() self.completeWaferList = self.DatabaseFetcher.getWaferList() + # for dut summary + self.noWaferID = self.DatabaseFetcher.isDutInfoColumnEmpty("WaferIndex") + self.noWaferXY = self.DatabaseFetcher.isDutInfoColumnEmpty("XCOORD") def close(self): diff --git a/deps/DatabaseFetcher.py b/deps/DatabaseFetcher.py index 97668d1..f6aac55 100644 --- a/deps/DatabaseFetcher.py +++ b/deps/DatabaseFetcher.py @@ -4,7 +4,7 @@ # Author: noonchen - chennoon233@foxmail.com # Created Date: May 15th 2021 # ----- -# Last Modified: Sun Oct 19 2025 +# Last Modified: Sun Nov 02 2025 # Modified By: noonchen # ----- # Copyright (c) 2021 noonchen @@ -84,6 +84,22 @@ def readFilePaths(self): self.file_paths = file_paths + def isDutInfoColumnEmpty(self, columnName: str) -> bool: + '''return True if the given column of Dut_Info table has no valid value''' + if self.cursor is None: raise RuntimeError("No database is connected") + + sql = f'''SELECT EXISTS ( + SELECT 1 + FROM Dut_Info + WHERE {columnName} IS NOT NULL + AND + (typeof({columnName}) != 'text' OR trim({columnName}) != "") + )''' + + validDataExist = self.cursor.execute(sql).fetchone()[0] + return not validDataExist + + @property def num_files(self): return len(self.file_paths) diff --git a/deps/customizedQtClass.py b/deps/customizedQtClass.py index 3f1fef5..89b1709 100644 --- a/deps/customizedQtClass.py +++ b/deps/customizedQtClass.py @@ -4,7 +4,7 @@ # Author: noonchen - chennoon233@foxmail.com # Created Date: May 26th 2021 # ----- -# Last Modified: Thu Dec 08 2022 +# Last Modified: Sun Nov 02 2025 # Modified By: noonchen # ----- # Copyright (c) 2021 noonchen @@ -28,6 +28,7 @@ from PyQt5.QtWidgets import QStyledItemDelegate from PyQt5.QtCore import Qt, QModelIndex, QSortFilterProxyModel, QAbstractProxyModel from deps.SharedSrc import * +from enum import IntEnum import numpy as np @@ -104,21 +105,24 @@ def getHS(text: str): return head << 8 | site +class DutTableColIndex(IntEnum): + DutIndex = 0 + FileID = 1 + PartID = 2 + HeadSite = 3 + TestCount = 4 + TestTime = 5 + HBIN = 6 + SBIN = 7 + WaferID = 8 + XYCOORD = 9 + DutFlag = 10 + + class DutSortFilter(QSortFilterProxyModel): def __init__(self, parent=None): super().__init__(parent) self.hsFilterString = QtCore.QRegularExpression(r".*") - self.dutIndexInd = 0 - self.fidColInd = 1 - self.pidColInd = 2 - self.hsColInd = 3 - self.tcntColInd = 4 - self.ttimColInd = 5 - self.hbinColInd = 6 - self.sbinColInd = 7 - self.widColInd = 8 - self.xyColInd = 9 - self.flagColInd = 10 def lessThan(self, left: QModelIndex, right: QModelIndex) -> bool: @@ -127,33 +131,26 @@ def lessThan(self, left: QModelIndex, right: QModelIndex) -> bool: textLeft = self.sourceModel().data(left, Qt.ItemDataRole.DisplayRole) textRight = self.sourceModel().data(right, Qt.ItemDataRole.DisplayRole) try: - if (left.column() == self.fidColInd or - left.column() == self.pidColInd): - # sort file id || part id - return int(textLeft) < int(textRight) + match left.column(): + case DutTableColIndex.FileID | DutTableColIndex.PartID: + # sort file id || part id + return int(textLeft) < int(textRight) - elif left.column() == self.hsColInd: - # sort head - site - return getHS(textLeft) < getHS(textRight) - - elif left.column() == self.tcntColInd: - # sort test count - return int(textLeft) < int(textRight) - - elif left.column() == self.ttimColInd: - # sort test time - return int(textLeft.strip("ms")) < int(textRight.strip("ms")) - - elif (left.column() == self.hbinColInd or - left.column() == self.sbinColInd): - # sort hbin / sbin - return int(textLeft.split(" ")[-1]) < int(textRight.split(" ")[-1]) - - elif (left.column() == self.widColInd or - left.column() == self.xyColInd or - left.column() == self.flagColInd): - # sort flag, wafer id, (X, Y) - pass + case DutTableColIndex.HeadSite: + # sort head - site + return getHS(textLeft) < getHS(textRight) + + case DutTableColIndex.TestCount: + # sort test count + return int(textLeft) < int(textRight) + + case DutTableColIndex.TestTime: + # sort test time + return int(textLeft.strip("ms")) < int(textRight.strip("ms")) + + case DutTableColIndex.HBIN | DutTableColIndex.SBIN: + # sort hbin / sbin + return int(textLeft.split(" ")[-1]) < int(textRight.split(" ")[-1]) except ValueError: # use default string compare @@ -180,7 +177,7 @@ def updateHeadsSites(self, selHeads: list, selSites: list): def filterAcceptsRow(self, source_row: int, source_parent: QtCore.QModelIndex) -> bool: - hsIndex = self.sourceModel().index(source_row, self.hsColInd, source_parent) + hsIndex = self.sourceModel().index(source_row, DutTableColIndex.HeadSite, source_parent) hsMatched = self.hsFilterString.match(self.sourceModel().data(hsIndex, Qt.ItemDataRole.DisplayRole)).hasMatch() @@ -227,7 +224,7 @@ def headerData(self, section, orientation, role): # For normal tableView display -class NormalProxyModel(QAbstractProxyModel): +class NormalProxyModel(FlippedProxyModel): def __init__(self, parent=None): super().__init__(parent) @@ -242,18 +239,6 @@ def columnCount(self, parent = QModelIndex()): def rowCount(self, parent = QModelIndex()): return self.sourceModel().rowCount(parent) - - def index(self, row, column, parent = QModelIndex()): - return self.createIndex(row, column) - - def parent(self, index): - return QModelIndex() - - def data(self, index, role): - return self.sourceModel().data(self.mapToSource(index), role) - - def item(self, row, column) -> QtGui.QStandardItem: - return self.sourceModel().item(column, row) def headerData(self, section, orientation, role): return self.sourceModel().headerData(section, orientation, role) @@ -383,146 +368,178 @@ def data(self, index: QModelIndex, role: int): ------------------------------ dut info | test data ''' + blankW = len(self.hheader_base) + blankH = len(self.vheader_base) + row, col = index.row(), index.column() + try: - # upper left corner contains no info - if index.row() < len(self.vheader_base) and index.column() < len(self.hheader_base): - # prevent fall below - return None + if row < blankH: + if col < blankW: + # upper left corner contains no info + return None + else: + # upper right corner contains test info + return self.dataOfTestInfo(index, role) + else: + if col < blankW: + # lower left contains dut info + return self.dataOfDutInfo(index, role) + else: + # lower right contains test data + return self.dataOfTestData(index, role) + + except (IndexError, KeyError): + pass - # upper right corner contains test info - if index.row() < len(self.vheader_base): - if role == Qt.ItemDataRole.DisplayRole: - test_index = index.column() - len(self.hheader_base) - if index.row() == 0: + return None + + def dataOfTestInfo(self, index: QModelIndex, role: int): + match role: + case Qt.ItemDataRole.DisplayRole: + testIndex = index.column() - len(self.hheader_base) + infoTuple = self.testInfo[self.testLists[testIndex]] + + match index.row(): + case 0: # test number - return "%d" % self.testInfo[self.testLists[test_index]][1] - if index.row() == 1: + return "%d" % infoTuple[1] + case 1: # high limit - hl = self.testInfo[self.testLists[test_index]][2] + hl = infoTuple[2] return "N/A" if np.isnan(hl) else self.floatFormat % hl - if index.row() == 2: + case 2: # low limit - ll = self.testInfo[self.testLists[test_index]][3] + ll = infoTuple[3] return "N/A" if np.isnan(ll) else self.floatFormat % ll - if index.row() == 3: + case 3: # unit - return self.testInfo[self.testLists[test_index]][4] - if role == Qt.ItemDataRole.BackgroundRole: - return QtGui.QColor("#0F80FF7F") - if role == Qt.ItemDataRole.TextAlignmentRole: - return Qt.AlignmentFlag.AlignCenter - return None - - # lower left contains dut info - if index.row() >= len(self.vheader_base) and index.column() < len(self.hheader_base): - fileStr, dutIndStr = self.vheader_ext[index.row() - len(self.vheader_base)].split(" ") - fid = int(fileStr.strip("File")) - dutIndex = int(dutIndStr.strip("#")) - # dut info can be 3-element or 10-element - # depending on which table is using this model - dutInfoTup = self.dutInfoMap[fid][dutIndex] - # flag string is always the last element - flagStr = dutInfoTup[-1] + return infoTuple[4] - if role == Qt.ItemDataRole.DisplayRole: - return dutInfoTup[index.column()] - - if role == Qt.ItemDataRole.ForegroundRole: - if flagStr.startswith("Fail") or flagStr.startswith("Supersede"): - # set to font color to white - return QtGui.QColor(WHITE_COLOR) - - if role == Qt.ItemDataRole.BackgroundRole: + case Qt.ItemDataRole.BackgroundRole: + return QtGui.QColor("#0F80FF7F") + + case Qt.ItemDataRole.TextAlignmentRole: + return Qt.AlignmentFlag.AlignCenter + + return None + + def dataOfDutInfo(self, index: QModelIndex, role: int): + fileStr, dutIndStr = self.vheader_ext[index.row() - len(self.vheader_base)].split(" ") + fid = int(fileStr.strip("File")) + dutIndex = int(dutIndStr.strip("#")) + # dut info can be 3-element or 10-element + # depending on which table is using this model + dutInfoTup = self.dutInfoMap[fid][dutIndex] + # flag string is always the last element + flagStr = dutInfoTup[-1] + isDutFail = flagStr.startswith("Fail") + isDutRplc = flagStr.startswith("Supersede") + isDutUnkn = flagStr.startswith("Unknown") + + match role: + case Qt.ItemDataRole.DisplayRole: + return dutInfoTup[index.column()] + + case Qt.ItemDataRole.ForegroundRole: + if isDutFail or isDutRplc: + # set to font color to white + return QtGui.QColor(WHITE_COLOR) + + case Qt.ItemDataRole.BackgroundRole: + if isDutFail: # mark fail row as red - if flagStr.startswith("Fail"): return QtGui.QColor(FAIL_DUT_COLOR) + return QtGui.QColor(FAIL_DUT_COLOR) + elif isDutRplc: # mark superseded row as gray - elif flagStr.startswith("Supersede"): return QtGui.QColor(OVRD_DUT_COLOR) + return QtGui.QColor(OVRD_DUT_COLOR) + elif isDutUnkn: # mark unknown as orange - elif flagStr.startswith("Unknown"): return QtGui.QColor(UNKN_DUT_COLOR) - - if role == Qt.ItemDataRole.FontRole: - return self.font - if role == Qt.ItemDataRole.TextAlignmentRole: - return Qt.AlignmentFlag.AlignCenter - if role == Qt.ItemDataRole.ToolTipRole: - if not flagStr.startswith("Pass"): - # get flag number - numStr = flagStr.split("-")[-1] - tip = dut_flag_parser(numStr) - if flagStr.startswith("Supersede"): - tip = "This dut is replaced by other dut\n" + tip - return tip - return None - - # lower right contains test data - if index.row() >= len(self.vheader_base): - # get test data indexes - test_index = index.column() - len(self.hheader_base) - fileStr, dutIndStr = self.vheader_ext[index.row() - len(self.vheader_base)].split(" ") - fid = int(fileStr.strip("File")) - dutIndex = int(dutIndStr.strip("#")) - # dict is empty if current fid doesn't contains `self.testLists[test_index]` - data_test_file: dict = self.testData[self.testLists[test_index]][fid] - data_ind = self.dutIndMap[fid].get(dutIndex, -1) + return QtGui.QColor(UNKN_DUT_COLOR) + + case Qt.ItemDataRole.FontRole: + return self.font + + case Qt.ItemDataRole.TextAlignmentRole: + return Qt.AlignmentFlag.AlignCenter + + case Qt.ItemDataRole.ToolTipRole: + if isDutFail or isDutRplc or isDutUnkn: + # get flag number + numStr = flagStr.split("-")[-1] + tip = dut_flag_parser(numStr) + if isDutRplc: + tip = "This dut is replaced by other dut\n" + tip + return tip + return None + + def dataOfTestData(self, index: QModelIndex, role: int): + # get test data indexes + test_index = index.column() - len(self.hheader_base) + fileStr, dutIndStr = self.vheader_ext[index.row() - len(self.vheader_base)].split(" ") + fid = int(fileStr.strip("File")) + dutIndex = int(dutIndStr.strip("#")) + # dict is empty if current fid doesn't contains `self.testLists[test_index]` + data_test_file: dict = self.testData[self.testLists[test_index]][fid] + data_ind = self.dutIndMap[fid].get(dutIndex, -1) + emptyTest = len(data_test_file) == 0 + + match role: + case Qt.ItemDataRole.DisplayRole: + if data_ind == -1 or emptyTest: + # test not exist in current file + return "Not Tested" - if role == Qt.ItemDataRole.DisplayRole: - if data_ind == -1 or len(data_test_file) == 0: - # test not exist in current file - return "Not Tested" - recHeader = data_test_file["recHeader"] - if recHeader == REC.FTR: - data = data_test_file["dataList"][data_ind] + dataList = data_test_file["dataList"] + flagList = data_test_file["flagList"] + data = dataList[data_ind] if data_ind < len(dataList) else np.nan + match data_test_file["recHeader"]: + case REC.FTR: return "Not Tested" if np.isnan(data) or data < 0 else f"Test Flag: {int(data)}" - elif recHeader == REC.PTR: - data = data_test_file["dataList"][data_ind] + + case REC.PTR: return "Not Tested" if np.isnan(data) else self.floatFormat % data - else: + + case REC.MPR: # MPR - if data_test_file["dataList"].size == 0: + if dataList.size == 0: # No PMR related and no test data in MPR, use test flag instead - flag = data_test_file["flagList"][data_ind] + flag = flagList[data_ind] return "Not Tested" if flag < 0 else f"Test Flag: {flag}" else: - data = data_test_file["dataList"][data_ind] return "Not Tested" if np.isnan(data) else self.floatFormat % data - - if role == Qt.ItemDataRole.ForegroundRole: - # only if failed - if (data_ind != -1 and - len(data_test_file) != 0 and - not isPass(data_test_file["flagList"][data_ind])): - return QtGui.QColor(WHITE_COLOR) - - if role == Qt.ItemDataRole.BackgroundRole: - # only if failed - if (data_ind != -1 and - len(data_test_file) != 0 and - not isPass(data_test_file["flagList"][data_ind])): - return QtGui.QColor(FAIL_DUT_COLOR) - - if role == Qt.ItemDataRole.TextAlignmentRole: - return Qt.AlignmentFlag.AlignCenter - - if role == Qt.ItemDataRole.ToolTipRole: - if data_ind == -1 or len(data_test_file) == 0: - return None - recHeader = data_test_file["recHeader"] - flag = data_test_file["flagList"][data_ind] - flagTip = test_flag_parser(flag) - if recHeader == REC.MPR: - RTNStat = data_test_file["stateList"][data_ind] - statTip = return_state_parser(RTNStat) - return "\n".join([t for t in [statTip, flagTip] if t]) - else: - # PTR & FTR - if flagTip: - return flagTip - except (IndexError, KeyError): - pass + case Qt.ItemDataRole.ForegroundRole: + # only if failed + if (data_ind != -1 and + not emptyTest and + not isPass(data_test_file["flagList"][data_ind])): + return QtGui.QColor(WHITE_COLOR) + + case Qt.ItemDataRole.BackgroundRole: + # only if failed + if (data_ind != -1 and + not emptyTest and + not isPass(data_test_file["flagList"][data_ind])): + return QtGui.QColor(FAIL_DUT_COLOR) + + case Qt.ItemDataRole.TextAlignmentRole: + return Qt.AlignmentFlag.AlignCenter + case Qt.ItemDataRole.ToolTipRole: + if data_ind == -1 or emptyTest: + return None + + flag = data_test_file["flagList"][data_ind] + flagTip = test_flag_parser(flag) + + if data_test_file["recHeader"] == REC.MPR: + RTNStat = data_test_file["stateList"][data_ind] + statTip = return_state_parser(RTNStat) + return "\n".join([t for t in [statTip, flagTip] if t]) + else: + return flagTip return None - + def flags(self, index: QModelIndex) -> Qt.ItemFlags: if index.row() >= len(self.vheader_base): # dut info + data section @@ -654,7 +671,7 @@ def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) return "" -class BinWaferTableModel(QtCore.QAbstractTableModel): +class BinWaferTableModel(TestStatisticTableModel): ''' content: 2D list of tuple ("Display String", bin_number, isHBIN), if bin_num is -1, indicating it's not related to HBIN or SBIN, @@ -663,29 +680,13 @@ class BinWaferTableModel(QtCore.QAbstractTableModel): ''' def __init__(self): super().__init__() - self.content = [] - self.hheader = [] - self.vheader = [] self.hbin_color = {} self.sbin_color = {} - self.colLen = 0 - - def setContent(self, content: list): - self.content = content def setColorDict(self, hbin_color: dict, sbin_color: dict): self.hbin_color = hbin_color self.sbin_color = sbin_color - def setColumnCount(self, colLen: int): - self.colLen = colLen - - def setHHeader(self, hheader: list): - self.hheader = hheader - - def setVHeader(self, vheader: list): - self.vheader = vheader - def data(self, index: QModelIndex, role: int): try: item: tuple = self.content[index.row()][index.column()] @@ -712,35 +713,8 @@ def data(self, index: QModelIndex, role: int): return getProperFontColor(background) return None - - def flags(self, index: QModelIndex) -> Qt.ItemFlags: - try: - _ = self.content[index.row()][index.column()] - return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled - except IndexError: - return Qt.ItemFlag.NoItemFlags - - def rowCount(self, parent=None) -> int: - return len(self.content) - - def columnCount(self, parent=None) -> int: - return self.colLen - - def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...): - if role != Qt.ItemDataRole.DisplayRole: - return None - - if orientation == Qt.Orientation.Horizontal: - header = self.hheader - else: - header = self.vheader - - try: - return header[section] - except IndexError: - return "" - - + + class MergeTableModel(QtCore.QAbstractTableModel): ''' For displaying STDF MIR records in merge panel @@ -828,5 +802,6 @@ def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) "ColorSqlQueryModel", "DatalogSqlQueryModel", "TestDataTableModel", "TestStatisticTableModel", "BinWaferTableModel", "MergeTableModel", + "DutTableColIndex" ]