-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #129 from noonchen/PrepareReleaseV4.0.0
Prepare release v4.0.0
- Loading branch information
Showing
15 changed files
with
2,468 additions
and
145 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ | |
# Author: noonchen - [email protected] | ||
# Created Date: December 13th 2020 | ||
# ----- | ||
# Last Modified: Thu Dec 08 2022 | ||
# Last Modified: Mon Dec 12 2022 | ||
# Modified By: noonchen | ||
# ----- | ||
# Copyright (c) 2020 noonchen | ||
|
@@ -41,7 +41,8 @@ | |
from deps.uic_stdSettings import stdfSettings | ||
from deps.uic_stdDutData import DutDataDisplayer | ||
from deps.uic_stdDebug import stdDebugPanel | ||
|
||
from deps.uic_stdConverter import StdfConverter | ||
import rust_stdf_helper | ||
# pyqt5 | ||
from deps.ui.stdfViewer_MainWindows import Ui_MainWindow | ||
from PyQt5 import QtCore, QtWidgets, QtGui, QtSql | ||
|
@@ -79,6 +80,9 @@ | |
class signals4MainUI(QtCore.QObject): | ||
dataInterfaceSignal = Signal(object) # get `DataInterface` from loader | ||
statusSignal = Signal(str, bool, bool, bool) # status bar | ||
showDutDataSignal_TrendHisto = Signal(list) # trend & histo | ||
showDutDataSignal_Bin = Signal(list) # bin chart | ||
showDutDataSignal_Wafer = Signal(list) # wafer | ||
|
||
|
||
class MyWindow(QtWidgets.QMainWindow): | ||
|
@@ -108,6 +112,9 @@ def __init__(self): | |
self.signals = signals4MainUI() | ||
self.signals.dataInterfaceSignal.connect(self.updateData) | ||
self.signals.statusSignal.connect(self.updateStatus) | ||
self.signals.showDutDataSignal_TrendHisto.connect(self.onReadDutData_TrendHisto) | ||
self.signals.showDutDataSignal_Bin.connect(self.onReadDutData_Bin) | ||
self.signals.showDutDataSignal_Wafer.connect(self.onReadDutData_Wafer) | ||
# sub windows | ||
self.loader = stdfLoader(self.signals, self) | ||
self.mergePanel = MergePanel(self) | ||
|
@@ -134,6 +141,8 @@ def __init__(self): | |
self.ui.actionAbout.triggered.connect(self.onAbout) | ||
self.ui.actionReadDutData_DS.triggered.connect(self.onReadDutData_DS) | ||
self.ui.actionReadDutData_TS.triggered.connect(self.onReadDutData_TS) | ||
self.ui.actionFetchDuts.triggered.connect(lambda: self.onFetchAllRows(self.ui.dutInfoTable)) | ||
self.ui.actionFetchDatalog.triggered.connect(lambda: self.onFetchAllRows(self.ui.datalogTable)) | ||
self.ui.actionAddFont.triggered.connect(self.onAddFont) | ||
self.ui.actionToXLSX.triggered.connect(self.onToXLSX) | ||
# init search-related UI | ||
|
@@ -316,8 +325,12 @@ def onLoadSession(self): | |
directory=getSetting().recentFolder, | ||
filter=self.tr("Database (*.db)")) | ||
if p: | ||
#TODO validation | ||
self.loadDatabase(p) | ||
isvalid, msg = validateSession(p) | ||
if isvalid: | ||
self.loadDatabase(p) | ||
else: | ||
QMessageBox.warning(self, self.tr("Warning"), | ||
self.tr("This session cannot be loaded: \n{}\n\n{}").format(p, msg)) | ||
|
||
|
||
def onSaveSession(self): | ||
|
@@ -406,11 +419,31 @@ def onAbout(self): | |
|
||
|
||
def onAddFont(self): | ||
QMessageBox.information(self, self.tr("Notice"), self.tr("Not implemented yet...")) | ||
p, _ = QFileDialog.getOpenFileName(self, caption=self.tr("Select a .ttf font file"), | ||
directory=getSetting().recentFolder, | ||
filter=self.tr("TTF Font (*.ttf)")) | ||
if not p: | ||
return | ||
|
||
if QtGui.QFontDatabase.addApplicationFont(p) < 0: | ||
QMessageBox.warning(self, self.tr("Warning"), self.tr("This font cannot be loaded:\n{}").format(p)) | ||
else: | ||
shutil.copy(src=p, | ||
dst=os.path.join(sys.rootFolder, "fonts"), | ||
follow_symlinks=True) | ||
# manually refresh font list | ||
loadFonts() | ||
self.settingUI.refreshFontList() | ||
QMessageBox.information(self, self.tr("Completed"), self.tr("Load successfully, change font in settings to take effect")) | ||
|
||
|
||
def onToXLSX(self): | ||
QMessageBox.information(self, self.tr("Notice"), self.tr("Not implemented yet...")) | ||
cv = StdfConverter(self) | ||
cv.setupConverter(self.tr("STDF to XLSX Converter"), | ||
self.tr("XLSX Path Selection"), | ||
".xlsx", | ||
rust_stdf_helper.stdf_to_xlsx) | ||
cv.showUI() | ||
|
||
|
||
def onExit(self): | ||
|
@@ -487,6 +520,47 @@ def onReadDutData_TS(self): | |
QMessageBox.information(None, self.tr("No DUTs selected"), self.tr("You need to select DUT row(s) first"), buttons=QMessageBox.Ok) | ||
|
||
|
||
def onFetchAllRows(self, activeTable: QtWidgets.QTableView): | ||
model = activeTable.model() | ||
if isinstance(model, DutSortFilter): | ||
# dut summary uses proxy model | ||
model = model.sourceModel() | ||
if isinstance(model, QtSql.QSqlQueryModel): | ||
self.signals.statusSignal.emit(self.tr("Fetching all..."), False, False, False) | ||
while model.canFetchMore(): | ||
model.fetchMore() | ||
self.signals.statusSignal.emit(self.tr("Fetch Done!"), False, False, False) | ||
|
||
|
||
@Slot(list) | ||
def onReadDutData_TrendHisto(self, selectedDutIndex: list): | ||
''' | ||
selectedDutIndex: a list of (fid, dutIndex) | ||
''' | ||
if selectedDutIndex: | ||
self.showDutDataTable(selectedDutIndex) | ||
|
||
|
||
@Slot(list) | ||
def onReadDutData_Bin(self, selectedBin: list): | ||
''' | ||
selectedBin: a list of (fid, isHBIN, [bin_num]) | ||
''' | ||
selectedDutIndex = self.data_interface.DatabaseFetcher.getDUTIndexFromBin(selectedBin) | ||
if selectedDutIndex: | ||
self.showDutDataTable(selectedDutIndex) | ||
|
||
|
||
@Slot(list) | ||
def onReadDutData_Wafer(self, selectedDie: list): | ||
''' | ||
selectedDie: a list of (waferInd, fid, (x, y)) | ||
''' | ||
selectedDutIndex = self.data_interface.DatabaseFetcher.getDUTIndexFromXY(selectedDie) | ||
if selectedDutIndex: | ||
self.showDutDataTable(selectedDutIndex) | ||
|
||
|
||
def enableDragDrop(self): | ||
for obj in [self.ui.TestList, self.ui.tabControl, self.ui.dataTable]: | ||
obj.setAcceptDrops(True) | ||
|
@@ -553,7 +627,9 @@ def init_DataTable(self): | |
# datalog info table | ||
self.tmodel_datalog = DatalogSqlQueryModel(self, 13 if isMac else 10) | ||
self.ui.datalogTable.setModel(self.tmodel_datalog) | ||
self.ui.datalogTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) # select row only | ||
self.ui.datalogTable.setItemDelegate(StyleDelegateForTable_List(self.ui.datalogTable)) | ||
self.ui.datalogTable.addAction(self.ui.actionFetchDatalog) | ||
# test data table | ||
self.tmodel_data = TestDataTableModel() | ||
self.ui.rawDataTable.setModel(self.tmodel_data) | ||
|
@@ -568,6 +644,7 @@ def init_DataTable(self): | |
self.ui.dutInfoTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) # select row only | ||
self.ui.dutInfoTable.setItemDelegate(StyleDelegateForTable_List(self.ui.dutInfoTable)) | ||
self.ui.dutInfoTable.addAction(self.ui.actionReadDutData_DS) # add context menu for reading dut data | ||
self.ui.dutInfoTable.addAction(self.ui.actionFetchDuts) | ||
# file header table | ||
self.tmodel_info = QtGui.QStandardItemModel() | ||
self.ui.fileInfoTable.setModel(self.tmodel_info) | ||
|
@@ -584,6 +661,7 @@ def init_DataTable(self): | |
self.ui.dutInfoTable.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) | ||
self.ui.dutInfoTable.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) | ||
self.ui.fileInfoTable.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) | ||
self.ui.fileInfoTable.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) | ||
|
||
|
||
def init_Head_SiteCheckbox(self): | ||
|
@@ -986,20 +1064,23 @@ def genPlot(self, testTuple: tuple, head: int, selectSites: list[int], tabType: | |
tchart = TrendChart() | ||
tchart.setTrendData(tdata) | ||
if tchart.validData: | ||
tchart.setShowDutSignal(self.signals.showDutDataSignal_TrendHisto) | ||
return tchart | ||
|
||
elif tabType == tab.Histo: | ||
tdata = self.data_interface.getTrendChartData(testTuple, head, selectSites) | ||
hchart = HistoChart() | ||
hchart.setTrendData(tdata) | ||
if hchart.validData: | ||
hchart.setShowDutSignal(self.signals.showDutDataSignal_TrendHisto) | ||
return hchart | ||
|
||
elif tabType == tab.Wafer: | ||
wdata = self.data_interface.getWaferMapData(testTuple, selectSites) | ||
wchart = WaferMap() | ||
wchart.setWaferData(wdata) | ||
if wchart.validData: | ||
wchart.setShowDutSignal(self.signals.showDutDataSignal_Wafer) | ||
return wchart | ||
|
||
elif tabType == tab.Bin: | ||
|
@@ -1010,6 +1091,7 @@ def genPlot(self, testTuple: tuple, head: int, selectSites: list[int], tabType: | |
bchart = BinChart() | ||
bchart.setBinData(bdata) | ||
if bchart.validData: | ||
bchart.setShowDutSignal(self.signals.showDutDataSignal_Bin) | ||
bcharts.append(bchart) | ||
return bcharts | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ | |
# Author: noonchen - [email protected] | ||
# Created Date: November 25th 2022 | ||
# ----- | ||
# Last Modified: Sun Dec 11 2022 | ||
# Last Modified: Mon Dec 12 2022 | ||
# Modified By: noonchen | ||
# ----- | ||
# Copyright (c) 2022 noonchen | ||
|
@@ -219,8 +219,7 @@ def onShowDut(self): | |
selectedData = [] | ||
for view in self.view_list: | ||
selectedData.extend(view.getSelectedDataForDutTable()) | ||
#TODO send data to main UI | ||
print(selectedData) | ||
# send data to main UI | ||
if self.showDutSignal: | ||
self.showDutSignal.emit(selectedData) | ||
|
||
|
@@ -382,7 +381,9 @@ def getSelectedDataForDutTable(self): | |
dataSet = set() | ||
dutIndexArray, _ = self.slpoints.getData() | ||
# remove duplicates | ||
_ = [dataSet.add((self.fileID, i)) for i in dutIndexArray] | ||
# array is float type, must convert to int | ||
# otherwise will cause crash in `TestDataTable` | ||
_ = [dataSet.add((self.fileID, int(i))) for i in dutIndexArray] | ||
return list(dataSet) | ||
|
||
|
||
|
@@ -505,14 +506,14 @@ def getSelectedDataForDutTable(self) -> list: | |
class BinViewBox(SVBarViewBox): | ||
def getSelectedDataForDutTable(self) -> list: | ||
''' | ||
For BinChart, return [(fid, isHBIN, duts)] | ||
For BinChart, return [(fid, isHBIN, bins)] | ||
''' | ||
tmp = {} | ||
for _, binTup in self.slbars.getRects(): | ||
isHBIN, bin_num = binTup | ||
tmp.setdefault( (self.fileID, isHBIN), []).append(bin_num) | ||
return [(fid, isHBIN, duts) | ||
for (fid, isHBIN), duts | ||
return [(fid, isHBIN, bins) | ||
for (fid, isHBIN), bins | ||
in tmp.items()] | ||
|
||
|
||
|
@@ -559,7 +560,7 @@ def getSelectedDataForDutTable(self): | |
# remove duplicates | ||
_ = [dataSet.add((self.waferInd, | ||
self.fileID, | ||
(x, y))) | ||
(int(x), int(y)))) | ||
for x, y | ||
in zip(xData, yData)] | ||
return list(dataSet) | ||
|
@@ -602,7 +603,11 @@ def setTrendData(self, trendData: dict): | |
y_min_list.extend([i_file["LLimit"], i_file["LSpec"]]) | ||
y_max_list.extend([i_file["HLimit"], i_file["HSpec"]]) | ||
for d_site in d_file.values(): | ||
# TODO dynamic limits | ||
# dynamic limits | ||
if d_site.get("dyLLimit", {}): | ||
y_min_list.append(min(d_site["dyLLimit"].values())) | ||
if d_site.get("dyHLimit", {}): | ||
y_max_list.append(max(d_site["dyHLimit"].values())) | ||
y_min_list.append(d_site["Min"]) | ||
y_max_list.append(d_site["Max"]) | ||
# at least one site data should be valid | ||
|
@@ -612,10 +617,6 @@ def setTrendData(self, trendData: dict): | |
# set the flag to True and put it in top GUI | ||
if not self.validData: | ||
return | ||
# # if these two list is empty, means no test value | ||
# # is found in all files. this is is rare but I've encountered | ||
# if len(y_min_list) == 0 or len(y_max_list) == 0: | ||
# return | ||
y_min = np.nanmin(y_min_list) | ||
y_max = np.nanmax(y_max_list) | ||
# add 15% as overhead | ||
|
@@ -627,10 +628,6 @@ def setTrendData(self, trendData: dict): | |
if y_min == y_max: | ||
y_min -= 1 | ||
y_max += 1 | ||
# # there is a possibility that y_min or y_max | ||
# # is nan, in this case we cannot draw anything | ||
# if np.isnan(y_min) or np.isnan(y_max): | ||
# return | ||
# add title | ||
self.plotlayout.addLabel(f"{test_num} {test_name}", row=0, col=0, | ||
rowspan=1, colspan=len(testInfo), | ||
|
@@ -655,6 +652,8 @@ def setTrendData(self, trendData: dict): | |
continue | ||
x_min_list = [] | ||
x_max_list = [] | ||
dyL = {} | ||
dyH = {} | ||
for site, data_per_site in sitesData.items(): | ||
x = data_per_site["dutList"] | ||
y = data_per_site["dataList"] | ||
|
@@ -664,6 +663,8 @@ def setTrendData(self, trendData: dict): | |
continue | ||
x_min_list.append(np.nanmin(x)) | ||
x_max_list.append(np.nanmax(x)) | ||
dyL.update(data_per_site.get("dyLLimit", {})) | ||
dyH.update(data_per_site.get("dyHLimit", {})) | ||
fsymbol = settings.fileSymbol[fid] | ||
siteColor = settings.siteColor[site] | ||
# test value | ||
|
@@ -701,6 +702,13 @@ def setTrendData(self, trendData: dict): | |
label=f"{name} = {{value:0.2f}}", | ||
labelOpts={"position":pos, "color": pen.color(), | ||
"movable": True, "anchors": anchors}) | ||
# dynamic limits | ||
for (dyDict, name, pen, enabled) in [(dyL, "Dynamic Low Limit", self.lolimitPen, settings.showLL_trend), | ||
(dyH, "Dynamic High Limit", self.hilimitPen, settings.showHL_trend)]: | ||
if enabled and len(dyDict) > 0: | ||
x = np.array(sorted(dyDict.keys())) | ||
dylims = np.array([dyDict[i] for i in x]) | ||
pitem.addItem(pg.PlotDataItem(x=x, y=dylims, pen=pen, name=name, color=pen.color())) | ||
# labels and file id | ||
unit = infoDict["Unit"] | ||
pitem.getAxis("left").setLabel(f"Test Value" + f" ({unit})" if unit else "") | ||
|
@@ -1009,7 +1017,10 @@ def setWaferData(self, waferData: dict): | |
|
||
settings = ss.getSetting() | ||
self.validData = True | ||
waferInd, fid = waferData["ID"] | ||
waferView = WaferViewBox() | ||
waferView.setWaferIndex(waferInd) | ||
waferView.setFileID(fid) | ||
pitem = pg.PlotItem(viewBox=waferView) | ||
# put legend in another view | ||
view_legend = pg.ViewBox() | ||
|
@@ -1020,15 +1031,14 @@ def setWaferData(self, waferData: dict): | |
verSpacing=5, | ||
labelTextSize="15pt") | ||
xyData = waferData["Data"] | ||
isStackMap = waferData["Stack"] | ||
stackColorMap = pg.ColorMap(pos=None, color=["#00EE00", "#EEEE00", "#EE0000"]) | ||
sortedKeys = sorted(xyData.keys()) | ||
|
||
for num in sortedKeys: | ||
xyDict = xyData[num] | ||
# for stack map, num = fail counts | ||
# for wafer map, num = sbin number | ||
if isStackMap: | ||
if waferInd == -1: | ||
color = stackColorMap.mapToQColor(num/sortedKeys[-1]) | ||
tipFunc = f"XY: ({{x:.0f}}, {{y:.0f}})\nFail Count: {num}".format | ||
legendString = f"Fail Count: {num}" | ||
|
Oops, something went wrong.