diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b42097e86 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/__pycache__/ \ No newline at end of file diff --git a/QtBrainChartGUI/plugins/computeSPAREs/computeSPAREs.py b/QtBrainChartGUI/plugins/computeSPAREs/computeSPAREs.py index a9f3ca5c5..8bd4d9dd4 100644 --- a/QtBrainChartGUI/plugins/computeSPAREs/computeSPAREs.py +++ b/QtBrainChartGUI/plugins/computeSPAREs/computeSPAREs.py @@ -3,7 +3,7 @@ from yapsy.IPlugin import IPlugin from PyQt5 import QtGui, QtCore, QtWidgets, uic import joblib -import sys, os +import sys, os, time import seaborn as sns import numpy as np @@ -23,6 +23,7 @@ def __init__(self): self.plotCanvas.axes = self.plotCanvas.fig.add_subplot(111) self.SPAREs = None self.ui.stackedWidget.setCurrentIndex(0) + self.ui.factorial_progressBar.setValue(0) def getUI(self): @@ -43,17 +44,20 @@ def SetupConnections(self): # are present in data frame if ('SPARE_BA' in self.datamodel.GetColumnHeaderNames() and 'SPARE_AD' in self.datamodel.GetColumnHeaderNames()): + self.ui.show_SPARE_scores_from_data_Btn.setStyleSheet("background-color: rgb(230,230,255)") self.ui.show_SPARE_scores_from_data_Btn.setEnabled(True) + self.ui.show_SPARE_scoresfrom_data_Btn.setToolTip('The data frame has variables `SPARE_AD` and `SPARE_BA` so these can be plotted.') else: self.ui.show_SPARE_scores_from_data_Btn.setEnabled(False) - # Allow loading of SPARE-* model only when harmonized residuals are - # present - if 'RES_ICV_Sex_MUSE_Volume_47' in self.datamodel.GetColumnHeaderNames(): - self.ui.load_SPARE_model_Btn.setEnabled(True) - else: - self.ui.load_SPARE_model_Btn.setEnabled(False) + # Allow loading of SPARE-* model always, even when residuals are not + # calculated yet + self.ui.load_SPARE_model_Btn.setEnabled(True) + + def updateProgress(self, txt, vl): + self.ui.SPARE_computation_info.setText(txt) + self.ui.factorial_progressBar.setValue(vl) def OnLoadSPAREModel(self): @@ -64,25 +68,56 @@ def OnLoadSPAREModel(self): if fileName != "": self.model['BrainAge'], self.model['AD'] = joblib.load(fileName) self.ui.compute_SPARE_scores_Btn.setEnabled(True) + self.ui.SPARE_model_info.setText('File: %s' % (fileName)) self.ui.stackedWidget.setCurrentIndex(0) - - def OnComputeSPAREs(self): if 'RES_ICV_Sex_MUSE_Volume_47' in self.datamodel.GetColumnHeaderNames(): - self.SPAREs = pd.DataFrame.from_dict({ - 'SPARE_BA': predictBrainAge(self.datamodel.data, self.model['BrainAge']), - 'SPARE_AD': predictAD(self.datamodel.data, self.model['AD'])}) - self.plotSPAREs() - self.ui.stackedWidget.setCurrentIndex(1) + self.ui.compute_SPARE_scores_Btn.setStyleSheet("background-color: rgb(230,255,230)") + self.ui.compute_SPARE_scores_Btn.setEnabled(True) + self.ui.compute_SPARE_scores_Btn.setToolTip('Model loaded and `RES_ICV_Sex_MUSE_Volmue_*` available so the MUSE volumes can be harmonized.') else: + self.ui.compute_SPARE_scores_Btn.setStyleSheet("background-color: rgb(255,230,230)") + self.ui.compute_SPARE_scores_Btn.setEnabled(False) + self.ui.compute_SPARE_scores_Btn.setToolTip('Model loaded but `RES_ICV_Sex_MUSE_Volmue_*` not available so the MUSE volumes can not be harmonized.') + + print('No field `RES_ICV_Sex_MUSE_Volume_47` found. ' + - 'Make sure to compute harmonized residuals first.') + 'Make sure to compute and add harmonized residuals first.') + + + def OnComputationDone(self, y_hat): + self.SPAREs = y_hat + self.plotSPAREs() + self.ui.stackedWidget.setCurrentIndex(1) + + + + def OnComputeSPAREs(self): + # Setup tasks for long running jobs + # Using this example: https://realpython.com/python-pyqt-qthread/ + self.thread = QtCore.QThread() + self.worker = BrainAgeWorker(self.datamodel.data, self.model) + self.worker.moveToThread(self.thread) + self.thread.started.connect(self.worker.run) + self.worker.done.connect(self.thread.quit) + self.worker.done.connect(self.worker.deleteLater) + self.thread.finished.connect(self.thread.deleteLater) + self.worker.progress.connect(self.updateProgress) + self.worker.done.connect(lambda y_hat: self.OnComputationDone(y_hat)) + self.ui.factorial_progressBar.setRange(0, len(self.model['BrainAge']['scaler'])-1) + self.thread.start() + self.ui.compute_SPARE_scores_Btn.setEnabled(False) def plotSPAREs(self): + # Plot data sns.scatterplot(x='SPARE_AD', y='SPARE_BA', data=self.SPAREs, - ax=self.plotCanvas.axes) + ax=self.plotCanvas.axes, linewidth=0, + facecolor=(0.5, 0.5, 0.5, 0.5), size=1, legend=None) + sns.despine(ax=self.plotCanvas.axes, trim=True) + self.plotCanvas.axes.set(ylabel='SPARE-BA', xlabel='SPARE-AD') + self.plotCanvas.axes.get_figure().set_tight_layout(True) def OnAddToDataFrame(self): @@ -103,58 +138,63 @@ def OnDataChanged(self): if ('SPARE_BA' in self.datamodel.GetColumnHeaderNames() and 'SPARE_AD' in self.datamodel.GetColumnHeaderNames()): self.ui.show_SPARE_scores_from_data_Btn.setEnabled(True) + self.ui.show_SPARE_scores_from_data_Btn.setStyleSheet("background-color: rgb(230,230,255)") else: self.ui.show_SPARE_scores_from_data_Btn.setEnabled(False) - # Allow loading of SPARE-* model only when harmonized residuals are - # present - if 'RES_ICV_Sex_MUSE_Volume_47' in self.datamodel.GetColumnHeaderNames(): - self.ui.load_SPARE_model_Btn.setEnabled(True) - else: - self.ui.load_SPARE_model_Btn.setEnabled(False) +class BrainAgeWorker(QtCore.QObject): + + done = QtCore.pyqtSignal(pd.DataFrame) + progress = QtCore.pyqtSignal(str, int) + + #constructor + def __init__(self, data, model): + super(BrainAgeWorker, self).__init__() + self.data = data + self.model = model + + def run(self): + y_hat = pd.DataFrame.from_dict({'SPARE_BA': np.full((self.data.shape[0],),np.nan), + 'SPARE_AD': np.full((self.data.shape[0],),np.nan)}) -def predictBrainAge(data,model): - - idx = ~data[model['predictors'][0]].isnull() + # SPARE-BA + idx = ~self.data[self.model['BrainAge']['predictors'][0]].isnull() - y_hat_test = np.zeros((np.sum(idx),)) - n_ensembles = np.zeros((np.sum(idx),)) + y_hat_test = np.zeros((np.sum(idx),)) + n_ensembles = np.zeros((np.sum(idx),)) - for i,_ in enumerate(model['scaler']): - # Predict validation (fold) and test - print('Fold %d' % (i)) - test = np.logical_not(data[idx]['participant_id'].isin(np.concatenate(model['train']))) | data[idx]['participant_id'].isin(model['validation'][i]) - X = data[idx].loc[test, model['predictors']].values - X = model['scaler'][i].transform(X) - y_hat_test[test] += (model['svm'][i].predict(X) - model['bias_ints'][i]) / model['bias_slopes'][i] - n_ensembles[test] += 1. + for i,_ in enumerate(self.model['BrainAge']['scaler']): + # Predict validation (fold) and test + self.progress.emit('Computing SPARE-BA | Task 1 of 2', i) + test = np.logical_not(self.data[idx]['participant_id'].isin(np.concatenate(self.model['BrainAge']['train']))) | self.data[idx]['participant_id'].isin(self.model['BrainAge']['validation'][i]) + X = self.data[idx].loc[test, self.model['BrainAge']['predictors']].values + X = self.model['BrainAge']['scaler'][i].transform(X) + y_hat_test[test] += (self.model['BrainAge']['svm'][i].predict(X) - self.model['BrainAge']['bias_ints'][i]) / self.model['BrainAge']['bias_slopes'][i] + n_ensembles[test] += 1. - y_hat_test /= n_ensembles - y_hat = np.full((data.shape[0],),np.nan) - y_hat[idx] = y_hat_test + y_hat_test /= n_ensembles + y_hat.loc[idx, 'SPARE_BA'] = y_hat_test - return y_hat + idx = ~self.data[self.model['BrainAge']['predictors'][0]].isnull() + y_hat_test = np.zeros((np.sum(idx),)) + n_ensembles = np.zeros((np.sum(idx),)) -def predictAD(data,model): - - idx = ~data[model['predictors'][0]].isnull() + for i,_ in enumerate(self.model['AD']['scaler']): + # Predict validation (fold) and test + self.progress.emit('Computing SPARE-AD | Task 2 of 2', i) - y_hat_test = np.zeros((np.sum(idx),)) - n_ensembles = np.zeros((np.sum(idx),)) + test = np.logical_not(self.data[idx]['participant_id'].isin(np.concatenate(self.model['AD']['train']))) | self.data[idx]['participant_id'].isin(self.model['AD']['validation'][i]) + X = self.data[idx].loc[test, self.model['AD']['predictors']].values + X = self.model['AD']['scaler'][i].transform(X) + y_hat_test[test] += self.model['AD']['svm'][i].decision_function(X) + n_ensembles[test] += 1. - for i,_ in enumerate(model['scaler']): - # Predict validation (fold) and test - print('Fold %d' % (i)) - test = np.logical_not(data[idx]['participant_id'].isin(np.concatenate(model['train']))) | data[idx]['participant_id'].isin(model['validation'][i]) - X = data[idx].loc[test, model['predictors']].values - X = model['scaler'][i].transform(X) - y_hat_test[test] += model['svm'][i].decision_function(X) - n_ensembles[test] += 1. + y_hat_test /= n_ensembles + y_hat.loc[idx, 'SPARE_AD'] = y_hat_test - y_hat_test /= n_ensembles - y_hat = np.full((data.shape[0],),np.nan) - y_hat[idx] = y_hat_test + self.progress.emit('All done.', i) - return y_hat + # Emit the result + self.done.emit(y_hat) diff --git a/QtBrainChartGUI/plugins/computeSPAREs/computeSPAREs.ui b/QtBrainChartGUI/plugins/computeSPAREs/computeSPAREs.ui index d74a721c6..de939006a 100644 --- a/QtBrainChartGUI/plugins/computeSPAREs/computeSPAREs.ui +++ b/QtBrainChartGUI/plugins/computeSPAREs/computeSPAREs.ui @@ -11,7 +11,7 @@ - Form + Compute SPAREs @@ -20,10 +20,23 @@ - 1 + 0 + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -34,6 +47,13 @@ + + + + Qt::Horizontal + + + @@ -41,6 +61,13 @@ + + + + No SPARE-* model loaded + + + @@ -51,6 +78,33 @@ + + + + No computation running + + + + + + + 24 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + +