@@ -2555,6 +3237,10 @@ def loadSettingsFile(self, settingsFile, checkExperimentType=True, silent=False)
self.ui.useSILRatioForConvolution.setChecked(self.to_bool(sett.value("useSILRatioForConvolution")))
if sett.contains("minConnectionRate"):
self.ui.minConnectionRate.setValue(self.to_double(sett.value("minConnectionRate")) * 100.0)
+ if sett.contains("useAbundanceSimilarityForConvolution"):
+ self.ui.useAbundanceSimilarityForConvolution.setChecked(self.to_bool(sett.value("useAbundanceSimilarityForConvolution")))
+ if sett.contains("abundanceSimilarityThreshold"):
+ self.ui.abundanceSimilarityThreshold.setValue(self.to_double(sett.value("abundanceSimilarityThreshold")) * 100.0)
if sett.contains("GroupIntegrateMissedPeaks"):
self.ui.integratedMissedPeaks.setChecked(self.to_bool(sett.value("GroupIntegrateMissedPeaks")))
@@ -2780,6 +3466,11 @@ def saveSettingsFile(self, settingsFile, clear=True):
self.ui.useSILRatioForConvolution.checkState() == QtCore.Qt.Checked,
)
sett.setValue("minConnectionRate", self.ui.minConnectionRate.value() / 100.0)
+ sett.setValue(
+ "useAbundanceSimilarityForConvolution",
+ self.ui.useAbundanceSimilarityForConvolution.checkState() == QtCore.Qt.Checked,
+ )
+ sett.setValue("abundanceSimilarityThreshold", self.ui.abundanceSimilarityThreshold.value() / 100.0)
sett.setValue("GroupIntegrateMissedPeaks", self.ui.integratedMissedPeaks.isChecked())
sett.setValue(
@@ -3116,6 +3807,18 @@ def runProcess(self, dontSave=False, askStarting=True):
errorCount = 0
+ # Per-step tracking for the final summary dialog
+ _ST_SKIPPED_USER = "skipped_user"
+ _ST_OK = "ok"
+ _ST_ERROR = "error"
+ _ST_SKIPPED_PREV = "skipped_prev_error"
+ _step_status = {k: _ST_SKIPPED_USER for k in ("individual_files", "bracketing", "reintegration", "convolution", "annotation")}
+ _step_elapsed = {k: 0.0 for k in _step_status}
+ _step_details = {k: "" for k in _step_status}
+ _bracketing_failed = False
+ _reintegration_failed = False
+ _convolution_failed = False
+
cpus = min(cpu_count(), self.ui.cpuCores.value())
if self.terminateJobs:
@@ -3137,6 +3840,7 @@ def runProcess(self, dontSave=False, askStarting=True):
# process individual files
if self.ui.processIndividualFiles.isChecked():
+ _ind_step_start = time.time()
logging.info("")
logging.info("Processing %d individual LC-HRMS data files on %d CPU core(s).." % (len(files), min(len(files), cpus)))
@@ -3264,81 +3968,79 @@ def runProcess(self, dontSave=False, askStarting=True):
completed = res._index
if completed == len(files):
loop = False
- else:
- pwMain("value")(completed)
-
- mess = {}
- while not (queue.empty()):
- mes = queue.get(block=False, timeout=1)
- if mes.pid not in mess:
- mess[mes.pid] = {}
- mess[mes.pid][mes.mes] = mes
-
- for v in mess.values():
- if "start" in v.keys():
- mes = v["start"]
- if len(freeSlots) > 0:
- w = freeSlots.pop()
- assignedThreads[mes.pid] = w
-
- pw.getCallingFunction()("statuscolor")(pIds[mes.pid], "orange")
- pw.getCallingFunction()("statustext")(
- pIds[mes.pid],
- text="File: %s\nStatus: %s\nProcess ID: %d" % (pIds[mes.pid], "processing", mes.pid),
- )
- else:
- logging.error("Something went wrong..")
- logging.error('Progress bars do not work correctly, but files will be processed and "finished.." will be printed..')
-
- for v in mess.values():
- for mes in v.values():
- if mes.mes == "log":
- messages_to_print[mes.pid].append(mes.val)
- elif mes.mes in ["text", "max", "value"]:
- if mes.pid in assignedThreads:
- pw.getCallingFunction(assignedThreads[mes.pid])(mes.mes)(mes.val)
- else:
- logging.error("Error in messaging pipeline of subprocess id %d" % mes.pid)
- elif mes.mes in ["end", "failed", "start"]:
- pass
- else:
- logging.error(f"Received unknown message {mes.mes} with payload {mes.__dict__}")
-
- for v in mess.values():
- if "end" in v.keys() or "failed" in v.keys():
- mes = None
- if "end" in v.keys():
- mes = v["end"]
- elif "failed" in v.keys():
- mes = v["failed"]
- failedFiles.append(pIds[mes.pid])
- freeS = assignedThreads[mes.pid]
- pw.getCallingFunction(assignedThreads[mes.pid])("text")("")
- pw.getCallingFunction(assignedThreads[mes.pid])("value")(0)
-
- logging.info("\n##############################################################")
- logging.info("\n".join(messages_to_print[mes.pid]))
- logging.info("##############################################################\n")
-
- pw.getCallingFunction()("statuscolor")(
- pIds[mes.pid],
- "olivedrab" if mes.mes == "end" else "firebrick",
- )
+
+ pwMain("value")(completed)
+
+ mess = {}
+ while not (queue.empty()):
+ mes = queue.get(block=False, timeout=1)
+ if mes.pid not in mess:
+ mess[mes.pid] = {}
+ mess[mes.pid][mes.mes] = mes
+
+ for v in mess.values():
+ if "start" in v.keys():
+ mes = v["start"]
+ if len(freeSlots) > 0:
+ w = freeSlots.pop()
+ assignedThreads[mes.pid] = w
+
+ pw.getCallingFunction()("statuscolor")(pIds[mes.pid], "orange")
pw.getCallingFunction()("statustext")(
pIds[mes.pid],
- text="File: %s\nStatus: %s"
- % (
- pIds[mes.pid],
- "finished" if mes.mes == "end" else "failed",
- ),
+ text="File: %s\nStatus: %s\nProcess ID: %d" % (pIds[mes.pid], "processing", mes.pid),
)
-
- if freeS == -1:
- logging.error("Something went wrong..")
- logging.error('Progress bars do not work correctly, but files will be processed and "finished.." will be printed..')
+ else:
+ logging.error("Something went wrong..")
+ logging.error('Progress bars do not work correctly, but files will be processed and "finished.." will be printed..')
+
+ for v in mess.values():
+ for mes in v.values():
+ if mes.mes == "log":
+ messages_to_print[mes.pid].append(mes.val)
+ elif mes.mes in ["text", "max", "value"]:
+ if mes.pid in assignedThreads:
+ pw.getCallingFunction(assignedThreads[mes.pid])(mes.mes)(mes.val)
else:
- assignedThreads[mes.pid] = -1
- freeSlots.append(freeS)
+ logging.error("Error in messaging pipeline of subprocess id %d" % mes.pid)
+ elif mes.mes in ["end", "failed", "start"]:
+ pass
+ else:
+ logging.error(f"Received unknown message {mes.mes} with payload {mes.__dict__}")
+
+ for v in mess.values():
+ if "end" in v.keys() or "failed" in v.keys():
+ mes = None
+ if "end" in v.keys():
+ mes = v["end"]
+ elif "failed" in v.keys():
+ mes = v["failed"]
+ failedFiles.append(pIds[mes.pid])
+ freeS = assignedThreads[mes.pid]
+ pw.getCallingFunction(assignedThreads[mes.pid])("text")("")
+ pw.getCallingFunction(assignedThreads[mes.pid])("value")(0)
+
+ logging.info("\n" + "##############################################################\n" + "\n".join(messages_to_print[mes.pid]) + "\n" + "##############################################################\n" + "")
+
+ pw.getCallingFunction()("statuscolor")(
+ pIds[mes.pid],
+ "olivedrab" if mes.mes == "end" else "firebrick",
+ )
+ pw.getCallingFunction()("statustext")(
+ pIds[mes.pid],
+ text="File: %s\nStatus: %s"
+ % (
+ pIds[mes.pid],
+ "finished" if mes.mes == "end" else "failed",
+ ),
+ )
+
+ if freeS == -1:
+ logging.error("Something went wrong..")
+ logging.error('Progress bars do not work correctly, but files will be processed and "finished.." will be printed..')
+ else:
+ assignedThreads[mes.pid] = -1
+ freeSlots.append(freeS)
elapsed = (time.time() - start) / 60.0
hours = ""
@@ -3379,12 +4081,34 @@ def runProcess(self, dontSave=False, askStarting=True):
p.terminate()
p.join()
+ _step_elapsed["individual_files"] = (time.time() - _ind_step_start) / 60.0
+ if not self.terminateJobs:
+ _step_status["individual_files"] = _ST_OK
+ _step_details["individual_files"] = f"{len(files)} file(s): {len(files) - len(failedFiles)} finished, {len(failedFiles)} failed"
+
if self.terminateJobs:
return
resFileFull = str(self.ui.groupsSave.text())
resFilePath, resFileName = os.path.split(os.path.abspath(resFileFull))
excel_file = resFileFull.replace(".xlsx", ".tsv").replace(".tsv", ".txt").replace(".txt", "") + ".xlsx"
+
+ # Determine the best available input sheet for annotation when only the annotation step is run
+ # (will be overridden later if bracketing/grouping/re-integration runs as part of this execution)
+ annotation_input_sheet = "2_StatColumns"
+ if os.path.isfile(excel_file):
+ try:
+ import openpyxl as _opxl_detect
+
+ _wb_detect = _opxl_detect.load_workbook(excel_file, read_only=True)
+ for _candidate in ("4_Convoluted", "3_Reintegrated", "2_StatColumns"):
+ if _candidate in _wb_detect.sheetnames:
+ annotation_input_sheet = _candidate
+ break
+ _wb_detect.close()
+ except Exception:
+ pass
+
# bracket/group from individual LC-HRMS data / re-integrate missed peaks
if self.ui.processMultipleFiles.checkState() == QtCore.Qt.Checked:
pw = ProgressWrapper(1, parent=self)
@@ -3397,6 +4121,7 @@ def runProcess(self, dontSave=False, askStarting=True):
logging.info("\n\n##############################################################")
logging.info("Bracketing of individual LC-HRMS results..")
+ _bracketing_step_start = time.time()
try:
if True:
# Group results
@@ -3571,10 +4296,27 @@ def runProcess(self, dontSave=False, askStarting=True):
grpOmit(excel_file, grpStats, sheet_name="2_StatColumns", new_sheet_name="2_StatColumns")
logging.info("Statistic columns added (and feature pairs omitted)..")
+ _step_elapsed["bracketing"] = (time.time() - _bracketing_step_start) / 60.0
+ _step_status["bracketing"] = _ST_OK
+ try:
+ import openpyxl as _opxl_br
+
+ _wb_br = _opxl_br.load_workbook(excel_file, read_only=True)
+ _n_feat_br = max(0, _wb_br["2_StatColumns"].max_row - 1) if "2_StatColumns" in _wb_br.sheetnames else 0
+ _wb_br.close()
+ _step_details["bracketing"] = f"{_n_feat_br} feature(s) detected"
+ except Exception:
+ _step_details["bracketing"] = "features detected"
+
except Exception as ex:
traceback.print_exc()
logging.error(str(traceback))
+ _step_elapsed["bracketing"] = (time.time() - _bracketing_step_start) / 60.0
+ _step_status["bracketing"] = _ST_ERROR
+ _step_details["bracketing"] = str(ex)
+ _bracketing_failed = True
+
QtWidgets.QMessageBox.warning(
self,
"MetExtract",
@@ -3591,387 +4333,485 @@ def runProcess(self, dontSave=False, askStarting=True):
if self.terminateJobs:
return
- # Calculate metabolite groups
- if self.ui.convoluteResults.isChecked():
- logging.info("\n\n##############################################################")
- try:
- pw = ProgressWrapper(1, parent=self)
- pw.show()
- pw.getCallingFunction()("text")("Convoluting feature pairs")
-
- findIsoPairsInstance = FindIsoPairs(
- files[0],
- exOperator=str(self.ui.exOperator_LineEdit.text()),
- exExperimentID=str(self.ui.exExperimentID_LineEdit.text()),
- exComments=str(self.ui.exComments_TextEdit.toPlainText()),
- exExperimentName=str(self.ui.exExperimentName_LineEdit.text()),
- metabolisationExperiment=self.labellingExperiment == TRACER,
- labellingisotopeA=str(self.ui.isotopeAText.text()),
- labellingisotopeB=str(self.ui.isotopeBText.text()),
- xOffset=self.isotopeBmass - self.isotopeAmass,
- useRatio=self.ui.useRatio.isChecked(),
- minRatio=self.ui.minRatio.value(),
- maxRatio=self.ui.maxRatio.value(),
- useCIsotopePatternValidation=int(self.ui.useCValidation.checkState().value),
- configuredTracer=self.configuredTracer,
- intensityThreshold=self.ui.intensityThreshold.value(),
- intensityCutoff=self.ui.intensityCutoff.value(),
- startTime=self.ui.scanStartTime.value(),
- stopTime=self.ui.scanEndTime.value(),
- maxLoading=self.ui.maxLoading.value(),
- xCounts=str(self.ui.xCountSearch.text()),
- isotopicPatternCountLeft=self.ui.isotopePatternCountA.value(),
- isotopicPatternCountRight=self.ui.isotopePatternCountB.value(),
- lowAbundanceIsotopeCutoff=self.ui.isoAbundance.checkState() == QtCore.Qt.Checked,
- purityN=self.ui.isotopicAbundanceA.value() / 100.0,
- purityL=self.ui.isotopicAbundanceB.value() / 100.0,
- intensityErrorN=self.ui.baseRange.value() / 100.0,
- intensityErrorL=self.ui.isotopeRange.value() / 100.0,
- scanIndexOffset=self.ui.scanIndexOffset.value(),
- minSpectraCount=self.ui.minSpectraCount.value(),
- clustPPM=self.ui.clustPPM.value(),
- chromPeakPPM=self.ui.wavelet_EICppm.value(),
- eicSmoothingWindow=str(self.ui.eicSmoothingWindow.currentText()),
- eicSmoothingWindowSize=self.ui.eicSmoothingWindowSize.value(),
- eicSmoothingPolynom=self.ui.smoothingPolynom_spinner.value(),
- artificialMPshift_start=self.ui.spinBox_artificialMPshift_start.value(),
- artificialMPshift_stop=self.ui.spinBox_artificialMPshift_stop.value(),
- calcIsoRatioNative=self.ui.calcIsoRatioNative_spinBox.value(),
- calcIsoRatioLabelled=self.ui.calcIsoRatioLabelled_spinBox.value(),
- calcIsoRatioMoiety=self.ui.calcIsoRatioMoiety_spinBox.value(),
- minCorrelationConnections=self.ui.minCorrelationConnections.value() / 100.0,
- positiveScanEvent=str(self.ui.positiveScanEvent.currentText()),
- negativeScanEvent=str(self.ui.negativeScanEvent.currentText()),
- correctCCount=self.ui.correctcCount.checkState() == QtCore.Qt.Checked,
- minCorrelation=self.ui.minCorrelation.value() / 100.0,
- hAIntensityError=self.ui.hAIntensityError.value() / 100.0,
- hAMinScans=self.ui.hAMinScans.value(),
- adducts=self.adducts,
- elements=self.elementsForNL,
- heteroAtoms=self.heteroElements,
- simplifyInSourceFragments=self.ui.checkBox_simplifyInSourceFragments.isChecked(),
- lock=None,
- queue=None,
- pID=1,
- meVersion="MetExtract (%s)" % MetExtractVersion,
- peak_picker=picker,
- peak_filter_config=filter_config,
- )
+ convolution_input_sheet = "2_StatColumns"
+ # tracked separately because grouping may run after re-integration and update only annotation input afterwards
+ annotation_input_sheet = "2_StatColumns"
- procProc = FuncProcess(
- _target=calculateMetaboliteGroups,
- file=excel_file,
- toFile=excel_file,
- sheet_name="2_StatColumns",
- new_sheet_name="3_Convoluted",
- groups=definedGroups,
- eicPPM=self.ui.wavelet_EICppm.value(),
- maxAnnotationTimeWindow=self.ui.maxAnnotationTimeWindow.value(),
- minConnectionsInFiles=self.ui.metaboliteClusterMinConnections.value(),
- minConnectionRate=self.ui.minConnectionRate.value() / 100.0,
- minPeakCorrelation=self.ui.minCorrelation.value() / 100.0,
- useRatio=self.ui.useSILRatioForConvolution.checkState() == QtCore.Qt.Checked,
- cpus=min(len(files), cpus),
- )
+ # re-integrate missed peaks (run before grouping, if enabled)
+ if self.ui.integratedMissedPeaks.isChecked():
+ if _bracketing_failed:
+ _step_status["reintegration"] = _ST_SKIPPED_PREV
+ logging.info("Skipping re-integration: bracketing step failed.")
+ else:
+ logging.info("\n\n##############################################################")
+ logging.info("Re-integrating of individual LC-HRMS results..")
- # Create a shared Queue and Lock and attach to the FindIsoPairs instance
- q = procProc.getQueue()
- lock = Lock()
- findIsoPairsInstance.queue = q
- findIsoPairsInstance.lock = lock
-
- procProc.addKwd("pwMaxSet", q)
- procProc.addKwd("pwValSet", q)
- procProc.addKwd("pwTextSet", q)
- procProc.addKwd("runIdentificationInstance", findIsoPairsInstance)
- procProc.start()
-
- pw.setCloseCallback(
- closeCallBack=CallBackMethod(
- _target=interruptConvolutingOfFeaturePairs,
- selfObj=self,
- funcProc=procProc,
- ).getRunMethod()
- )
+ _reintegration_step_start = time.time()
+ pw = ProgressWrapper(min(len(files), cpus) + 1, showLog=False, parent=self)
+ pw.show()
+ pw.getCallingFunction()("text")("Integrating..")
+ pw.getCallingFunction()("header")("Integrating..")
- # check for status updates
- while procProc.isAlive():
- QtWidgets.QApplication.processEvents()
- while not (procProc.getQueue().empty()):
- mes = procProc.getQueue().get(block=False, timeout=1)
-
- # No idea why / where there are sometimes other objects than Bunch(mes, val), but they occur
- if isinstance(mes, Bunch) and hasattr(mes, "mes") and (hasattr(mes, "val") or hasattr(mes, "text")):
- pw.getCallingFunction()(mes.mes)(mes.val)
- elif mes == (None, None):
- ## I have no idea where this object comes from
- pass
- else:
- logging.critical("UNKNONW OBJECT IN PROCESSING QUEUE: %s" % str(mes))
+ try:
+ # Reintegrate missed peaks in files
+ fDict = {}
+ for group in definedGroups:
+ for grp in natSort(group.files):
+ f = grp
+ f = f.replace("\\", "/")
+ fDict[f] = f[(f.rfind("/") + 1) : max(f.lower().rfind(".mzxml"), f.lower().rfind(".mzml"))]
+
+ reIntegrateResultsFile(
+ excel_file,
+ "2_StatColumns",
+ "3_Reintegrated",
+ fDict,
+ addPeakArea=bool(self.ui.checkBox_expPeakArea.checkState() == QtCore.Qt.Checked),
+ addPeakAbundance=bool(self.ui.checkBox_expPeakApexIntensity.checkState() == QtCore.Qt.Checked),
+ addPeakSNR=bool(self.ui.checkBox_expPeakSNR.checkState() == QtCore.Qt.Checked),
+ ppm=self.ui.groupPpm.value(),
+ maxRTShift=self.ui.integrationMaxTimeDifference.value(),
+ scales=[
+ self.ui.wavelet_minScale.value(),
+ self.ui.wavelet_maxScale.value(),
+ ],
+ reintegrateIntensityCutoff=self.ui.reintegrateIntensityCutoff.value(),
+ snrTH=self.ui.wavelet_SNRThreshold.value(),
+ smoothingWindow=str(self.ui.eicSmoothingWindow.currentText()),
+ smoothingWindowSize=self.ui.eicSmoothingWindowSize.value(),
+ smoothingWindowPolynom=self.ui.smoothingPolynom_spinner.value(),
+ positiveScanEvent=str(self.ui.positiveScanEvent.currentText()),
+ negativeScanEvent=str(self.ui.negativeScanEvent.currentText()),
+ pw=pw,
+ selfObj=self,
+ cpus=min(len(files), cpus),
+ start=start,
+ peak_filter_config=filter_config,
+ peak_picker=picker,
+ )
+ convolution_input_sheet = "3_Reintegrated"
+ annotation_input_sheet = "3_Reintegrated"
- time.sleep(0.5)
+ _step_elapsed["reintegration"] = (time.time() - _reintegration_step_start) / 60.0
+ _step_status["reintegration"] = _ST_OK
+ try:
+ import openpyxl as _opxl_ri
- # Log time used for bracketing
- elapsed = (time.time() - start) / 60.0
- hours = ""
- if elapsed >= 60.0:
- hours = "%d hours " % (elapsed // 60)
- mins = "%.2f mins" % (elapsed % 60.0)
+ _wb_ri = _opxl_ri.load_workbook(excel_file, read_only=True)
+ _n_feat_ri = max(0, _wb_ri["3_Reintegrated"].max_row - 1) if "3_Reintegrated" in _wb_ri.sheetnames else 0
+ _wb_ri.close()
+ _step_details["reintegration"] = f"{_n_feat_ri} feature(s) re-integrated"
+ except Exception:
+ _step_details["reintegration"] = "re-integration complete"
- if self.terminateJobs:
- return
- else:
- logging.info("Convoluting feature pairs finished (%s%s).." % (hours, mins))
+ elapsed = (time.time() - start) / 60.0
+ hours = ""
+ if elapsed >= 60.0:
+ hours = "%d hours " % (elapsed // 60)
+ mins = "%.2f mins" % (elapsed % 60.0)
+ logging.info("Re-integrating finished (%s%s).." % (hours, mins))
- except Exception as ex:
- traceback.print_exc()
- logging.error(str(traceback))
+ except Exception as e:
+ traceback.print_exc()
+ logging.error(str(traceback))
- QtWidgets.QMessageBox.warning(
- self,
- "MetExtract",
- "Error during convolution of feature pairs: '%s'" % str(ex),
- QtWidgets.QMessageBox.Ok,
- )
- errorCount += 1
- finally:
- pw.setSkipCallBack(True)
- logging.info("##############################################################")
+ _step_elapsed["reintegration"] = (time.time() - _reintegration_step_start) / 60.0
+ _step_status["reintegration"] = _ST_ERROR
+ _step_details["reintegration"] = str(e)
+ _reintegration_failed = True
- pw.setSkipCallBack(True)
- pw.hide()
+ QtWidgets.QMessageBox.warning(
+ self,
+ "MetExtract",
+ "Error during reintegrating files: '%s'" % str(e),
+ QtWidgets.QMessageBox.Ok,
+ )
+ errorCount += 1
+ finally:
+ pw.setSkipCallBack(True)
+ pw.hide()
+ logging.info("##############################################################")
if self.terminateJobs:
+ pw.hide()
return
- # re-integrate missed peaks
- if self.ui.integratedMissedPeaks.isChecked():
- logging.info("\n\n##############################################################")
- logging.info("Re-integrating of individual LC-HRMS results..")
+ # Calculate metabolite groups
+ if self.ui.convoluteResults.isChecked():
+ if _bracketing_failed or _reintegration_failed:
+ _step_status["convolution"] = _ST_SKIPPED_PREV
+ logging.info("Skipping convolution: a preceding step failed.")
+ else:
+ logging.info("\n\n##############################################################")
+ _convolution_step_start = time.time()
+ try:
+ pw = ProgressWrapper(1, parent=self)
+ pw.show()
+ pw.getCallingFunction()("text")("Convoluting feature pairs")
- pw = ProgressWrapper(min(len(files), cpus) + 1, showLog=False, parent=self)
- pw.show()
- pw.getCallingFunction()("text")("Integrating..")
- pw.getCallingFunction()("header")("Integrating..")
+ findIsoPairsInstance = FindIsoPairs(
+ files[0],
+ exOperator=str(self.ui.exOperator_LineEdit.text()),
+ exExperimentID=str(self.ui.exExperimentID_LineEdit.text()),
+ exComments=str(self.ui.exComments_TextEdit.toPlainText()),
+ exExperimentName=str(self.ui.exExperimentName_LineEdit.text()),
+ metabolisationExperiment=self.labellingExperiment == TRACER,
+ labellingisotopeA=str(self.ui.isotopeAText.text()),
+ labellingisotopeB=str(self.ui.isotopeBText.text()),
+ xOffset=self.isotopeBmass - self.isotopeAmass,
+ useRatio=self.ui.useRatio.isChecked(),
+ minRatio=self.ui.minRatio.value(),
+ maxRatio=self.ui.maxRatio.value(),
+ useCIsotopePatternValidation=int(self.ui.useCValidation.checkState().value),
+ configuredTracer=self.configuredTracer,
+ intensityThreshold=self.ui.intensityThreshold.value(),
+ intensityCutoff=self.ui.intensityCutoff.value(),
+ startTime=self.ui.scanStartTime.value(),
+ stopTime=self.ui.scanEndTime.value(),
+ maxLoading=self.ui.maxLoading.value(),
+ xCounts=str(self.ui.xCountSearch.text()),
+ isotopicPatternCountLeft=self.ui.isotopePatternCountA.value(),
+ isotopicPatternCountRight=self.ui.isotopePatternCountB.value(),
+ lowAbundanceIsotopeCutoff=self.ui.isoAbundance.checkState() == QtCore.Qt.Checked,
+ purityN=self.ui.isotopicAbundanceA.value() / 100.0,
+ purityL=self.ui.isotopicAbundanceB.value() / 100.0,
+ intensityErrorN=self.ui.baseRange.value() / 100.0,
+ intensityErrorL=self.ui.isotopeRange.value() / 100.0,
+ scanIndexOffset=self.ui.scanIndexOffset.value(),
+ minSpectraCount=self.ui.minSpectraCount.value(),
+ clustPPM=self.ui.clustPPM.value(),
+ chromPeakPPM=self.ui.wavelet_EICppm.value(),
+ eicSmoothingWindow=str(self.ui.eicSmoothingWindow.currentText()),
+ eicSmoothingWindowSize=self.ui.eicSmoothingWindowSize.value(),
+ eicSmoothingPolynom=self.ui.smoothingPolynom_spinner.value(),
+ artificialMPshift_start=self.ui.spinBox_artificialMPshift_start.value(),
+ artificialMPshift_stop=self.ui.spinBox_artificialMPshift_stop.value(),
+ calcIsoRatioNative=self.ui.calcIsoRatioNative_spinBox.value(),
+ calcIsoRatioLabelled=self.ui.calcIsoRatioLabelled_spinBox.value(),
+ calcIsoRatioMoiety=self.ui.calcIsoRatioMoiety_spinBox.value(),
+ minCorrelationConnections=self.ui.minCorrelationConnections.value() / 100.0,
+ positiveScanEvent=str(self.ui.positiveScanEvent.currentText()),
+ negativeScanEvent=str(self.ui.negativeScanEvent.currentText()),
+ correctCCount=self.ui.correctcCount.checkState() == QtCore.Qt.Checked,
+ minCorrelation=self.ui.minCorrelation.value() / 100.0,
+ hAIntensityError=self.ui.hAIntensityError.value() / 100.0,
+ hAMinScans=self.ui.hAMinScans.value(),
+ adducts=self.adducts,
+ elements=self.elementsForNL,
+ heteroAtoms=self.heteroElements,
+ simplifyInSourceFragments=self.ui.checkBox_simplifyInSourceFragments.isChecked(),
+ lock=None,
+ queue=None,
+ pID=1,
+ meVersion="MetExtract (%s)" % MetExtractVersion,
+ peak_picker=picker,
+ peak_filter_config=filter_config,
+ )
- try:
- # Reintegrate missed peaks in files
- fDict = {}
- for group in definedGroups:
- for grp in natSort(group.files):
- f = grp
- f = f.replace("\\", "/")
- fDict[f] = f[(f.rfind("/") + 1) : max(f.lower().rfind(".mzxml"), f.lower().rfind(".mzml"))]
-
- reIntegrateResultsFile(
- excel_file,
- "3_Convoluted",
- "4_Reintegrated",
- fDict,
- addPeakArea=bool(self.ui.checkBox_expPeakArea.checkState() == QtCore.Qt.Checked),
- addPeakAbundance=bool(self.ui.checkBox_expPeakApexIntensity.checkState() == QtCore.Qt.Checked),
- addPeakSNR=bool(self.ui.checkBox_expPeakSNR.checkState() == QtCore.Qt.Checked),
- ppm=self.ui.groupPpm.value(),
- maxRTShift=self.ui.integrationMaxTimeDifference.value(),
- scales=[
- self.ui.wavelet_minScale.value(),
- self.ui.wavelet_maxScale.value(),
- ],
- reintegrateIntensityCutoff=self.ui.reintegrateIntensityCutoff.value(),
- snrTH=self.ui.wavelet_SNRThreshold.value(),
- smoothingWindow=str(self.ui.eicSmoothingWindow.currentText()),
- smoothingWindowSize=self.ui.eicSmoothingWindowSize.value(),
- smoothingWindowPolynom=self.ui.smoothingPolynom_spinner.value(),
- positiveScanEvent=str(self.ui.positiveScanEvent.currentText()),
- negativeScanEvent=str(self.ui.negativeScanEvent.currentText()),
- pw=pw,
- selfObj=self,
- cpus=min(len(files), cpus),
- start=start,
- peak_filter_config=filter_config,
- peak_picker=picker,
- )
- # Log time used for bracketing
- elapsed = (time.time() - start) / 60.0
- hours = ""
- if elapsed >= 60.0:
- hours = "%d hours " % (elapsed // 60)
- mins = "%.2f mins" % (elapsed % 60.0)
- logging.info("Re-integrating finished (%s%s).." % (hours, mins))
-
- except Exception as e:
- traceback.print_exc()
- logging.error(str(traceback))
+ procProc = FuncProcess(
+ _target=calculateMetaboliteGroups,
+ file=excel_file,
+ toFile=excel_file,
+ sheet_name=convolution_input_sheet,
+ new_sheet_name="4_Convoluted",
+ groups=definedGroups,
+ eicPPM=self.ui.wavelet_EICppm.value(),
+ maxAnnotationTimeWindow=self.ui.maxAnnotationTimeWindow.value(),
+ minConnectionsInFiles=self.ui.metaboliteClusterMinConnections.value(),
+ minConnectionRate=self.ui.minConnectionRate.value() / 100.0,
+ minPeakCorrelation=self.ui.minCorrelation.value() / 100.0,
+ useAbundanceSimilarity=self.ui.useAbundanceSimilarityForConvolution.checkState() == QtCore.Qt.Checked,
+ abundanceSimilarityThreshold=self.ui.abundanceSimilarityThreshold.value() / 100.0,
+ useRatio=self.ui.useSILRatioForConvolution.checkState() == QtCore.Qt.Checked,
+ cpus=min(len(files), cpus),
+ )
- QtWidgets.QMessageBox.warning(
- self,
- "MetExtract",
- "Error during reintegrating files: '%s'" % str(e),
- QtWidgets.QMessageBox.Ok,
- )
- errorCount += 1
- finally:
- pw.setSkipCallBack(True)
- pw.hide()
- logging.info("##############################################################")
+ # Create a shared Queue and Lock and attach to the FindIsoPairs instance
+ q = procProc.getQueue()
+ lock = Lock()
+ findIsoPairsInstance.queue = q
+ findIsoPairsInstance.lock = lock
+
+ procProc.addKwd("pwMaxSet", q)
+ procProc.addKwd("pwValSet", q)
+ procProc.addKwd("pwTextSet", q)
+ procProc.addKwd("runIdentificationInstance", findIsoPairsInstance)
+ procProc.start()
+
+ pw.setCloseCallback(
+ closeCallBack=CallBackMethod(
+ _target=interruptConvolutingOfFeaturePairs,
+ selfObj=self,
+ funcProc=procProc,
+ ).getRunMethod()
+ )
+
+ # check for status updates
+ while procProc.isAlive():
+ QtWidgets.QApplication.processEvents()
+ while not (procProc.getQueue().empty()):
+ mes = procProc.getQueue().get(block=False, timeout=1)
+
+ # No idea why / where there are sometimes other objects than Bunch(mes, val), but they occur
+ if isinstance(mes, Bunch) and hasattr(mes, "mes") and (hasattr(mes, "val") or hasattr(mes, "text")):
+ pw.getCallingFunction()(mes.mes)(mes.val)
+ elif mes == (None, None):
+ ## I have no idea where this object comes from
+ pass
+ else:
+ logging.critical("UNKNONW OBJECT IN PROCESSING QUEUE: %s" % str(mes))
+
+ time.sleep(0.5)
+
+ elapsed = (time.time() - start) / 60.0
+ hours = ""
+ if elapsed >= 60.0:
+ hours = "%d hours " % (elapsed // 60)
+ mins = "%.2f mins" % (elapsed % 60.0)
+
+ if self.terminateJobs:
+ return
+ else:
+ logging.info("Convoluting feature pairs finished (%s%s).." % (hours, mins))
+ annotation_input_sheet = "4_Convoluted"
+
+ _step_elapsed["convolution"] = (time.time() - _convolution_step_start) / 60.0
+ _step_status["convolution"] = _ST_OK
+ try:
+ import openpyxl as _opxl_cv
+
+ _wb_cv = _opxl_cv.load_workbook(excel_file, read_only=True)
+ if "4_Convoluted" in _wb_cv.sheetnames:
+ _ws_cv = _wb_cv["4_Convoluted"]
+ _headers_cv = [c.value for c in next(_ws_cv.iter_rows(max_row=1))]
+ if "OGroup" in _headers_cv:
+ _og_col = _headers_cv.index("OGroup")
+ _ogroups = {row[_og_col] for row in _ws_cv.iter_rows(min_row=2, values_only=True) if row[_og_col] is not None}
+ _n_groups = len(_ogroups)
+ _n_feats_cv = max(0, _ws_cv.max_row - 1)
+ else:
+ _n_groups = 0
+ _n_feats_cv = max(0, _ws_cv.max_row - 1)
+ else:
+ _n_groups = 0
+ _n_feats_cv = 0
+ _wb_cv.close()
+ _step_details["convolution"] = f"{_n_feats_cv} feature(s) in {_n_groups} group(s)"
+ except Exception:
+ _step_details["convolution"] = "grouping complete"
+
+ except Exception as ex:
+ traceback.print_exc()
+ logging.error(str(traceback))
+
+ _step_elapsed["convolution"] = (time.time() - _convolution_step_start) / 60.0
+ _step_status["convolution"] = _ST_ERROR
+ _step_details["convolution"] = str(ex)
+ _convolution_failed = True
+
+ QtWidgets.QMessageBox.warning(
+ self,
+ "MetExtract",
+ "Error during convolution of feature pairs: '%s'" % str(ex),
+ QtWidgets.QMessageBox.Ok,
+ )
+ errorCount += 1
+ finally:
+ pw.setSkipCallBack(True)
+ logging.info("##############################################################")
+
+ pw.setSkipCallBack(True)
+ pw.hide()
if self.terminateJobs:
- pw.hide()
return
annotationColumns = []
## annotate results
if self.ui.annotateMetabolites_CheckBox.isChecked():
- logging.info("\n\n##############################################################")
- logging.info("Annotation of detected metabolites..")
+ if _bracketing_failed or _convolution_failed:
+ _step_status["annotation"] = _ST_SKIPPED_PREV
+ logging.info("Skipping annotation: a preceding step failed.")
+ else:
+ logging.info("\n\n##############################################################")
+ logging.info("Annotation of detected metabolites..")
- useAdducts = []
- for adduct in self.adducts:
- useAdducts.append(
- [
- str(adduct.name),
- adduct.mzoffset,
- str(adduct.polarity),
- adduct.charge,
- adduct.mCount,
- ]
- )
+ _annotation_step_start = time.time()
+ _annotation_error = False
- pw = ProgressWrapper(1, parent=self)
- pw.show()
- pw.getCallingFunction()("text")("Annotation of detected metabolites")
- pw.getCallingFunction()("header")("Annotating..")
+ useAdducts = []
+ for adduct in self.adducts:
+ useAdducts.append(
+ [
+ str(adduct.name),
+ adduct.mzoffset,
+ str(adduct.polarity),
+ adduct.charge,
+ adduct.mCount,
+ ]
+ )
- if self.ui.searchDB_checkBox.isChecked():
- pw.getCallingFunction()("text")("Searching hits in databases..")
+ pw = ProgressWrapper(1, parent=self)
+ pw.show()
+ pw.getCallingFunction()("text")("Annotation of detected metabolites")
+ pw.getCallingFunction()("header")("Annotating..")
- # Collect database files
- dbFiles = []
- for entryInd in range(self.ui.dbList_listView.model().rowCount()):
- dbFile = str(self.ui.dbList_listView.model().item(entryInd, 0).data())
- dbFiles.append(dbFile)
+ if self.ui.searchDB_checkBox.isChecked():
+ pw.getCallingFunction()("text")("Searching hits in databases..")
- # Prepare parameters
- useExactXn = str(self.ui.sumFormulasUseExactXn_ComboBox.currentText())
- if useExactXn.lower() == "plusminus":
- useExactXn = "PlusMinus_%d" % (self.ui.sumFormulasPlusMinus_spinBox.value())
+ # Collect database files
+ dbFiles = []
+ for entryInd in range(self.ui.dbList_listView.model().rowCount()):
+ dbFile = str(self.ui.dbList_listView.model().item(entryInd, 0).data())
+ dbFiles.append(dbFile)
- try:
- addedColumns = annotateResultMatrix.annotateWithDatabases(
- file=excel_file,
- sheet_name="4_Reintegrated",
- new_sheet_name="5_Annotated",
- dbFiles=dbFiles,
- useAdducts=useAdducts,
- ppm=self.ui.annotationMaxPPM_doubleSpinBox.value(),
- correctppmPosMode=self.ui.annotation_correctMassByPPMposMode.value(),
- correctppmNegMode=self.ui.annotation_correctMassByPPMnegMode.value(),
- rtError=self.ui.maxRTErrorInHits_spinnerBox.value(),
- useRt=self.ui.checkRTInHits_checkBox.isChecked(),
- checkXnInHits=useExactXn,
- processedElement=getElementOfIsotope(str(self.ui.isotopeAText.text())),
- pwMaxSet=pw.getCallingFunction()("max"),
- pwValSet=pw.getCallingFunction()("value"),
- )
- annotationColumns.extend(addedColumns)
+ # Prepare parameters
+ useExactXn = str(self.ui.sumFormulasUseExactXn_ComboBox.currentText())
+ if useExactXn.lower() == "plusminus":
+ useExactXn = "PlusMinus_%d" % (self.ui.sumFormulasPlusMinus_spinBox.value())
- if False:
- logging.info(
- "## Database search: checkXn %s, ppm: %.5f, correctppm: pos.mode: %.5f / neg.mode: %.5f, Adducts: %s"
- % (
- useExactXn,
- self.ui.annotationMaxPPM_doubleSpinBox.value(),
- self.ui.annotation_correctMassByPPMposMode.value(),
- self.ui.annotation_correctMassByPPMnegMode.value(),
- str(useAdducts),
- )
+ try:
+ db_info_messages = []
+ addedColumns = annotateResultMatrix.annotateWithDatabases(
+ file=excel_file,
+ sheet_name=annotation_input_sheet,
+ new_sheet_name="5_Annotated",
+ dbFiles=dbFiles,
+ useAdducts=useAdducts,
+ ppm=self.ui.annotationMaxPPM_doubleSpinBox.value(),
+ correctppmPosMode=self.ui.annotation_correctMassByPPMposMode.value(),
+ correctppmNegMode=self.ui.annotation_correctMassByPPMnegMode.value(),
+ rtError=self.ui.maxRTErrorInHits_spinnerBox.value(),
+ useRt=self.ui.checkRTInHits_checkBox.isChecked(),
+ checkXnInHits=useExactXn,
+ processedElement=getElementOfIsotope(str(self.ui.isotopeAText.text())),
+ pwMaxSet=pw.getCallingFunction()("max"),
+ pwValSet=pw.getCallingFunction()("value"),
+ db_info_messages=db_info_messages,
)
+ annotationColumns.extend(addedColumns)
- except Exception as e:
- traceback.print_exc()
- logging.error(f"Error during database search: {e}")
- QtWidgets.QMessageBox.warning(
- self,
- "MetExtract",
- f"Error during database search annotation: {str(e)}",
- QtWidgets.QMessageBox.Ok,
- )
- errorCount += 1
- logging.info("##############################################################\n\n")
+ # Write DB_info log sheet
+ if db_info_messages:
+ from .utils import add_sheet_to_excel as _add_sheet_db_info
- if self.ui.generateSumFormulas_CheckBox.isChecked():
- pw.getCallingFunction()("text")("Generating sum formulas..")
- pw.getCallingFunction()("value")(0)
+ _add_sheet_db_info(excel_file, pl.DataFrame({"text": db_info_messages}), "DB_info", overwrite=True)
- try:
- fT = formulaTools()
- elemsMin = fT.parseFormula(str(self.ui.sumFormulasMinimumElements_lineEdit.text()))
- elemsMax = fT.parseFormula(str(self.ui.sumFormulasMaximumElements_lineEdit.text()))
-
- useAtoms = []
- if getElementOfIsotope(str(self.ui.isotopeAText.text())) in elemsMax.keys():
- useAtoms.append(getElementOfIsotope(str(self.ui.isotopeAText.text())))
-
- if "C" in elemsMax.keys() and "C" not in useAtoms:
- useAtoms.append("C")
- if "H" in elemsMax.keys() and "H" not in useAtoms:
- useAtoms.append("H")
- if "N" in elemsMax.keys() and "N" not in useAtoms:
- useAtoms.append("N")
- if "O" in elemsMax.keys() and "O" not in useAtoms:
- useAtoms.append("O")
- if "S" in elemsMax.keys() and "S" not in useAtoms:
- useAtoms.append("S")
-
- for atom in elemsMax.keys():
- if atom not in useAtoms:
- useAtoms.append(atom)
-
- atomsRange = []
- for atom in useAtoms:
- minE = 0
- if atom in elemsMin.keys():
- minE = elemsMin[atom]
- maxE = elemsMax[atom]
- atomsRange.append([minE, maxE])
+ if False:
+ logging.info(
+ "## Database search: checkXn %s, ppm: %.5f, correctppm: pos.mode: %.5f / neg.mode: %.5f, Adducts: %s"
+ % (
+ useExactXn,
+ self.ui.annotationMaxPPM_doubleSpinBox.value(),
+ self.ui.annotation_correctMassByPPMposMode.value(),
+ self.ui.annotation_correctMassByPPMnegMode.value(),
+ str(useAdducts),
+ )
+ )
- useExactXn = str(self.ui.sumFormulasUseExactXn_ComboBox.currentText())
- if useExactXn.lower() == "plusminus":
- useExactXn = "PlusMinus_%d" % (self.ui.sumFormulasPlusMinus_spinBox.value())
+ except Exception as e:
+ traceback.print_exc()
+ logging.error(f"Error during database search: {e}")
+ QtWidgets.QMessageBox.warning(
+ self,
+ "MetExtract",
+ f"Error during database search annotation: {str(e)}",
+ QtWidgets.QMessageBox.Ok,
+ )
+ _annotation_error = True
+ errorCount += 1
+ logging.info("##############################################################\n\n")
- addedColumns = annotateResultMatrix.annotateWithSumFormulas(
- file=excel_file,
- sheet_name="5_Annotated",
- useAtoms=useAtoms,
- atomsRange=atomsRange,
- processedElement=getElementOfIsotope(str(self.ui.isotopeAText.text())),
- useExactXn=useExactXn,
- ppm=self.ui.annotationMaxPPM_doubleSpinBox.value(),
- ppmCorrectionPosMode=self.ui.annotation_correctMassByPPMposMode.value(),
- ppmCorrectionNegMode=self.ui.annotation_correctMassByPPMnegMode.value(),
- useAdducts=useAdducts,
- pwMaxSet=pw.getCallingFunction()("max"),
- pwValSet=pw.getCallingFunction()("value"),
- nCores=min(len(files), cpus),
- )
- annotationColumns.extend(addedColumns)
+ if self.ui.generateSumFormulas_CheckBox.isChecked():
+ pw.getCallingFunction()("text")("Generating sum formulas..")
+ pw.getCallingFunction()("value")(0)
- except Exception as e:
- traceback.print_exc()
- logging.error(f"Error during sum formula generation: {e}")
- QtWidgets.QMessageBox.warning(
- self,
- "MetExtract",
- f"Error during sum formula generation: {str(e)}",
- QtWidgets.QMessageBox.Ok,
- )
- errorCount += 1
- logging.info("\n\n##############################################################")
+ try:
+ fT = formulaTools()
+ elemsMin = fT.parseFormula(str(self.ui.sumFormulasMinimumElements_lineEdit.text()))
+ elemsMax = fT.parseFormula(str(self.ui.sumFormulasMaximumElements_lineEdit.text()))
+
+ useAtoms = []
+ if getElementOfIsotope(str(self.ui.isotopeAText.text())) in elemsMax.keys():
+ useAtoms.append(getElementOfIsotope(str(self.ui.isotopeAText.text())))
+
+ if "C" in elemsMax.keys() and "C" not in useAtoms:
+ useAtoms.append("C")
+ if "H" in elemsMax.keys() and "H" not in useAtoms:
+ useAtoms.append("H")
+ if "N" in elemsMax.keys() and "N" not in useAtoms:
+ useAtoms.append("N")
+ if "O" in elemsMax.keys() and "O" not in useAtoms:
+ useAtoms.append("O")
+ if "S" in elemsMax.keys() and "S" not in useAtoms:
+ useAtoms.append("S")
+
+ for atom in elemsMax.keys():
+ if atom not in useAtoms:
+ useAtoms.append(atom)
+
+ atomsRange = []
+ for atom in useAtoms:
+ minE = 0
+ if atom in elemsMin.keys():
+ minE = elemsMin[atom]
+ maxE = elemsMax[atom]
+ atomsRange.append([minE, maxE])
+
+ useExactXn = str(self.ui.sumFormulasUseExactXn_ComboBox.currentText())
+ if useExactXn.lower() == "plusminus":
+ useExactXn = "PlusMinus_%d" % (self.ui.sumFormulasPlusMinus_spinBox.value())
+
+ addedColumns = annotateResultMatrix.annotateWithSumFormulas(
+ file=excel_file,
+ sheet_name="5_Annotated",
+ useAtoms=useAtoms,
+ atomsRange=atomsRange,
+ processedElement=getElementOfIsotope(str(self.ui.isotopeAText.text())),
+ useExactXn=useExactXn,
+ ppm=self.ui.annotationMaxPPM_doubleSpinBox.value(),
+ ppmCorrectionPosMode=self.ui.annotation_correctMassByPPMposMode.value(),
+ ppmCorrectionNegMode=self.ui.annotation_correctMassByPPMnegMode.value(),
+ useAdducts=useAdducts,
+ pwMaxSet=pw.getCallingFunction()("max"),
+ pwValSet=pw.getCallingFunction()("value"),
+ nCores=min(len(files), cpus),
+ )
+ annotationColumns.extend(addedColumns)
- print("\n\n")
- pw.hide()
+ except Exception as e:
+ traceback.print_exc()
+ logging.error(f"Error during sum formula generation: {e}")
+ QtWidgets.QMessageBox.warning(
+ self,
+ "MetExtract",
+ f"Error during sum formula generation: {str(e)}",
+ QtWidgets.QMessageBox.Ok,
+ )
+ _annotation_error = True
+ errorCount += 1
+ logging.info("\n\n##############################################################")
- logging.info("##############################################################")
+ print("\n\n")
+ pw.hide()
+
+ _step_elapsed["annotation"] = (time.time() - _annotation_step_start) / 60.0
+ if _annotation_error:
+ _step_status["annotation"] = _ST_ERROR
+ _step_details["annotation"] = "annotation encountered errors"
+ else:
+ _step_status["annotation"] = _ST_OK
+ try:
+ import openpyxl as _opxl_an
+
+ _wb_an = _opxl_an.load_workbook(excel_file, read_only=True)
+ _n_feat_an = max(0, _wb_an["5_Annotated"].max_row - 1) if "5_Annotated" in _wb_an.sheetnames else 0
+ _wb_an.close()
+ _step_details["annotation"] = f"{_n_feat_an} feature(s) annotated"
+ except Exception:
+ _step_details["annotation"] = "annotation complete"
+
+ logging.info("##############################################################")
## Process MSMS info
if self.ui.generateMSMSInfo_CheckBox.isChecked():
@@ -4037,8 +4877,9 @@ def runProcess(self, dontSave=False, askStarting=True):
self.updateLCMSSampleSettings()
- # Log time used for bracketing
- elapsed = (time.time() - overallStart) / 60.0
+ # Log overall time
+ _overall_elapsed = (time.time() - overallStart) / 60.0
+ elapsed = _overall_elapsed
hours = ""
if elapsed >= 60.0:
hours = "%d hours " % (elapsed // 60)
@@ -4054,240 +4895,218 @@ def runProcess(self, dontSave=False, askStarting=True):
notification_msg = "Processing finished with %d errors in %s%s" % (errorCount, hours, mins)
self._send_desktop_notification("MetExtract II", notification_msg)
- QtWidgets.QMessageBox.information(
- self,
- "MetExtract II",
- "Processing finished %sin %s%s" % ("(%d errors) " % errorCount if errorCount > 0 else "", hours, mins),
- QtWidgets.QMessageBox.Ok,
- )
- self.loadGroupsResultsFile(str(self.ui.groupsSave.text()))
-
- def showResultsSummary(self):
- texts = []
-
- definedGroups = self.getAllSampleGroups()
- for group in definedGroups:
- for i in range(len(group.files)):
- group.files[i] = str(group.files[i]).replace("\\", "/")
-
- # get all individual LC-HRMS files for processing
-
- maxFileNameLength = 10
- for group in definedGroups:
- for file in natSort(group.files):
- maxFileNameLength = max(maxFileNameLength, len(file))
-
- texts.append("Note: \n")
- texts.append("All calculated numbers shown here are based on the last data processing that was done for this dataset.\n")
- texts.append("Thus, these calculated values are influenced and distorted by used parameters. \n")
- texts.append("For example, an incorrect value for the parameter M:M' ratio will have a dramatic impact on the results and\n")
- texts.append("consequently also on these number. Please only consider these numbers for a quick overview of the generated last results. \n")
- texts.append("\n")
- texts.append("Caution:\n")
- texts.append("Do not use these numbers as the only means of further improving your data processing parameters. This could\n")
- texts.append("potentially lead to a deadlock and an incorrect data processing with many false-positive results and/or false-negatives. \n")
- texts.append("If you are unsure always try to contact an expert in data processing and/or ask a colleague of yours to\nhelp you with optimizing these values.\n\n\n\n")
- texts.append("Results of individual files\n-=-=-=-=-=-=-=-=-=-=-=-=-=-\n\n")
- texts.append(
- ("%%%ds ionMode %%12s %%12s %%12s %%12s %%20s %%40s %%25s\n" % (maxFileNameLength))
- % (
- "File",
- "MZs",
- "MZ Bins",
- "Features",
- "Metabolites",
- "mz delta ppm MZs",
- "avg M:M' MZs; Area; Abundance",
- "avg L-Enrichment",
- )
- )
- indGroups = {}
- for group in definedGroups:
- grName = str(group.name)
- indGroups[grName] = []
- texts.append(
- " Group "
- + grName
- + " [%d files%s%s%s%s, color %s]\n"
- % (
- len(group.files),
- ", Omit (minFound %d)" % group.minFound if group.omitFeatures else "",
- ", use for grouping" if group.useForMetaboliteGrouping else "",
- ", False positives remove",
- ", use as MSMS targets" if group.useAsMSMSTarget else "",
- group.color,
- )
- )
- texts.append("%s\n" % ("-" * (2 + 6 + maxFileNameLength + 12 * 4 + 6 + 20 * 3 + 25)))
- for file in natSort(group.files):
- showFileName = True
- for ionMode in ["+", "-"]:
- indGroups[grName].append(str(file))
-
- if os.path.exists(file + getDBSuffix()):
- try:
- file_db_con = PolarsDB(file + getDBSuffix(), format=getDBFormat())
-
- # Count MZs for this ionMode
- nMZs = len(file_db_con.tables["MZs"].filter(pl.col("ionMode") == ionMode))
- nMZsPPMDelta = file_db_con.tables["MZs"].filter(pl.col("ionMode") == ionMode).with_columns(((pl.col("lmz") - pl.col("mz") - pl.col("tmz")) * 1_000_000 / pl.col("mz")).alias("ppm_delta"))["ppm_delta"].mean()
-
- nMZsPPMDeltaStd = file_db_con.tables["MZs"].filter(pl.col("ionMode") == ionMode).with_columns(((pl.col("lmz") - pl.col("mz") - pl.col("tmz")) * 1_000_000 / pl.col("mz")).alias("ppm_delta"))["ppm_delta"].std()
-
- nMZBins = file_db_con.tables["MZBins"].filter(pl.col("ionMode") == ionMode).shape[0]
-
- nFeatures = file_db_con.tables["chromPeaks"].filter(pl.col("ionMode") == ionMode).shape[0]
-
- nMetabolites = file_db_con.tables["featureGroups"].shape[0]
-
- avgRatioSignals = file_db_con.tables["MZs"].filter(pl.col("ionMode") == ionMode).select((pl.col("intensity") / pl.col("intensityL")).alias("ratio"))["ratio"].mean()
-
- avgRatioSignalsStd = file_db_con.tables["MZs"].filter(pl.col("ionMode") == ionMode).select((pl.col("intensity") / pl.col("intensityL")).alias("ratio"))["ratio"].std()
-
- avgRatioFeaturesArea = file_db_con.tables["chromPeaks"].filter(pl.col("ionMode") == ionMode).select((pl.col("NPeakArea") / pl.col("LPeakArea")).alias("area_ratio"))["area_ratio"].mean()
+ # Build and show the processing summary dialog
+ def _fmt_elapsed(m):
+ if m <= 0:
+ return "—"
+ if m < 1:
+ return f"{m * 60:.1f} sec"
+ if m < 60:
+ return f"{m:.2f} min"
+ return f"{int(m // 60)}h {m % 60:.1f} min"
+
+ _step_names = {
+ "individual_files": "1. Individual file processing",
+ "bracketing": "2. Bracketing",
+ "reintegration": "3. Re-integration",
+ "convolution": "4. Feature grouping",
+ "annotation": "5. Annotation",
+ }
+ _status_style = {
+ _ST_OK: ("olivedrab", "OK"),
+ _ST_ERROR: ("firebrick", "Error"),
+ _ST_SKIPPED_USER: ("gray", "Skipped (not enabled)"),
+ _ST_SKIPPED_PREV: ("darkorange", "Skipped (previous step failed)"),
+ }
- avgRatioFeaturesAbundance = file_db_con.tables["chromPeaks"].filter(pl.col("ionMode") == ionMode).select((pl.col("NPeakAbundance") / pl.col("LPeakAbundance")).alias("abundance_ratio"))["abundance_ratio"].mean()
+ rows_html = ""
+ for _key in ("individual_files", "bracketing", "reintegration", "convolution", "annotation"):
+ _color, _label = _status_style.get(_step_status[_key], ("gray", _step_status[_key]))
+ _det = _step_details[_key] or "—"
+ _dur = _fmt_elapsed(_step_elapsed[_key])
+ rows_html += f"| {_step_names[_key]} | {_label} | {_det} | {_dur} |
"
+
+ _summary_html = f"""
+Processing Summary
+
+
+ | Step |
+ Status |
+ Details |
+ Duration |
+
+ {rows_html}
+
+
+ Total time: {_fmt_elapsed(_overall_elapsed)}
+
+Results can be viewed:
+
+ - In this application via the Experimental results tab
+ - In the Excel file at:
{excel_file}
+
+"""
+
+ _dlg = QtWidgets.QDialog(self)
+ _dlg.setWindowTitle("MetExtract II – Processing Summary")
+ _dlg.setMinimumSize(800, 420)
+ _dlg_layout = QtWidgets.QVBoxLayout(_dlg)
+ _browser = QtWidgets.QTextBrowser(_dlg)
+ _browser.setHtml(_summary_html)
+ _browser.setOpenExternalLinks(False)
+ _dlg_layout.addWidget(_browser)
+ _btn_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok, parent=_dlg)
+ _btn_box.accepted.connect(_dlg.accept)
+ _dlg_layout.addWidget(_btn_box)
+ _dlg.exec()
- avgEnrichmentL = file_db_con.tables["chromPeaks"].filter((pl.col("peaksRatioMPm1") > 0) & (pl.col("ionMode") == ionMode)).select((pl.col("xcount") / (pl.col("xcount") + pl.col("peaksRatioMPm1"))).alias("enrichment"))["enrichment"].mean()
+ self.loadGroupsResultsFile(str(self.ui.groupsSave.text()))
- avgEnrichmentLStd = file_db_con.tables["chromPeaks"].filter((pl.col("peaksRatioMPm1") > 0) & (pl.col("ionMode") == ionMode)).select((pl.col("xcount") / (pl.col("xcount") + pl.col("peaksRatioMPm1"))).alias("enrichment"))["enrichment"].std()
+ def showResultsSummary(self):
+ from .mePyGuis.ResultsSummaryDialog import ResultsSummaryDialog
- texts.append(
- ("%%%ds %%s %%12s %%12s %%12s %%12s %%20s %%40s %%25s\n" % (maxFileNameLength))
- % (
- file if showFileName else "",
- ionMode,
- nMZs if nMZs > 0 else "",
- nMZBins if nMZBins > 0 else "",
- nFeatures if nFeatures > 0 else "",
- nMetabolites if nMetabolites > 0 else "",
- "%s (+/- %s)"
- % (
- "%.2f" % nMZsPPMDelta if nMZsPPMDelta is not None else "",
- "%.2f" % nMZsPPMDeltaStd if nMZsPPMDeltaStd is not None else "",
- )
- if nMZsPPMDelta is not None
- else "",
- "%s; %s; %s"
- % (
- "%6.2f (+/- %s)"
- % (
- avgRatioSignals,
- "%.2f" % avgRatioSignalsStd if avgRatioSignalsStd is not None else "",
- )
- if avgRatioSignals is not None
- else "-",
- "%6.2f" % avgRatioFeaturesArea if avgRatioFeaturesArea is not None else "-",
- "%6.2f" % avgRatioFeaturesAbundance if avgRatioFeaturesAbundance is not None else "-",
- )
- if avgRatioSignalsStd is not None or avgRatioFeaturesArea is not None or avgRatioFeaturesAbundance is not None
- else "",
- "%.2f%% (+/- %s%%)"
- % (
- 100 * avgEnrichmentL,
- "%.2f" % (100 * avgEnrichmentLStd) if avgEnrichmentLStd is not None else "",
- )
- if avgEnrichmentL is not None
- else "",
- )
- )
- showFileName = False
- except Exception as ex:
- texts.append(("%%%ds %%s Error reading file: %%s\n" % (maxFileNameLength)) % (file if showFileName else "", ionMode, str(ex)))
- showFileName = False
- else:
- texts.append(("%%%ds File not processed\n" % (maxFileNameLength)) % file)
+ definedGroups = self.getAllSampleGroups()
+ for group in definedGroups:
+ for i in range(len(group.files)):
+ group.files[i] = str(group.files[i]).replace("\\", "/")
- texts.append("\n\n\n")
+ # ── Collect per-file rows ──────────────────────────────────────
+ file_rows = []
+ for group in definedGroups:
+ grName = str(group.name)
+ grColor = group.color if group.color else ""
+ for file in natSort(group.files):
+ if not os.path.exists(file + getDBSuffix()):
+ for ionMode in ["+", "-"]:
+ file_rows.append(
+ {
+ "group_name": grName,
+ "group_color": grColor,
+ "file": file,
+ "ion_mode": ionMode,
+ "error": "File not processed",
+ }
+ )
+ continue
+ for ionMode in ["+", "-"]:
+ try:
+ file_db_con = PolarsDB(file + getDBSuffix(), format=getDBFormat())
+
+ nMZs = len(file_db_con.tables["MZs"].filter(pl.col("ionMode") == ionMode))
+ ppm_df = file_db_con.tables["MZs"].filter(pl.col("ionMode") == ionMode).with_columns(((pl.col("lmz") - pl.col("mz") - pl.col("tmz")) * 1_000_000 / pl.col("mz")).alias("ppm_delta"))
+ nMZsPPMDelta = ppm_df["ppm_delta"].mean()
+ nMZsPPMDeltaStd = ppm_df["ppm_delta"].std()
+
+ nMZBins = file_db_con.tables["MZBins"].filter(pl.col("ionMode") == ionMode).shape[0]
+ nFeatures = file_db_con.tables["chromPeaks"].filter(pl.col("ionMode") == ionMode).shape[0]
+ nMetabolites = file_db_con.tables["featureGroups"].shape[0]
+
+ ratio_df = file_db_con.tables["MZs"].filter(pl.col("ionMode") == ionMode).select((pl.col("intensity") / pl.col("intensityL")).alias("ratio"))
+ avgRatioSignals = ratio_df["ratio"].mean()
+ avgRatioSignalsStd = ratio_df["ratio"].std()
+
+ avgRatioFeaturesArea = file_db_con.tables["chromPeaks"].filter(pl.col("ionMode") == ionMode).select((pl.col("NPeakArea") / pl.col("LPeakArea")).alias("area_ratio"))["area_ratio"].mean()
+
+ avgRatioFeaturesAbundance = file_db_con.tables["chromPeaks"].filter(pl.col("ionMode") == ionMode).select((pl.col("NPeakAbundance") / pl.col("LPeakAbundance")).alias("abundance_ratio"))["abundance_ratio"].mean()
+
+ enr_df = file_db_con.tables["chromPeaks"].filter((pl.col("peaksRatioMPm1") > 0) & (pl.col("ionMode") == ionMode)).select((pl.col("xcount") / (pl.col("xcount") + pl.col("peaksRatioMPm1"))).alias("enrichment"))
+ avgEnrichmentL = enr_df["enrichment"].mean()
+ avgEnrichmentLStd = enr_df["enrichment"].std()
+
+ file_rows.append(
+ {
+ "group_name": grName,
+ "group_color": grColor,
+ "file": file,
+ "ion_mode": ionMode,
+ "n_mzs": nMZs if nMZs > 0 else None,
+ "n_mz_bins": nMZBins if nMZBins > 0 else None,
+ "n_features": nFeatures if nFeatures > 0 else None,
+ "n_metabolites": nMetabolites if nMetabolites > 0 else None,
+ "mz_delta_mean": nMZsPPMDelta,
+ "mz_delta_std": nMZsPPMDeltaStd,
+ "avg_ratio_signals": avgRatioSignals,
+ "avg_ratio_signals_std": avgRatioSignalsStd,
+ "avg_ratio_area": avgRatioFeaturesArea,
+ "avg_ratio_abundance": avgRatioFeaturesAbundance,
+ "avg_enrichment": avgEnrichmentL,
+ "avg_enrichment_std": avgEnrichmentLStd,
+ "error": None,
+ }
+ )
+ except Exception as ex:
+ file_rows.append(
+ {
+ "group_name": grName,
+ "group_color": grColor,
+ "file": file,
+ "ion_mode": ionMode,
+ "error": str(ex),
+ }
+ )
+ # ── Collect bracketing / convoluted summary ────────────────────
+ summary_data = {}
resFileFull = str(self.ui.groupsSave.text())
if os.path.exists(resFileFull):
- texts.append("Convoluted results\n-=-=-=-=-=-=-=-=-=-\n\n")
try:
res_db = PolarsDB(resFileFull, format="xlsx", load_all_tables=True)
convoluted_sheet = None
- for candidate in ("3_Convoluted", "2_StatColumns"):
+ for candidate in ("3_Reintegrated", "2_StatColumns"):
if candidate in res_db.tables:
convoluted_sheet = candidate
break
- if convoluted_sheet is None:
- raise ValueError("No convoluted results sheet found in results file")
-
- table_df = res_db.tables[convoluted_sheet]
-
- features = set()
- negMode = set()
- posMode = set()
- metabolites = {}
- metabolitesIonMode = {}
+ if convoluted_sheet is not None:
+ table_df = res_db.tables[convoluted_sheet]
+
+ features = set()
+ metabolites = {}
+ metabolitesIonMode = {}
+
+ for row in table_df.iter_rows(named=True):
+ num = row["Num"]
+ features.add(num)
+ ogroup = row["OGroup"]
+ if ogroup not in metabolites:
+ metabolites[ogroup] = []
+ metabolites[ogroup].append(num)
+ if ogroup not in metabolitesIonMode:
+ metabolitesIonMode[ogroup] = set()
+ metabolitesIonMode[ogroup].add(row["Ionisation_Mode"])
+
+ summary_data["n_features"] = len(features)
+ summary_data["n_metabolites"] = len(metabolites)
+ summary_data["features_1"] = len([1 for k in metabolites if len(metabolites[k]) == 1])
+ summary_data["features_2"] = len([1 for k in metabolites if len(metabolites[k]) == 2])
+ summary_data["features_3"] = len([1 for k in metabolites if len(metabolites[k]) == 3])
+ summary_data["features_4"] = len([1 for k in metabolites if len(metabolites[k]) == 4])
+ summary_data["features_5"] = len([1 for k in metabolites if len(metabolites[k]) == 5])
+ summary_data["features_5_to_10"] = len([1 for k in metabolites if 5 < len(metabolites[k]) < 11])
+ summary_data["features_10_to_20"] = len([1 for k in metabolites if 10 < len(metabolites[k]) < 21])
+ summary_data["features_gt20"] = len([1 for k in metabolites if 20 < len(metabolites[k])])
+ summary_data["pos_only"] = len([1 for k in metabolitesIonMode if metabolitesIonMode[k] == {"+"}])
+ summary_data["neg_only"] = len([1 for k in metabolitesIonMode if metabolitesIonMode[k] == {"-"}])
+ summary_data["both_modes"] = len([1 for k in metabolitesIonMode if {"+", "-"}.issubset(metabolitesIonMode[k])])
- for row in table_df.iter_rows(named=True):
- num = row["Num"]
- features.add(num)
- if row["Ionisation_Mode"] == "-":
- negMode.add(num)
- else:
- posMode.add(num)
- ogroup = row["OGroup"]
- if ogroup not in metabolites:
- metabolites[ogroup] = []
- metabolites[ogroup].append(num)
- if ogroup not in metabolitesIonMode:
- metabolitesIonMode[ogroup] = set()
- metabolitesIonMode[ogroup].add(row["Ionisation_Mode"])
-
- texts.append(" Features %12d\n Metabolites %12d\n" % (len(features), len(metabolites)))
- texts.append("\n")
- texts.append(" %12d metabolites with a single feature\n" % (len([1 for key in metabolites.keys() if len(metabolites[key]) == 1])))
- texts.append(" %12d metabolites with two features\n" % (len([1 for key in metabolites.keys() if len(metabolites[key]) == 2])))
- texts.append(" %12d metabolites with three features\n" % (len([1 for key in metabolites.keys() if len(metabolites[key]) == 3])))
- texts.append(" %12d metabolites with four features\n" % (len([1 for key in metabolites.keys() if len(metabolites[key]) == 4])))
- texts.append(" %12d metabolites with five features\n" % (len([1 for key in metabolites.keys() if len(metabolites[key]) == 5])))
- texts.append(" %12d metabolites with >5 and <11 features\n" % (len([1 for key in metabolites.keys() if 5 < len(metabolites[key]) < 11])))
- texts.append(" %12d metabolites with >10 and <21 features\n" % (len([1 for key in metabolites.keys() if 10 < len(metabolites[key]) < 21])))
- texts.append(" %12d metabolites with >20 features\n" % (len([1 for key in metabolites.keys() if 20 < len(metabolites[key])])))
- texts.append("\n")
- texts.append(" %12d metabolites with only ions in the positive ionization mode\n" % (len([1 for key in metabolitesIonMode.keys() if metabolitesIonMode[key] == {"+"}])))
- texts.append(" %12d metabolites with only ions in the negative ionization mode\n" % (len([1 for key in metabolitesIonMode.keys() if metabolitesIonMode[key] == {"-"}])))
- texts.append(" %12d metabolites with ions in both ionization modes\n" % (len([1 for key in metabolitesIonMode.keys() if {"+", "-"}.issubset(metabolitesIonMode[key])])))
- texts.append("\n")
- texts.append("\n")
except Exception as ex:
import traceback as _tb
- texts.append(f" Error reading convoluted results: {ex}\n")
- texts.append(f" {_tb.format_exc()}\n\n")
+ logging.warning(f"Error reading convoluted results for summary: {ex}\n{_tb.format_exc()}")
- resFileFull = str(self.ui.groupsSave.text())
- if os.path.exists(resFileFull):
- texts.append("Omitted features\n-=-=-=-=-=-=-=-=-\n\n")
try:
- res_db = PolarsDB(resFileFull, format="xlsx", load_all_tables=True)
+ res_db2 = PolarsDB(resFileFull, format="xlsx", load_all_tables=True)
omitted_sheet = "2_StatColumns_Omitted"
- if omitted_sheet in res_db.tables:
- omitted_df = res_db.tables[omitted_sheet]
- features = set(omitted_df["Num"].to_list())
- texts.append(" Features %12d\n" % len(features))
- else:
- texts.append(" No omitted features found\n")
+ if omitted_sheet in res_db2.tables:
+ omitted_df = res_db2.tables[omitted_sheet]
+ summary_data["omitted_features"] = len(set(omitted_df["Num"].to_list()))
except Exception as ex:
- import traceback as _tb
-
- texts.append(f" Error reading omitted features: {ex}\n")
- texts.append(f" {_tb.format_exc()}\n\n")
-
- # logging.info("".join(texts))
+ logging.warning(f"Error reading omitted features for summary: {ex}")
- pw = QScrollableMessageBox(
- parent=None,
- text="".join(texts),
- title="Processing results",
- width=700,
- height=700,
- )
- pw.exec()
+ dlg = ResultsSummaryDialog(parent=self, file_rows=file_rows, summary_data=summary_data)
+ dlg.exec()
def groupFilesChanges(self, sta):
self.ui.label_26.setEnabled(sta)
@@ -5491,6 +6310,11 @@ def _resolve_col_name(self, df, *candidate_names):
def selectedResChanged(self):
annotationPPM = self.ui.doubleSpinBox_isotopologAnnotationPPM.value()
+ # Always restore "Result name" row; individual branches may hide it again
+ self.ui.label_22.setVisible(True)
+ self.ui.chromPeakName.setVisible(True)
+ self.ui.setChromPeakName.setVisible(True)
+
for i in range(self.ui.res_ExtractedData.topLevelItemCount()):
self.deColorQTreeWidgetItem(self.ui.res_ExtractedData.topLevelItem(i))
@@ -5672,6 +6496,10 @@ def selectedResChanged(self):
#
elif item.myType == "Features" or item.myType == "feature":
self.ui.chromPeakName.setText("")
+ if item.myType == "Features":
+ self.ui.label_22.setVisible(False)
+ self.ui.chromPeakName.setVisible(False)
+ self.ui.setChromPeakName.setVisible(False)
self.ui.res_ExtractedData.setHeaderLabels(
[
@@ -5690,24 +6518,60 @@ def selectedResChanged(self):
)
if item.myType == "Features":
- mzs = []
- rts = []
+ _fm_rts = []
+ _fm_mzs = []
+ _fm_areas = []
+ _fm_point_data = []
plotTypes.add("Features")
- for i in range(item.childCount()):
- child = item.child(i)
- assert child.myType == "feature"
- mzs.append(child.myData.mz)
- rts.append(child.myData.NPeakCenterMin / 60.0)
- self.drawPlot(
- self.ui.pl1,
- plotIndex=0,
- x=rts,
- y=mzs,
- ylab="m/z",
- useCol=0,
- scatter=True,
- plot=False,
+ plotTypes.add("FeatureMap")
+ for _fmi in range(item.childCount()):
+ _fmc = item.child(_fmi)
+ assert _fmc.myType == "feature"
+ _rt = _fmc.myData.NPeakCenterMin / 60.0
+ _mz = _fmc.myData.mz
+ try:
+ _area = max(1.0, float(_fmc.text(7).split(" / ")[0]))
+ except Exception:
+ _area = 1.0
+ _fm_rts.append(_rt)
+ _fm_mzs.append(_mz)
+ _fm_areas.append(_area)
+ _fm_point_data.append(
+ {
+ "rt": _rt,
+ "mz": _mz,
+ "native_area": _area,
+ "id": _fmc.myData.id,
+ "ogroup": "N/A",
+ "polarity": _fmc.myData.ionMode,
+ "charge": _fmc.myData.loading,
+ "xcount": _fmc.myData.xCount,
+ "tree_item": _fmc,
+ }
+ )
+ if _fm_areas:
+ import math as _math
+
+ _log_areas = [_math.log10(a) for a in _fm_areas]
+ _min_log = min(_log_areas)
+ _max_log = max(_log_areas)
+ _range_log = max(_max_log - _min_log, 1.0)
+ _fm_sizes = [20 + 200 * (la - _min_log) / _range_log for la in _log_areas]
+ else:
+ _fm_sizes = []
+ _ax_fm = self.ui.pl1.twinxs[0]
+ _ax_fm._fm_point_data = _fm_point_data
+ _ax_fm.scatter(
+ _fm_rts,
+ _fm_mzs,
+ s=_fm_sizes,
+ c=[predefinedColors[0]] * len(_fm_rts),
+ alpha=0.6,
)
+ _ax_fm.set_xlabel("Retention time (min)")
+ _ax_fm.set_ylabel("m/z")
+ _ax_fm.set_title(f"Feature map ({len(_fm_rts)} feature pairs)")
+ mzs = _fm_mzs
if item.myType == "feature":
cp = item.myData
@@ -6072,6 +6936,10 @@ def selectedResChanged(self):
#
elif item.myType == "featureGroup" or item.myType == "Feature Groups":
+ if item.myType == "Feature Groups":
+ self.ui.label_22.setVisible(False)
+ self.ui.chromPeakName.setVisible(False)
+ self.ui.setChromPeakName.setVisible(False)
self.ui.res_ExtractedData.setHeaderLabels(
[
"Feature group / MZ (/Ionmode Z)",
@@ -6097,27 +6965,81 @@ def selectedResChanged(self):
item.setBackground(7, QColor(predefinedColors[(useColi) % len(predefinedColors)]))
if item.myType == "Feature Groups":
+ import math as _math
+
plotTypes.add("Feature Groups")
- for i in range(item.childCount()):
- child = item.child(i)
- mzs = []
- rts = []
- for j in range(child.childCount()):
- feature = child.child(j)
- assert feature.myType == "feature"
-
- mzs.append(feature.myData.mz)
- rts.append(feature.myData.NPeakCenterMin / 60.0)
- self.drawPlot(
- self.ui.pl1,
- plotIndex=0,
- x=rts,
- y=mzs,
- ylab="m/z",
- useCol=i,
- scatter=True,
- plot=True,
- )
+ plotTypes.add("FeatureMap")
+ _all_fm_rts = []
+ _all_fm_mzs = []
+ _all_fm_sizes = []
+ _all_fm_colors = []
+ _all_fm_point_data = []
+ _fm_group_lines = []
+ for _gi in range(item.childCount()):
+ _gc = item.child(_gi)
+ _g_rts = []
+ _g_mzs = []
+ _g_areas = []
+ _g_group_name = _gc.myData.featureName if hasattr(_gc, "myData") and hasattr(_gc.myData, "featureName") else str(_gc.text(0))
+ for _gfi in range(_gc.childCount()):
+ _gff = _gc.child(_gfi)
+ assert _gff.myType == "feature"
+ _rt = _gff.myData.NPeakCenterMin / 60.0
+ _mz = _gff.myData.mz
+ try:
+ _area = max(1.0, float(_gff.text(7).split(" / ")[0]))
+ except Exception:
+ _area = 1.0
+ _g_rts.append(_rt)
+ _g_mzs.append(_mz)
+ _g_areas.append(_area)
+ _all_fm_point_data.append(
+ {
+ "rt": _rt,
+ "mz": _mz,
+ "native_area": _area,
+ "id": _gff.myData.id,
+ "ogroup": _g_group_name,
+ "polarity": _gff.myData.ionMode,
+ "charge": _gff.myData.loading,
+ "xcount": _gff.myData.xCount,
+ "tree_item": _gff,
+ }
+ )
+ if _g_areas:
+ _g_log = [_math.log10(a) for a in _g_areas]
+ _g_min = min(_g_log)
+ _g_max = max(_g_log)
+ _g_range = max(_g_max - _g_min, 1.0)
+ _g_sizes = [20 + 200 * (la - _g_min) / _g_range for la in _g_log]
+ else:
+ _g_sizes = []
+ _col = predefinedColors[_gi % len(predefinedColors)]
+ _all_fm_rts.extend(_g_rts)
+ _all_fm_mzs.extend(_g_mzs)
+ _all_fm_sizes.extend(_g_sizes)
+ _all_fm_colors.extend([_col] * len(_g_rts))
+ _fm_group_lines.append((_g_rts, _g_mzs, _col))
+ _ax_fg = self.ui.pl1.twinxs[0]
+ _ax_fg._fm_point_data = _all_fm_point_data
+ _ax_fg.scatter(
+ _all_fm_rts,
+ _all_fm_mzs,
+ s=_all_fm_sizes,
+ c=_all_fm_colors,
+ alpha=0.6,
+ )
+ for _line_rts, _line_mzs, _line_col in _fm_group_lines:
+ if len(_line_rts) >= 2:
+ _sorted_pts = sorted(zip(_line_mzs, _line_rts))
+ _sx = [_rt for _, _rt in _sorted_pts]
+ _sy = [_mz for _mz, _ in _sorted_pts]
+ _ax_fg.plot(_sx, _sy, color=_line_col, linewidth=0.8, alpha=0.5, zorder=1)
+ _ax_fg.set_xlabel("Retention time (min)")
+ _ax_fg.set_ylabel("m/z")
+ _n_groups = item.childCount()
+ _n_feat = len(_all_fm_rts)
+ _ax_fg.set_title(f"Feature map ({_n_feat} feature pairs in {_n_groups} metabolite groups)")
if item.myType == "featureGroup":
selFeatureGroups.append(item)
@@ -7188,6 +8110,9 @@ def selectedResChanged(self):
# self.drawPlot(self.ui.pl3, plotIndex=0, x=toDrawMzs, y=toDrawInts, useCol="lightgrey", multipleLocator=None, alpha=0.1, title="", xlab="MZ")
+ if not toDrawMzs:
+ continue
+
bm = (
min(
range(len(toDrawMzs)),
@@ -7989,18 +8914,25 @@ def selectedResChanged(self):
#
elif "Features" in plotTypes or "Feature Groups" in plotTypes or "feature" in plotTypes:
- if self.ui.scaleFeatures.checkState() == QtCore.Qt.Checked:
- self.ui.pl1.axes.set_ylabel("Intensity [counts; normalised]")
- else:
- self.ui.pl1.axes.set_ylabel("Intensity [counts]")
- if self.ui.autoZoomPlot.checkState() == QtCore.Qt.Checked:
- self.drawCanvas(
- self.ui.pl1,
- ylim=(minIntY * 1.6, maxIntY * 1.6),
- xlim=(minTime * 0.85, maxTime * 1.15),
- )
- else:
+ if "FeatureMap" in plotTypes and "feature" not in plotTypes:
+ # Top-level feature map: axes already labelled; just draw canvas
self.drawCanvas(self.ui.pl1)
+ # Connect hover/click handlers so the user can inspect scatter points
+ _fm_ax = self.ui.pl1.twinxs[0]
+ self._setup_feature_map_hover(self.ui.pl1, _fm_ax)
+ else:
+ if self.ui.scaleFeatures.checkState() == QtCore.Qt.Checked:
+ self.ui.pl1.axes.set_ylabel("Intensity [counts; normalised]")
+ else:
+ self.ui.pl1.axes.set_ylabel("Intensity [counts]")
+ if self.ui.autoZoomPlot.checkState() == QtCore.Qt.Checked:
+ self.drawCanvas(
+ self.ui.pl1,
+ ylim=(minIntY * 1.6, maxIntY * 1.6),
+ xlim=(minTime * 0.85, maxTime * 1.15),
+ )
+ else:
+ self.drawCanvas(self.ui.pl1)
#
#
@@ -8106,8 +9038,18 @@ def hasMSMSSpectra(self, mz, rt_min, rt_max, ppm=5.0):
return False
def updateMSMSList(self, selectedItems):
- """Filter and populate MSMS spectra list based on selected features"""
- self.ui.msms_SpectraList.clear()
+ """Filter and populate MSMS spectra table based on selected features"""
+
+ class _NSItem(QTableWidgetItem):
+ def __lt__(self, other):
+ try:
+ return float(self.text()) < float(other.text())
+ except (ValueError, TypeError):
+ return self.text() < other.text()
+
+ tbl = self.ui.msms_SpectraList
+ tbl.setSortingEnabled(False)
+ tbl.setRowCount(0)
if not hasattr(self, "currentOpenRawFile") or self.currentOpenRawFile is None:
return
@@ -8121,26 +9063,23 @@ def updateMSMSList(self, selectedItems):
if hasattr(item, "myType") and (item.myType == "feature" or item.myType == "Features"):
if item.myType == "feature":
cp = item.myData
- # Get RT range in seconds
- rt_min = cp.NPeakCenterMin - cp.NPeakScale
- rt_max = cp.NPeakCenterMin + cp.NPeakScale
- # Also consider labeled peak RT range
- rt_min = min(rt_min, cp.LPeakCenterMin - cp.LPeakScale)
- rt_max = max(rt_max, cp.LPeakCenterMin + cp.LPeakScale)
-
- # Get m/z range with tolerance
+ # Use actual peak start/end RT (in seconds); fall back to center ± scale
+ rt_min_n = getattr(cp, "N_startRT", cp.NPeakCenterMin - cp.NPeakScale)
+ rt_max_n = getattr(cp, "N_endRT", cp.NPeakCenterMin + cp.NPeakScale)
+ rt_min_l = getattr(cp, "L_startRT", cp.LPeakCenterMin - cp.LPeakScale)
+ rt_max_l = getattr(cp, "L_endRT", cp.LPeakCenterMin + cp.LPeakScale)
+ rt_min = min(rt_min_n, rt_min_l)
+ rt_max = max(rt_max_n, rt_max_l)
+
try:
ppm = float(self.getParametersFromCurrentRes("Mass deviation (+/- ppm)"))
except Exception:
ppm = 5.0
- # Store native (M) and labeled (M') ranges separately
native_mz_min = cp.mz * (1 - ppm / 1000000.0)
native_mz_max = cp.mz * (1 + ppm / 1000000.0)
-
feature_ranges.append({"rt_min": rt_min, "rt_max": rt_max, "mz_min": native_mz_min, "mz_max": native_mz_max, "feature_name": "%.4f @ %.2f min" % (cp.mz, cp.NPeakCenterMin / 60.0), "form": "native"})
- # Add labeled form if available
if hasattr(cp, "lmz"):
labeled_mz_min = cp.lmz * (1 - ppm / 1000000.0)
labeled_mz_max = cp.lmz * (1 + ppm / 1000000.0)
@@ -8149,37 +9088,288 @@ def updateMSMSList(self, selectedItems):
if not feature_ranges:
return
- # Filter MS2 spectra within feature ranges
matching_ms2 = []
for ms2_scan in self.currentOpenRawFile.MS2_list:
for fr in feature_ranges:
- # Check if MS2 scan is within RT range
if fr["rt_min"] <= ms2_scan.retention_time <= fr["rt_max"]:
- # Check if precursor m/z is within feature m/z range
if fr["mz_min"] <= ms2_scan.precursor_mz <= fr["mz_max"]:
matching_ms2.append({"scan": ms2_scan, "feature_name": fr["feature_name"], "form": fr["form"]})
break
- # Populate list widget
+ _native_color = QtGui.QColor(30, 144, 255, 60) # dodgerblue
+ _labeled_color = QtGui.QColor(178, 34, 34, 60) # firebrick
+
for ms2_info in sorted(matching_ms2, key=lambda x: x["scan"].precursor_intensity):
scan = ms2_info["scan"]
- form_label = "M" if ms2_info["form"] == "native" else "M'"
- item_text = "%s: I %.3g, %.4f @ %.2f min" % (form_label, scan.precursor_intensity, scan.precursor_mz, scan.retention_time / 60.0)
- list_item = QtWidgets.QListWidgetItem(item_text)
- list_item.setData(QtCore.Qt.UserRole, scan) # Store scan object
- list_item.setData(QtCore.Qt.UserRole + 1, ms2_info["form"]) # Store form type
- self.ui.msms_SpectraList.addItem(list_item)
-
- # Auto-select last 6 entries
- total_items = self.ui.msms_SpectraList.count()
- if total_items > 0:
- self.ui.msms_SpectraList.item(total_items - 1).setSelected(True)
+ form_label = "M" if ms2_info["form"] == "native" else "M\u2032"
+ row_idx = tbl.rowCount()
+ tbl.insertRow(row_idx)
+
+ nl_item = _NSItem(form_label)
+ nl_item.setData(QtCore.Qt.UserRole, scan)
+ nl_item.setData(QtCore.Qt.UserRole + 1, ms2_info["form"])
+ tbl.setItem(row_idx, 0, nl_item)
+ tbl.setItem(row_idx, 1, _NSItem(f"{scan.precursor_intensity:.4g}"))
+ tbl.setItem(row_idx, 2, _NSItem(f"{scan.precursor_mz:.4f}"))
+ tbl.setItem(row_idx, 3, _NSItem(f"{scan.retention_time / 60.0:.2f}"))
+
+ row_color = _labeled_color if ms2_info["form"] == "labeled" else _native_color
+ for col in range(4):
+ tbl.item(row_idx, col).setBackground(row_color)
+
+ tbl.setSortingEnabled(True)
+ tbl.resizeColumnsToContents()
+
+ total_rows = tbl.rowCount()
+ if total_rows > 0:
+ tbl.selectRow(total_rows - 1)
+
+ def _setup_msms_hover(self, plot_obj):
+ """Connect hover and click handlers to an MSMS canvas.
+
+ Hover: when the cursor is close to a fragment peak (using normalised 2-D
+ distance in m/z and intensity), the vline is redrawn 3× thicker and a
+ popup annotation shows its m/z value.
+
+ Click: same proximity test; if close enough the annotation is *pinned*
+ and will survive zoom/pan. Pinned annotations are cleared the next time
+ this method is called (i.e. when new spectra are selected).
+
+ State on plot_obj:
+ _hover_cid – mpl connection id for motion_notify_event
+ _click_cid – mpl connection id for button_press_event
+ _hover_artists – temporary artists removed on next move event
+ _pinned_artists – persistent artists cleared on next setup call
+ """
+ canvas = plot_obj.canvas
+
+ # Disconnect any previous handlers
+ for attr in ("_hover_cid", "_click_cid"):
+ cid = getattr(plot_obj, attr, None)
+ if cid is not None:
+ try:
+ canvas.mpl_disconnect(cid)
+ except Exception:
+ pass
+ plot_obj._hover_cid = None
+ plot_obj._click_cid = None
+ plot_obj._hover_artists = []
+
+ # Clear pinned annotations from previous spectra
+ for artist in list(getattr(plot_obj, "_pinned_artists", [])):
+ try:
+ artist.remove()
+ except Exception:
+ pass
+ plot_obj._pinned_artists = []
+
+ if not plot_obj.axes:
+ return
+
+ def _find_closest(ax, mx, my):
+ """Return (cidx, norm_dist) for the nearest peak using 2-D normalised distance."""
+ peaks = getattr(ax, "_msms_peaks", None)
+ if peaks is None or len(peaks[0]) == 0:
+ return None, None
+ mz_arr, int_arr, _ = peaks
+ xlim = ax.get_xlim()
+ ylim = ax.get_ylim()
+ x_range = xlim[1] - xlim[0] or 1.0
+ y_range = ylim[1] - ylim[0] or 1.0
+ best_idx, best_dist = 0, float("inf")
+ for i, (mz, iv) in enumerate(zip(mz_arr, int_arr)):
+ dx = abs(float(mz) - mx) / x_range
+ dy = abs(float(iv) - my) / y_range
+ d = (dx**2 + dy**2) ** 0.5
+ if d < best_dist:
+ best_dist = d
+ best_idx = i
+ return best_idx, best_dist
+
+ def _on_hover(event):
+ for artist in list(plot_obj._hover_artists):
+ try:
+ artist.remove()
+ except Exception:
+ pass
+ plot_obj._hover_artists.clear()
+
+ if event.inaxes is None or event.xdata is None or event.ydata is None:
+ canvas.draw_idle()
+ return
+
+ ax = event.inaxes
+ peaks = getattr(ax, "_msms_peaks", None)
+ if peaks is None or len(peaks[0]) == 0:
+ canvas.draw_idle()
+ return
+
+ mz_arr, int_arr, spec_color = peaks
+ cidx, nd = _find_closest(ax, event.xdata, event.ydata)
+ if nd is None or nd > 0.03:
+ canvas.draw_idle()
+ return
+
+ mz_val = float(mz_arr[cidx])
+ int_val = float(int_arr[cidx])
+
+ vl = ax.vlines(mz_val, 0, int_val, colors=spec_color, linewidth=4.5, zorder=5)
+ plot_obj._hover_artists.append(vl)
+
+ ann = ax.annotate(
+ "m/z %.4f" % mz_val,
+ xy=(mz_val, int_val),
+ xytext=(8, 8),
+ textcoords="offset points",
+ fontsize=11,
+ color="#202124",
+ bbox=dict(boxstyle="round,pad=0.4", facecolor="lightyellow", edgecolor="#aaaaaa", alpha=0.95),
+ zorder=10,
+ )
+ plot_obj._hover_artists.append(ann)
+ canvas.draw_idle()
+
+ def _on_click(event):
+ if event.inaxes is None or event.xdata is None or event.ydata is None:
+ return
+
+ ax = event.inaxes
+ peaks = getattr(ax, "_msms_peaks", None)
+ if peaks is None or len(peaks[0]) == 0:
+ return
+
+ mz_arr, int_arr, spec_color = peaks
+ cidx, nd = _find_closest(ax, event.xdata, event.ydata)
+ if nd is None or nd > 0.03:
+ return
+
+ mz_val = float(mz_arr[cidx])
+ int_val = float(int_arr[cidx])
+
+ # Pin a thick vline and labelled annotation that survive zoom/pan
+ vl = ax.vlines(mz_val, 0, int_val, colors=spec_color, linewidth=4.5, zorder=5)
+ plot_obj._pinned_artists.append(vl)
+
+ ann = ax.annotate(
+ "m/z %.4f" % mz_val,
+ xy=(mz_val, int_val),
+ xytext=(8, 8),
+ textcoords="offset points",
+ fontsize=11,
+ color="#202124",
+ bbox=dict(boxstyle="round,pad=0.4", facecolor="lightyellow", edgecolor="#888888", alpha=0.98),
+ zorder=11,
+ )
+ plot_obj._pinned_artists.append(ann)
+ canvas.draw_idle()
+
+ plot_obj._hover_cid = canvas.mpl_connect("motion_notify_event", _on_hover)
+ plot_obj._click_cid = canvas.mpl_connect("button_press_event", _on_click)
+
+ def _setup_feature_map_hover(self, plot_obj, ax):
+ """Connect hover and click handlers to a feature-map scatter canvas.
+
+ On hover: find the closest scatter point (normalised 2-D RT/m/z distance)
+ and display a popup showing id, metabolite-group, polarity, charge, m/z,
+ retention time and native abundance.
+
+ On click: same proximity test; if close enough, navigate the sample-results
+ tree to the corresponding tree item.
+
+ Feature data is read from ``ax._fm_point_data``, a list of dicts with keys:
+ rt, mz, native_area, id, ogroup, polarity, charge, tree_item
+ """
+ canvas = plot_obj.canvas
+
+ # Disconnect any previous feature-map handlers
+ for attr in ("_fm_hover_cid", "_fm_click_cid"):
+ cid = getattr(plot_obj, attr, None)
+ if cid is not None:
+ try:
+ canvas.mpl_disconnect(cid)
+ except Exception:
+ pass
+ plot_obj._fm_hover_cid = None
+ plot_obj._fm_click_cid = None
+ plot_obj._fm_hover_artists = []
+
+ point_data = getattr(ax, "_fm_point_data", [])
+ if not point_data:
+ return
+
+ def _find_closest_fm(event_ax, ex, ey):
+ xlim = event_ax.get_xlim()
+ ylim = event_ax.get_ylim()
+ x_range = (xlim[1] - xlim[0]) or 1.0
+ y_range = (ylim[1] - ylim[0]) or 1.0
+ best_idx, best_dist = 0, float("inf")
+ for i, pt in enumerate(point_data):
+ dx = abs(pt["rt"] - ex) / x_range
+ dy = abs(pt["mz"] - ey) / y_range
+ d = (dx**2 + dy**2) ** 0.5
+ if d < best_dist:
+ best_dist = d
+ best_idx = i
+ return best_idx, best_dist
+
+ def _on_fm_hover(event):
+ for artist in list(plot_obj._fm_hover_artists):
+ try:
+ artist.remove()
+ except Exception:
+ pass
+ plot_obj._fm_hover_artists.clear()
+
+ if event.inaxes is not ax or event.xdata is None or event.ydata is None:
+ canvas.draw_idle()
+ return
+
+ cidx, nd = _find_closest_fm(ax, event.xdata, event.ydata)
+ if nd > 0.04:
+ canvas.draw_idle()
+ return
+
+ pt = point_data[cidx]
+ label = f"ID: {pt['id']}\nGroup: {pt['ogroup']}\nPolarity: {pt['polarity']}\nCharge: {pt['charge']}\nXn: {pt.get('xcount', 'N/A')}\nm/z: {pt['mz']:.5f}\nRT: {pt['rt']:.2f} min\nNative abundance: {pt['native_area']:.1f}"
+ ann = ax.annotate(
+ label,
+ xy=(pt["rt"], pt["mz"]),
+ xytext=(12, 8),
+ textcoords="offset points",
+ fontsize=18,
+ color="#202124",
+ bbox=dict(boxstyle="round,pad=0.7", facecolor="lightyellow", edgecolor="#aaaaaa", alpha=0.95),
+ zorder=10,
+ )
+ plot_obj._fm_hover_artists.append(ann)
+ canvas.draw_idle()
+
+ def _on_fm_dblclick(event):
+ if event.dblclick is False or event.inaxes is not ax or event.xdata is None or event.ydata is None:
+ return
+
+ cidx, nd = _find_closest_fm(ax, event.xdata, event.ydata)
+ if nd > 0.04:
+ return
+
+ tree_item = point_data[cidx].get("tree_item")
+ if tree_item is None:
+ return
+ tree = self.ui.res_ExtractedData
+ parent = tree_item.parent()
+ if parent is not None:
+ tree.expandItem(parent)
+ tree.setCurrentItem(tree_item)
+ tree.scrollToItem(tree_item)
+
+ plot_obj._fm_hover_cid = canvas.mpl_connect("motion_notify_event", _on_fm_hover)
+ plot_obj._fm_click_cid = canvas.mpl_connect("button_press_event", _on_fm_dblclick)
def plotSelectedMSMSSpectra(self):
"""Plot selected MSMS spectra as subplots with shared x-axis"""
- selected_items = self.ui.msms_SpectraList.selectedItems()
+ selected_rows = sorted(set(item.row() for item in self.ui.msms_SpectraList.selectedItems()))
- if not selected_items:
+ if not selected_rows:
# Clear the plot
self.ui.plMSMS.fig.clear()
self.ui.plMSMS.axes = []
@@ -8191,15 +9381,18 @@ def plotSelectedMSMSSpectra(self):
self.ui.plMSMS.axes = []
# Calculate subplot grid
- n_spectra = len(selected_items)
- n_cols = min(2, n_spectra)
+ n_spectra = len(selected_rows)
+ n_cols = 1 if n_spectra == 2 else min(2, n_spectra)
n_rows = (n_spectra + n_cols - 1) // n_cols
# Create subplots with shared x-axis
first_ax = None
- for idx, item in enumerate(selected_items):
- scan = item.data(QtCore.Qt.UserRole)
- form_type = item.data(QtCore.Qt.UserRole + 1) # Get form type (native/labeled)
+ for idx, row_idx in enumerate(selected_rows):
+ col0 = self.ui.msms_SpectraList.item(row_idx, 0)
+ if col0 is None:
+ continue
+ scan = col0.data(QtCore.Qt.UserRole)
+ form_type = col0.data(QtCore.Qt.UserRole + 1)
# Determine color based on form type
if form_type == "labeled":
@@ -8222,115 +9415,202 @@ def plotSelectedMSMSSpectra(self):
if len(scan.mz_list) > 0:
ax.vlines(scan.mz_list, 0, scan.intensity_list, colors=spectrum_color, linewidth=1.5)
ax.plot(scan.mz_list, scan.intensity_list, "o", markersize=3, color=spectrum_color)
+ # Store per-axis peak data for hover interactivity
+ ax._msms_peaks = (scan.mz_list.copy(), scan.intensity_list.copy(), spectrum_color)
- # Label the 10 most abundant peaks
if len(scan.intensity_list) > 0:
- # Get indices of top 10 peaks by intensity
- # Create list of (intensity, index) pairs, sort by intensity, get top 10 indices
- intensity_with_idx = [(intensity, idx) for idx, intensity in enumerate(scan.intensity_list)]
+ intensity_with_idx = [(intensity, i) for i, intensity in enumerate(scan.intensity_list)]
intensity_with_idx.sort(reverse=True)
- top_indices = [idx for _, idx in intensity_with_idx[:10]]
+ top_indices = [i for _, i in intensity_with_idx[:10]]
for peak_idx in top_indices:
mz_val = scan.mz_list[peak_idx]
intensity_val = scan.intensity_list[peak_idx]
- # Add text label above the peak
- ax.text(mz_val, intensity_val, "%.4f" % mz_val, fontsize=12, ha="center", va="bottom", rotation=90, color=label_color, alpha=0.3)
+ ax.text(mz_val, intensity_val * 1.01, "%.4f" % mz_val, fontsize=9, ha="center", va="bottom", rotation=0, color=label_color, alpha=0.6)
- # Set labels and title
ax.set_xlabel("m/z", fontsize=12)
ax.set_ylabel("Intensity", fontsize=12)
ax.set_title("Scan %d: %.4f m/z | RT %.2f min | I %.3g" % (scan.id, scan.precursor_mz, scan.retention_time / 60.0, scan.precursor_intensity), fontsize=11)
ax.tick_params(labelsize=12)
ax.grid(True, alpha=0.3)
- # Apply tight layout for optimal spacing
try:
self.ui.plMSMS.fig.tight_layout()
except Exception:
- # Fallback to manual adjustment if tight_layout fails
self.ui.plMSMS.fig.subplots_adjust(left=0.08, bottom=0.08, right=0.98, top=0.95, hspace=0.4, wspace=0.3)
+ self._setup_msms_hover(self.ui.plMSMS)
self.ui.plMSMS.canvas.draw()
def updateMSMSList_exp(self, selectedItems):
- """Filter and populate MSMS spectra list for experimental results panel"""
- self.ui.msms_SpectraList_exp.clear()
+ """Filter and populate MSMS spectra table for experimental results panel"""
+
+ class _NSItem(QTableWidgetItem):
+ def __lt__(self, other):
+ try:
+ return float(self.text()) < float(other.text())
+ except (ValueError, TypeError):
+ return self.text() < other.text()
+
+ tbl = self.ui.msms_SpectraList_exp
+ tbl.setSortingEnabled(False)
+ tbl.setRowCount(0)
if not hasattr(self, "loadedMZXMLs") or self.loadedMZXMLs is None:
return
- # Get ppm tolerance and RT border offset from UI
- try:
- ppm = self.ui.doubleSpinBox_resultsExperiment_EICppm.value()
- except Exception:
- ppm = 5.0
-
try:
borderOffset = self.ui.doubleSpinBox_resultsExperiment_PeakWidth.value()
except Exception:
- borderOffset = 0.5 # Default 0.5 minutes
+ borderOffset = 0.5
+
+ # Build file-path -> group-name mapping (loadedMZXMLs is keyed by both path and group)
+ file_to_group = {}
+ for grp in self.getAllSampleGroups():
+ for fpath in grp.files:
+ file_to_group[fpath] = grp.name
+
+ # Only consider keys that are actual file paths (not group-name aliases)
+ file_keys = [k for k in self.loadedMZXMLs if k.lower().endswith(".mzxml") or k.lower().endswith(".mzml")]
- # Collect RT and m/z ranges from selected features
feature_ranges = []
+ # Build per-feature, per-sample RT bounds from the results DB when available
+ rows_by_num = {}
+ if hasattr(self, "experimentResults") and self.experimentResults is not None and self.experimentResults.db_con is not None:
+ for sheet in ["4_Convoluted", "3_Reintegrated", "1_Bracketed"]:
+ try:
+ tdf = self.experimentResults.db_con.get_table(sheet)
+ if tdf is not None and not tdf.is_empty():
+ rows_by_num = {r["Num"]: r for r in tdf.to_dicts()}
+ break
+ except Exception:
+ pass
+
for item in selectedItems:
if hasattr(item, "bunchData"):
bd = item.bunchData
if bd.type == "featurePair":
- # Get RT range using borderOffset (rt is in seconds)
- rt_min = bd.rt - (borderOffset * 60.0) # Convert minutes to seconds
- rt_max = bd.rt + (borderOffset * 60.0)
+ try:
+ ppm = self.ui.doubleSpinBox_resultsExperiment_EICppm.value()
+ except Exception:
+ ppm = 5.0
- # Store native (M) and labeled (M') ranges separately
native_mz_min = bd.mz * (1 - ppm / 1000000.0)
native_mz_max = bd.mz * (1 + ppm / 1000000.0)
-
- feature_ranges.append({"rt_min": rt_min, "rt_max": rt_max, "mz_min": native_mz_min, "mz_max": native_mz_max, "feature_name": "%.4f @ %.2f min" % (bd.mz, bd.rt / 60.0), "form": "native"})
-
- # Add labeled form
labeled_mz_min = bd.lmz * (1 - ppm / 1000000.0)
labeled_mz_max = bd.lmz * (1 + ppm / 1000000.0)
- feature_ranges.append({"rt_min": rt_min, "rt_max": rt_max, "mz_min": labeled_mz_min, "mz_max": labeled_mz_max, "feature_name": "%.4f @ %.2f min" % (bd.lmz, bd.rt / 60.0), "form": "labeled"})
+
+ # Build a per-file RT range dict: file_key -> (rt_min_s, rt_max_s)
+ # Values are in seconds for comparison with ms2_scan.retention_time
+ per_file_rt = {}
+ row_data = rows_by_num.get(getattr(bd, "id", None))
+ if row_data is not None:
+ for file_key in file_keys:
+ fname = os.path.basename(file_key)
+ for ext in [".mzxml", ".mzml"]:
+ if fname.lower().endswith(ext):
+ fname = fname[: -len(ext)]
+ break
+ sv = row_data.get(f"{fname}_N_startRT")
+ ev = row_data.get(f"{fname}_N_endRT")
+ lsv = row_data.get(f"{fname}_L_startRT")
+ lev = row_data.get(f"{fname}_L_endRT")
+ try:
+ # DB stores RT in minutes; convert to seconds
+ rt_min_s = min(
+ float(str(sv).split(";")[0]) * 60.0 if sv is not None else float("inf"),
+ float(str(lsv).split(";")[0]) * 60.0 if lsv is not None else float("inf"),
+ )
+ rt_max_s = max(
+ float(str(ev).split(";")[0]) * 60.0 if ev is not None else float("-inf"),
+ float(str(lev).split(";")[0]) * 60.0 if lev is not None else float("-inf"),
+ )
+ if rt_min_s != float("inf") and rt_max_s != float("-inf"):
+ per_file_rt[file_key] = (rt_min_s, rt_max_s)
+ except (TypeError, ValueError):
+ pass
+
+ feature_ranges.append(
+ {
+ "native_mz_min": native_mz_min,
+ "native_mz_max": native_mz_max,
+ "labeled_mz_min": labeled_mz_min,
+ "labeled_mz_max": labeled_mz_max,
+ "per_file_rt": per_file_rt,
+ "global_rt": bd.rt, # seconds, used as fallback
+ "feature_num": getattr(bd, "id", None),
+ "metaboliteGroupID": getattr(bd, "metaboliteGroupID", None),
+ "xn": getattr(bd, "xn", None),
+ }
+ )
if not feature_ranges:
return
- # Collect MS2 scans from all loaded files
all_ms2_scans = []
- for file_key, mzxml_file in self.loadedMZXMLs.items():
- if hasattr(mzxml_file, "MS2_list") and len(mzxml_file.MS2_list) > 0:
- for ms2_scan in mzxml_file.MS2_list:
- # Check if this scan matches any feature range
- for fr in feature_ranges:
- if fr["rt_min"] <= ms2_scan.retention_time <= fr["rt_max"]:
- if fr["mz_min"] <= ms2_scan.precursor_mz <= fr["mz_max"]:
- all_ms2_scans.append({"scan": ms2_scan, "form": fr["form"], "file": file_key})
- break
-
- # Add to list widget - sort by filename then RT using natural sort
- temp_list = []
- for scan_info in all_ms2_scans:
- scan = scan_info["scan"]
- form = scan_info["form"]
- file_key = scan_info["file"]
-
- # Extract filename from path
- filename = os.path.basename(file_key)
- temp_list.append((scan, form, filename))
+ for file_key in file_keys:
+ mzxml_file = self.loadedMZXMLs[file_key]
+ if not (hasattr(mzxml_file, "MS2_list") and len(mzxml_file.MS2_list) > 0):
+ continue
+ for ms2_scan in mzxml_file.MS2_list:
+ for fr in feature_ranges:
+ # Determine RT bounds for this specific file
+ if file_key in fr["per_file_rt"]:
+ rt_min_s, rt_max_s = fr["per_file_rt"][file_key]
+ else:
+ # Fallback: borderOffset around the global feature RT
+ rt_min_s = fr["global_rt"] - (borderOffset * 60.0)
+ rt_max_s = fr["global_rt"] + (borderOffset * 60.0)
+
+ if not (rt_min_s <= ms2_scan.retention_time <= rt_max_s):
+ continue
+
+ # Check precursor m/z against native form
+ if fr["native_mz_min"] <= ms2_scan.precursor_mz <= fr["native_mz_max"]:
+ all_ms2_scans.append({"scan": ms2_scan, "form": "native", "file": file_key, "feature_num": fr.get("feature_num"), "o_group": fr.get("metaboliteGroupID"), "xn": fr.get("xn")})
+ break
+ # Check against labeled form
+ if fr["labeled_mz_min"] <= ms2_scan.precursor_mz <= fr["labeled_mz_max"]:
+ all_ms2_scans.append({"scan": ms2_scan, "form": "labeled", "file": file_key, "feature_num": fr.get("feature_num"), "o_group": fr.get("metaboliteGroupID"), "xn": fr.get("xn")})
+ break
+
+ sorted_scans = [(s["scan"], s["form"], s["file"], s.get("feature_num"), s.get("o_group"), s.get("xn")) for s in all_ms2_scans]
+ sorted_scans = natSort(sorted_scans, key=lambda x: x[0].precursor_intensity)
- # Sort by filename (natural sort) then by retention time
- temp_list = natSort(temp_list, key=lambda x: (x[0].precursor_intensity))
+ _native_color = QtGui.QColor(30, 144, 255, 60)
+ _labeled_color = QtGui.QColor(178, 34, 34, 60)
- for scan, form, filename in temp_list:
- item_text = "%s: I %.3g, %.4f @ %.2f, %s" % ("M'" if form == "labeled" else "M", scan.precursor_intensity, scan.precursor_mz, scan.retention_time / 60.0, filename)
- item = QtWidgets.QListWidgetItem(item_text)
- item.setData(QtCore.Qt.UserRole, scan)
- item.setData(QtCore.Qt.UserRole + 1, form)
- self.ui.msms_SpectraList_exp.addItem(item)
+ for scan, form, file_key, feature_num, o_group, xn in sorted_scans:
+ form_label = "M\u2032" if form == "labeled" else "M"
+ row_idx = tbl.rowCount()
+ tbl.insertRow(row_idx)
+
+ group_name = file_to_group.get(file_key, "")
+ filename = os.path.basename(file_key)
- # Auto-select last 6 entries
- total_items = self.ui.msms_SpectraList_exp.count()
- if total_items > 0:
- self.ui.msms_SpectraList_exp.item(total_items - 1).setSelected(True)
+ nl_item = _NSItem(form_label)
+ nl_item.setData(QtCore.Qt.UserRole, scan)
+ nl_item.setData(QtCore.Qt.UserRole + 1, form)
+ nl_item.setData(QtCore.Qt.UserRole + 2, feature_num)
+ nl_item.setData(QtCore.Qt.UserRole + 3, o_group)
+ nl_item.setData(QtCore.Qt.UserRole + 4, file_key)
+ nl_item.setData(QtCore.Qt.UserRole + 5, xn)
+ tbl.setItem(row_idx, 0, nl_item)
+ tbl.setItem(row_idx, 1, _NSItem(f"{scan.precursor_intensity:.4g}"))
+ tbl.setItem(row_idx, 2, _NSItem(f"{scan.precursor_mz:.4f}"))
+ tbl.setItem(row_idx, 3, _NSItem(f"{scan.retention_time / 60.0:.2f}"))
+ tbl.setItem(row_idx, 4, _NSItem(group_name))
+ tbl.setItem(row_idx, 5, _NSItem(filename))
+
+ row_color = _labeled_color if form == "labeled" else _native_color
+ for col in range(6):
+ tbl.item(row_idx, col).setBackground(row_color)
+
+ tbl.setSortingEnabled(True)
+ tbl.resizeColumnsToContents()
+
+ total_rows = tbl.rowCount()
+ if total_rows > 0:
+ tbl.selectRow(total_rows - 1)
def updatePeakDetailsTab(self, plotItems):
"""Populate the peak details tab tables for the selected features."""
@@ -8384,7 +9664,7 @@ def _make_item(text):
# Use cached DataFrame to avoid re-reading the Excel file on every feature selection change
results_df = getattr(self.experimentResults, "_peak_details_df", None)
if results_df is None:
- sheet_candidates = ["4_Reintegrated", "3_Convoluted", "1_Bracketed"]
+ sheet_candidates = ["4_Convoluted", "3_Reintegrated", "1_Bracketed"]
for sheet_name in sheet_candidates:
try:
tbl = self.experimentResults.db_con.get_table(sheet_name)
@@ -8704,39 +9984,36 @@ def _grp_cell(text, color=grp_color):
def plotSelectedMSMSSpectra_exp(self):
"""Plot selected MSMS spectra from experimental results panel"""
- selected_items = self.ui.msms_SpectraList_exp.selectedItems()
+ selected_rows = sorted(set(item.row() for item in self.ui.msms_SpectraList_exp.selectedItems()))
- if not selected_items:
- # Clear the plot
+ if not selected_rows:
self.ui.plMSMS_exp.fig.clear()
self.ui.plMSMS_exp.axes = []
self.ui.plMSMS_exp.canvas.draw()
return
- # Clear previous plots
self.ui.plMSMS_exp.fig.clear()
self.ui.plMSMS_exp.axes = []
- # Calculate subplot grid
- n_spectra = len(selected_items)
- n_cols = min(2, n_spectra)
+ n_spectra = len(selected_rows)
+ n_cols = 1 if n_spectra == 2 else min(2, n_spectra)
n_rows = (n_spectra + n_cols - 1) // n_cols
- # Create subplots with shared x-axis
first_ax = None
- for idx, item in enumerate(selected_items):
- scan = item.data(QtCore.Qt.UserRole)
- form_type = item.data(QtCore.Qt.UserRole + 1)
+ for idx, row_idx in enumerate(selected_rows):
+ col0 = self.ui.msms_SpectraList_exp.item(row_idx, 0)
+ if col0 is None:
+ continue
+ scan = col0.data(QtCore.Qt.UserRole)
+ form_type = col0.data(QtCore.Qt.UserRole + 1)
- # Determine color based on form type
if form_type == "labeled":
spectrum_color = "firebrick"
label_color = "darkred"
- else: # native
+ else:
spectrum_color = "dodgerblue"
label_color = "darkblue"
- # Create subplot with shared x-axis
if idx == 0:
ax = self.ui.plMSMS_exp.fig.add_subplot(n_rows, n_cols, idx + 1)
first_ax = ax
@@ -8745,36 +10022,455 @@ def plotSelectedMSMSSpectra_exp(self):
self.ui.plMSMS_exp.axes.append(ax)
- # Plot MS/MS spectrum as stem plot with form-specific color
if len(scan.mz_list) > 0:
ax.vlines(scan.mz_list, 0, scan.intensity_list, colors=spectrum_color, linewidth=1.5)
ax.plot(scan.mz_list, scan.intensity_list, "o", markersize=3, color=spectrum_color)
+ # Store per-axis peak data for hover interactivity
+ ax._msms_peaks = (scan.mz_list.copy(), scan.intensity_list.copy(), spectrum_color)
- # Label the 10 most abundant peaks
if len(scan.intensity_list) > 0:
- intensity_with_idx = [(intensity, idx) for idx, intensity in enumerate(scan.intensity_list)]
+ intensity_with_idx = [(intensity, i) for i, intensity in enumerate(scan.intensity_list)]
intensity_with_idx.sort(reverse=True)
- top_indices = [idx for _, idx in intensity_with_idx[:10]]
+ top_indices = [i for _, i in intensity_with_idx[:10]]
for peak_idx in top_indices:
mz_val = scan.mz_list[peak_idx]
intensity_val = scan.intensity_list[peak_idx]
- ax.text(mz_val, intensity_val, "%.4f" % mz_val, fontsize=12, ha="center", va="bottom", rotation=90, color=label_color, alpha=0.3)
+ ax.text(mz_val, intensity_val * 1.01, "%.4f" % mz_val, fontsize=9, ha="center", va="bottom", rotation=0, color=label_color, alpha=0.6)
- # Set labels and title
ax.set_xlabel("m/z", fontsize=12)
ax.set_ylabel("Intensity", fontsize=12)
ax.set_title("Scan %d: %.4f m/z | RT %.2f min | I %.3g" % (scan.id, scan.precursor_mz, scan.retention_time / 60.0, scan.precursor_intensity), fontsize=11)
ax.tick_params(labelsize=12)
ax.grid(True, alpha=0.3)
- # Apply tight layout for optimal spacing
try:
self.ui.plMSMS_exp.fig.tight_layout()
except Exception:
self.ui.plMSMS_exp.fig.subplots_adjust(left=0.08, bottom=0.08, right=0.98, top=0.95, hspace=0.4, wspace=0.3)
+ self._setup_msms_hover(self.ui.plMSMS_exp)
self.ui.plMSMS_exp.canvas.draw()
+ def _iter_exp_msms_rows(self):
+ tbl = self.ui.msms_SpectraList_exp
+ for row_idx in range(tbl.rowCount()):
+ col0 = tbl.item(row_idx, 0)
+ if col0 is None:
+ continue
+ scan = col0.data(QtCore.Qt.UserRole)
+ form = col0.data(QtCore.Qt.UserRole + 1)
+ feature_num = col0.data(QtCore.Qt.UserRole + 2)
+ o_group = col0.data(QtCore.Qt.UserRole + 3)
+ file_key = col0.data(QtCore.Qt.UserRole + 4)
+ xn = col0.data(QtCore.Qt.UserRole + 5)
+ if scan is None:
+ continue
+ yield {
+ "row": row_idx,
+ "scan": scan,
+ "form": form,
+ "feature_num": feature_num,
+ "o_group": o_group,
+ "file_key": file_key,
+ "xn": xn,
+ }
+
+ def _to_matchms_spectrum(self, scan):
+ if not MATCHMS_AVAILABLE:
+ return None
+ if scan is None or len(scan.mz_list) == 0:
+ return None
+ spec = MatchmsSpectrum(
+ mz=np.asarray(scan.mz_list, dtype=float),
+ intensities=np.asarray(scan.intensity_list, dtype=float),
+ metadata={"precursor_mz": float(scan.precursor_mz), "retention_time": float(scan.retention_time)},
+ )
+ return matchms_normalize_intensities(spec)
+
+ def _show_msms_similarity_dialog(self, form_filter):
+ if not MATCHMS_AVAILABLE:
+ QtWidgets.QMessageBox.warning(self, "MS/MS similarity", "matchms is not available in this environment.")
+ return
+
+ rows = [r for r in self._iter_exp_msms_rows() if r.get("form") == form_filter and r.get("feature_num") is not None]
+ by_feature = defaultdict(list)
+ for row in rows:
+ by_feature[row["feature_num"]].append(row)
+ feature_ids = sorted(by_feature.keys())
+ if len(feature_ids) < 2:
+ QtWidgets.QMessageBox.information(self, "MS/MS similarity", "At least two features with MS/MS spectra are required.")
+ return
+
+ repr_scans = {}
+ for fid in feature_ids:
+ repr_scans[fid] = max(by_feature[fid], key=lambda x: float(getattr(x["scan"], "precursor_intensity", 0.0)))["scan"]
+
+ dlg = QtWidgets.QDialog(self)
+ dlg.setWindowTitle(f"MS/MS similarity ({'native' if form_filter == 'native' else 'labeled'})")
+ dlg.resize(980, 760)
+ layout = QtWidgets.QVBoxLayout(dlg)
+
+ ctrl = QtWidgets.QHBoxLayout()
+ ctrl.addWidget(QtWidgets.QLabel("Similarity threshold:"))
+ threshold_spin = QtWidgets.QDoubleSpinBox()
+ threshold_spin.setRange(0.0, 1.0)
+ threshold_spin.setDecimals(3)
+ threshold_spin.setSingleStep(0.05)
+ threshold_spin.setValue(0.7)
+ ctrl.addWidget(threshold_spin)
+ ctrl.addStretch(1)
+ layout.addLayout(ctrl)
+
+ matrix = QtWidgets.QTableWidget()
+ matrix.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
+ matrix.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems)
+ matrix.setRowCount(len(feature_ids))
+ matrix.setColumnCount(len(feature_ids))
+ labels = [f"Num {fid}" for fid in feature_ids]
+ matrix.setVerticalHeaderLabels(labels)
+ matrix.setHorizontalHeaderLabels(labels)
+ layout.addWidget(matrix, 1)
+
+ mirror = QtCore.QObject()
+ mirror.fig = Figure((5.0, 3.0), dpi=80, facecolor="white")
+ mirror.canvas = FigureCanvas(mirror.fig)
+ mirror.axes = mirror.fig.add_subplot(111)
+ layout.addWidget(mirror.canvas, 1)
+
+ cosine = MatchmsCosineGreedy(tolerance=0.01)
+ pair_cache = {}
+
+ def _paint():
+ threshold = threshold_spin.value()
+ for i, fid_a in enumerate(feature_ids):
+ for j, fid_b in enumerate(feature_ids):
+ if j < i:
+ continue
+ if fid_a == fid_b:
+ score = 1.0
+ pair_cache[(i, j)] = (repr_scans[fid_a], repr_scans[fid_b], score)
+ else:
+ sp_a = self._to_matchms_spectrum(repr_scans[fid_a])
+ sp_b = self._to_matchms_spectrum(repr_scans[fid_b])
+ if sp_a is None or sp_b is None:
+ score = 0.0
+ else:
+ try:
+ score = float(cosine.pair(sp_a, sp_b).get("score", 0.0))
+ except Exception:
+ score = 0.0
+ pair_cache[(i, j)] = (repr_scans[fid_a], repr_scans[fid_b], score)
+ pair_cache[(j, i)] = pair_cache[(i, j)]
+
+ item = QtWidgets.QTableWidgetItem(f"{score:.3f}")
+ item.setTextAlignment(QtCore.Qt.AlignCenter)
+ red = int(255 * (1.0 - score))
+ green = int(255 * score)
+ item.setBackground(QtGui.QColor(red, green, 80))
+ if score >= threshold:
+ item.setForeground(QtGui.QBrush(QtGui.QColor("black")))
+ else:
+ item.setForeground(QtGui.QBrush(QtGui.QColor("white")))
+ matrix.setItem(i, j, item)
+ if i != j:
+ sym_item = QtWidgets.QTableWidgetItem(f"{score:.3f}")
+ sym_item.setTextAlignment(QtCore.Qt.AlignCenter)
+ sym_item.setBackground(QtGui.QColor(red, green, 80))
+ if score >= threshold:
+ sym_item.setForeground(QtGui.QBrush(QtGui.QColor("black")))
+ else:
+ sym_item.setForeground(QtGui.QBrush(QtGui.QColor("white")))
+ matrix.setItem(j, i, sym_item)
+ matrix.resizeColumnsToContents()
+
+ def _show_selected_pair():
+ idxs = matrix.selectedIndexes()
+ if len(idxs) == 0:
+ return
+ i, j = idxs[0].row(), idxs[0].column()
+ if (i, j) not in pair_cache:
+ return
+ scan_a, scan_b, score = pair_cache[(i, j)]
+ mirror.fig.clear()
+ ax = mirror.fig.add_subplot(111)
+ ax.vlines(scan_a.mz_list, 0, scan_a.intensity_list, colors="dodgerblue", linewidth=1.2)
+ ax.vlines(scan_b.mz_list, 0, -np.asarray(scan_b.intensity_list), colors="firebrick", linewidth=1.2)
+ ax.axhline(0, color="black", linewidth=0.8)
+ ax.set_xlabel("m/z")
+ ax.set_ylabel("Intensity (mirror)")
+ ax.set_title(f"Num {feature_ids[i]} vs Num {feature_ids[j]} | similarity={score:.3f}")
+ ax.grid(True, alpha=0.2)
+ mirror.fig.tight_layout()
+ mirror.canvas.draw()
+
+ threshold_spin.valueChanged.connect(_paint)
+ matrix.itemSelectionChanged.connect(_show_selected_pair)
+ _paint()
+ if matrix.rowCount() > 0 and matrix.columnCount() > 0:
+ matrix.setCurrentCell(0, 0)
+ _show_selected_pair()
+
+ dlg.exec()
+
+ def _copy_msms_spectrum(self, scan, fmt):
+ if scan is None:
+ return
+ lines = []
+ if fmt == "list":
+ lines = [f"{float(mz):.6f} {float(it):.6f}" for mz, it in zip(scan.mz_list, scan.intensity_list)]
+ elif fmt == "tsv":
+ lines = ["mz\tintensity"] + [f"{float(mz):.6f}\t{float(it):.6f}" for mz, it in zip(scan.mz_list, scan.intensity_list)]
+ else: # massbank-like
+ lines = [f"{float(mz):.6f}\t{float(it):.6f}" for mz, it in zip(scan.mz_list, scan.intensity_list)]
+ pyperclip.copy("\n".join(lines))
+
+ def _show_msms_context_menu(self, table_widget, pos):
+ item = table_widget.itemAt(pos)
+ if item is None:
+ return
+ row = item.row()
+ col0 = table_widget.item(row, 0)
+ if col0 is None:
+ return
+ scan = col0.data(QtCore.Qt.UserRole)
+ menu = QtWidgets.QMenu(table_widget)
+ act_list = menu.addAction("Copy spectrum (m/z intensity list)")
+ act_tsv = menu.addAction("Copy spectrum (TSV)")
+ act_mb = menu.addAction("Copy spectrum (MassBank style)")
+ sel = menu.exec(table_widget.mapToGlobal(pos))
+ if sel == act_list:
+ self._copy_msms_spectrum(scan, "list")
+ elif sel == act_tsv:
+ self._copy_msms_spectrum(scan, "tsv")
+ elif sel == act_mb:
+ self._copy_msms_spectrum(scan, "massbank")
+
+ def _export_msms_mgf(self):
+ rows = list(self._iter_exp_msms_rows())
+ if len(rows) == 0:
+ QtWidgets.QMessageBox.information(self, "MS/MS export", "No MS/MS spectra available for export.")
+ return
+
+ dlg = QtWidgets.QDialog(self)
+ dlg.setWindowTitle("Export MS/MS to MGF")
+ form = QtWidgets.QVBoxLayout(dlg)
+ grid = QtWidgets.QFormLayout()
+ mode = QtWidgets.QComboBox()
+ mode.addItems(["Raw spectra", "Average spectrum per feature", "Most abundant spectrum per feature", "Cleaned spectrum per feature"])
+ grid.addRow("Export mode:", mode)
+ include_native = QtWidgets.QCheckBox("Export native spectra")
+ include_native.setChecked(True)
+ include_labeled = QtWidgets.QCheckBox("Export labeled spectra")
+ include_labeled.setChecked(True)
+ grid.addRow(include_native)
+ grid.addRow(include_labeled)
+ allow_zero_label = QtWidgets.QCheckBox("Cleaning: allow 0 labeling atoms")
+ allow_zero_label.setChecked(True)
+ grid.addRow(allow_zero_label)
+ separate_collision = QtWidgets.QCheckBox("Create separate files for each collision setup")
+ grid.addRow(separate_collision)
+ collision_list = QtWidgets.QListWidget()
+ collision_list.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
+ collision_keys = sorted({f"{getattr(r['scan'], 'filter_line', '')}|CE={getattr(r['scan'], 'collisionEnergy', '')}" for r in rows})
+ for ck in collision_keys:
+ it = QtWidgets.QListWidgetItem(ck)
+ it.setFlags(it.flags() | QtCore.Qt.ItemIsUserCheckable)
+ it.setCheckState(QtCore.Qt.Checked)
+ collision_list.addItem(it)
+ grid.addRow("Collision setups:", collision_list)
+ form.addLayout(grid)
+ btns = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
+ form.addWidget(btns)
+ btns.accepted.connect(dlg.accept)
+ btns.rejected.connect(dlg.reject)
+ if dlg.exec() != QtWidgets.QDialog.Accepted:
+ return
+
+ forms = set()
+ if include_native.isChecked():
+ forms.add("native")
+ if include_labeled.isChecked():
+ forms.add("labeled")
+ if len(forms) == 0:
+ QtWidgets.QMessageBox.warning(self, "MS/MS export", "Select at least one isotopolog form.")
+ return
+
+ save_path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save MS/MS MGF", "msms_export.mgf", "MGF file (*.mgf)")
+ if save_path == "":
+ return
+
+ selected_collision = {collision_list.item(i).text() for i in range(collision_list.count()) if collision_list.item(i).checkState() == QtCore.Qt.Checked}
+ selected = [r for r in rows if r["form"] in forms and f"{getattr(r['scan'], 'filter_line', '')}|CE={getattr(r['scan'], 'collisionEnergy', '')}" in selected_collision]
+ if len(selected) == 0:
+ QtWidgets.QMessageBox.information(self, "MS/MS export", "No spectra match the current export selection.")
+ return
+
+ by_key = defaultdict(list)
+ for r in selected:
+ collision = f"{getattr(r['scan'], 'filter_line', '')}|CE={getattr(r['scan'], 'collisionEnergy', '')}"
+ if separate_collision.isChecked():
+ by_key[(r["form"], collision)].append(r)
+ else:
+ by_key[(r["form"], "all")].append(r)
+
+ isotope_mass_offset = getattr(self, "_cached_isotope_mass_offset", None)
+ if isotope_mass_offset is None:
+ isotope_mass_offset = abs(getIsotopeMass(str(self.ui.isotopeBText.text())) - getIsotopeMass(str(self.ui.isotopeAText.text())))
+ self._cached_isotope_mass_offset = isotope_mass_offset
+
+ def _clean_fragextract_like(native_scan, labeled_scan, xn):
+ if native_scan is None or labeled_scan is None:
+ return native_scan, labeled_scan
+ min_atoms = 0 if allow_zero_label.isChecked() else 1
+ max_atoms = int(xn) if xn is not None else 12
+ max_atoms = max(min_atoms, max_atoms)
+ best_n = min_atoms
+ best_score = -1
+ charge = max(1, int(getattr(native_scan, "precursorCharge", 1) or 1))
+ n_mz = np.asarray(native_scan.mz_list, dtype=float)
+ l_mz = np.asarray(labeled_scan.mz_list, dtype=float)
+ n_it = np.asarray(native_scan.intensity_list, dtype=float)
+ l_it = np.asarray(labeled_scan.intensity_list, dtype=float)
+ for n in range(min_atoms, max_atoms + 1):
+ shift = n * isotope_mass_offset / charge
+ score = 0.0
+ for mz, inten in zip(n_mz, n_it):
+ if np.any(np.abs((l_mz - mz) - shift) <= 0.01):
+ score += float(inten)
+ if score > best_score:
+ best_score = score
+ best_n = n
+ shift = best_n * isotope_mass_offset / charge
+ keep_n = []
+ keep_l = []
+ for i, mz in enumerate(n_mz):
+ if np.any(np.abs((l_mz - mz) - shift) <= 0.01):
+ keep_n.append(i)
+ for i, mz in enumerate(l_mz):
+ if np.any(np.abs((mz - n_mz) - shift) <= 0.01):
+ keep_l.append(i)
+ cn = deepcopy(native_scan)
+ cl = deepcopy(labeled_scan)
+ if keep_n:
+ cn.mz_list = n_mz[keep_n]
+ cn.intensity_list = n_it[keep_n]
+ if keep_l:
+ cl.mz_list = l_mz[keep_l]
+ cl.intensity_list = l_it[keep_l]
+ return cn, cl
+
+ def _representative_spectrum(entries):
+ if mode.currentText() == "Most abundant spectrum per feature":
+ return max(entries, key=lambda x: float(getattr(x["scan"], "precursor_intensity", 0.0)))["scan"]
+ if mode.currentText() == "Raw spectra":
+ return None
+ bins = defaultdict(float)
+ for e in entries:
+ scan = e["scan"]
+ for mz, inten in zip(scan.mz_list, scan.intensity_list):
+ mz_bin = round(float(mz), 3)
+ bins[mz_bin] += float(inten)
+ if len(bins) == 0:
+ return entries[0]["scan"]
+ mzs = sorted(bins.keys())
+ ints = [bins[mz] / max(1, len(entries)) for mz in mzs]
+ rep = deepcopy(entries[0]["scan"])
+ rep.mz_list = np.asarray(mzs, dtype=float)
+ rep.intensity_list = np.asarray(ints, dtype=float)
+ return rep
+
+ written_files = []
+ for (form_key, collision_key), vals in by_key.items():
+ out_path = save_path
+ if len(by_key) > 1:
+ suffix = f"_{form_key}_{sanitize_filename(collision_key)}"
+ out_path = save_path.replace(".mgf", f"{suffix}.mgf")
+ with open(out_path, "w", encoding="utf-8") as out:
+ if mode.currentText() == "Raw spectra":
+ export_entries = [[v] for v in vals]
+ else:
+ by_feature = defaultdict(list)
+ for v in vals:
+ by_feature[v["feature_num"]].append(v)
+ export_entries = [fe for fe in by_feature.values()]
+ for entries in export_entries:
+ if len(entries) == 0:
+ continue
+ feature_num = entries[0]["feature_num"]
+ o_group = entries[0]["o_group"]
+ scan = entries[0]["scan"] if mode.currentText() == "Raw spectra" else _representative_spectrum(entries)
+ if mode.currentText() == "Cleaned spectrum per feature":
+ native_entries = [x for x in selected if x["feature_num"] == feature_num and x["form"] == "native"]
+ labeled_entries = [x for x in selected if x["feature_num"] == feature_num and x["form"] == "labeled"]
+ n_scan = _representative_spectrum(native_entries) if native_entries else None
+ l_scan = _representative_spectrum(labeled_entries) if labeled_entries else None
+ n_scan, l_scan = _clean_fragextract_like(n_scan, l_scan, entries[0].get("xn"))
+ if form_key == "native" and n_scan is not None:
+ scan = n_scan
+ elif form_key == "labeled" and l_scan is not None:
+ scan = l_scan
+ out.write("BEGIN IONS\n")
+ out.write(f"TITLE=Num_{feature_num}_OGROUP_{o_group}_{form_key}\n")
+ out.write(f"PEPMASS={float(getattr(scan, 'precursor_mz', 0.0)):.6f}\n")
+ out.write(f"RTINSECONDS={float(getattr(scan, 'retention_time', 0.0)):.3f}\n")
+ out.write(f"FEATURE_NUM={feature_num}\n")
+ out.write(f"OGROUP={o_group}\n")
+ out.write(f"FORM={form_key}\n")
+ out.write(f"FILTER_LINE={getattr(scan, 'filter_line', '')}\n")
+ out.write(f"COLLISION_ENERGY={getattr(scan, 'collisionEnergy', '')}\n")
+ for mz, inten in zip(scan.mz_list, scan.intensity_list):
+ out.write(f"{float(mz):.6f} {float(inten):.6f}\n")
+ out.write("END IONS\n\n")
+ written_files.append(out_path)
+
+ QtWidgets.QMessageBox.information(self, "MS/MS export", "Exported:\n" + "\n".join(written_files))
+
+ def _show_msms_overview(self):
+ rows = list(self._iter_exp_msms_rows())
+ by_feature = defaultdict(lambda: {"native": 0, "labeled": 0})
+ for r in rows:
+ if r["feature_num"] is None:
+ continue
+ by_feature[r["feature_num"]][r["form"]] += 1
+ if len(by_feature) == 0:
+ QtWidgets.QMessageBox.information(self, "MS/MS overview", "No feature-linked MS/MS spectra available.")
+ return
+
+ dlg = QtWidgets.QDialog(self)
+ dlg.setWindowTitle("MS/MS overview")
+ dlg.resize(640, 420)
+ layout = QtWidgets.QVBoxLayout(dlg)
+ table = QtWidgets.QTableWidget()
+ table.setColumnCount(4)
+ table.setHorizontalHeaderLabels(["Feature Num", "Native scans", "Labeled scans", "Total"])
+ table.setRowCount(len(by_feature))
+ for i, fid in enumerate(sorted(by_feature.keys())):
+ n = by_feature[fid]["native"]
+ l = by_feature[fid]["labeled"]
+ table.setItem(i, 0, QtWidgets.QTableWidgetItem(str(fid)))
+ table.setItem(i, 1, QtWidgets.QTableWidgetItem(str(n)))
+ table.setItem(i, 2, QtWidgets.QTableWidgetItem(str(l)))
+ table.setItem(i, 3, QtWidgets.QTableWidgetItem(str(n + l)))
+ table.resizeColumnsToContents()
+ layout.addWidget(table)
+ fig = Figure((5.0, 2.5), dpi=80, facecolor="white")
+ canvas = FigureCanvas(fig)
+ ax = fig.add_subplot(111)
+ fids = sorted(by_feature.keys())
+ native_counts = [by_feature[f]["native"] for f in fids]
+ labeled_counts = [by_feature[f]["labeled"] for f in fids]
+ x = np.arange(len(fids))
+ ax.bar(x - 0.2, native_counts, width=0.4, color="dodgerblue", label="Native")
+ ax.bar(x + 0.2, labeled_counts, width=0.4, color="firebrick", label="Labeled")
+ ax.set_xticks(x)
+ ax.set_xticklabels([str(f) for f in fids], rotation=90)
+ ax.set_xlabel("Feature Num")
+ ax.set_ylabel("MS/MS scan count")
+ ax.legend(loc="upper right")
+ fig.tight_layout()
+ layout.addWidget(canvas)
+ dlg.exec()
+
#
#
@@ -9652,11 +11348,55 @@ def resultsExperimentChangedNew(self, askForFeature=False):
):
availableFilterLines.add(fl)
+ # --- Load saved custom features from JSON ---
+ import json as _json
+
+ _custom_features_path = os.path.join(local_folder, "custom_features.json")
+
+ def _load_saved_features():
+ try:
+ if os.path.exists(_custom_features_path):
+ with open(_custom_features_path, "r", encoding="utf-8") as _f:
+ return _json.load(_f)
+ except Exception:
+ pass
+ return []
+
+ def _save_features(features):
+ try:
+ with open(_custom_features_path, "w", encoding="utf-8") as _f:
+ _json.dump(features, _f, indent=2)
+ except Exception as ex:
+ logging.warning(f"Could not save custom features: {ex}")
+
+ saved_features = _load_saved_features()
+
dlg = QtWidgets.QDialog(self)
dlg.setWindowTitle("Custom feature")
- dlg.setMinimumWidth(400)
+ dlg.setMinimumWidth(450)
form = QtWidgets.QFormLayout(dlg)
+ # --- Saved compounds section ---
+ saved_layout = QtWidgets.QHBoxLayout()
+ saved_combo = QtWidgets.QComboBox()
+ saved_combo.setMinimumWidth(200)
+ saved_combo.addItem("-- select saved compound --")
+ for sf in saved_features:
+ saved_combo.addItem(sf.get("name", ""))
+ saved_layout.addWidget(saved_combo)
+ del_saved_btn = QtWidgets.QPushButton("Delete")
+ saved_layout.addWidget(del_saved_btn)
+ form.addRow("Saved compounds:", saved_layout)
+
+ sep0 = QtWidgets.QFrame()
+ sep0.setFrameShape(QtWidgets.QFrame.HLine)
+ form.addRow(sep0)
+
+ # --- Name field ---
+ name_edit = QtWidgets.QLineEdit()
+ name_edit.setPlaceholderText("Optional – set to save this feature")
+ form.addRow("Name:", name_edit)
+
# --- Sum formula helper ---
formula_layout = QtWidgets.QHBoxLayout()
native_formula_edit = QtWidgets.QLineEdit()
@@ -9711,7 +11451,6 @@ def _calc_mz_from_formula():
except Exception as ex:
QtWidgets.QMessageBox.warning(dlg, "Formula error", str(ex))
- # calcuate on button click and on change of formulas/adducts
native_formula_edit.textEdited.connect(_calc_mz_from_formula)
labeled_formula_edit.textEdited.connect(_calc_mz_from_formula)
adduct_combo.currentIndexChanged.connect(_calc_mz_from_formula)
@@ -9727,6 +11466,37 @@ def _calc_mz_from_formula():
fl_combo.addItems(sorted(availableFilterLines))
form.addRow("Filter line:", fl_combo)
+ def _populate_from_saved(index):
+ """Fill form fields when a saved compound is selected."""
+ if index <= 0 or index - 1 >= len(saved_features):
+ return
+ sf = saved_features[index - 1]
+ name_edit.setText(sf.get("name", ""))
+ native_formula_edit.setText(sf.get("native_formula", ""))
+ labeled_formula_edit.setText(sf.get("labeled_formula", ""))
+ mz_spin.setValue(sf.get("mz", 0.0))
+ lmz_spin.setValue(sf.get("lmz", 0.0))
+ rt_spin.setValue(sf.get("rt", 0.0))
+ fl_val = sf.get("filter_line", "")
+ idx_fl = fl_combo.findText(fl_val)
+ if idx_fl >= 0:
+ fl_combo.setCurrentIndex(idx_fl)
+ adduct_name = sf.get("adduct", "")
+ idx_ad = adduct_combo.findText(adduct_name)
+ if idx_ad >= 0:
+ adduct_combo.setCurrentIndex(idx_ad)
+
+ def _delete_saved():
+ idx = saved_combo.currentIndex()
+ if idx <= 0 or idx - 1 >= len(saved_features):
+ return
+ saved_features.pop(idx - 1)
+ _save_features(saved_features)
+ saved_combo.removeItem(idx)
+
+ saved_combo.currentIndexChanged.connect(_populate_from_saved)
+ del_saved_btn.clicked.connect(_delete_saved)
+
btn_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
btn_box.accepted.connect(dlg.accept)
btn_box.rejected.connect(dlg.reject)
@@ -9740,6 +11510,27 @@ def _calc_mz_from_formula():
rt = rt_spin.value()
fl = fl_combo.currentText()
+ # Save if a name was provided
+ compound_name = name_edit.text().strip()
+ if compound_name:
+ # Update existing entry or append new one
+ existing = next((sf for sf in saved_features if sf.get("name") == compound_name), None)
+ entry = {
+ "name": compound_name,
+ "native_formula": native_formula_edit.text().strip(),
+ "labeled_formula": labeled_formula_edit.text().strip(),
+ "adduct": adduct_combo.currentText(),
+ "mz": mz,
+ "lmz": lmz,
+ "rt": rt,
+ "filter_line": fl,
+ }
+ if existing is not None:
+ existing.update(entry)
+ else:
+ saved_features.append(entry)
+ _save_features(saved_features)
+
plotItems.append(Bunch(mz=mz, lmz=lmz, rt=rt * 60.0, scanEvent=fl))
else:
for item in self.ui.resultsExperiment_TreeWidget.selectedItems():
@@ -10099,7 +11890,7 @@ def _calc_mz_from_formula():
mean(meanRT) / 60.0 + borderOffset,
]
intlim = [intlim[0] * 1.1, intlim[1] * 1.1]
- self.drawCanvas(self.ui.resultsExperiment_plot, xlim=rtlim, ylim=intlim)
+ self.drawCanvas(self.ui.resultsExperiment_plot, xlim=rtlim, ylim=intlim, showLegendOverwrite=False)
self.drawCanvas(self.ui.resultsExperimentSeparatedPeaks_plot, showLegendOverwrite=self.ui.showLegend_experiment.isChecked())
self.drawCanvas(
self.ui.resultsExperimentMSScanPeaks_plot,
@@ -10442,23 +12233,24 @@ def loadAvailableSettingsFile(self, file):
pass
def addDB(self, events):
- dbFile, _filter = QtWidgets.QFileDialog.getOpenFileName(
- caption="Select database file",
+ dbFiles, _filter = QtWidgets.QFileDialog.getOpenFileNames(
+ caption="Select database file(s)",
dir=self.lastOpenDir,
filter="Database (*.xlsx *.tsv);;Excel files (*.xlsx);;TSV files (*.tsv);;All files (*.*)",
)
- dbFile = str(dbFile)
- if len(dbFile) > 0:
- self.lastOpenDir = str(dbFile).replace("\\", "/")
- self.lastOpenDir = self.lastOpenDir[: self.lastOpenDir.rfind("/")]
+ for dbFile in dbFiles:
+ dbFile = str(dbFile)
+ if len(dbFile) > 0:
+ self.lastOpenDir = dbFile.replace("\\", "/")
+ self.lastOpenDir = self.lastOpenDir[: self.lastOpenDir.rfind("/")]
- dbFile = dbFile.replace("\\", "/")
- dbName = dbFile[dbFile.rfind("/") + 1 : dbFile.rfind(".")]
+ dbFile = dbFile.replace("\\", "/")
+ dbName = dbFile[dbFile.rfind("/") + 1 : dbFile.rfind(".")]
- item = QtGui.QStandardItem("%s (Database)" % dbName)
- item.setData(dbFile)
- self.ui.dbList_listView.model().appendRow(item)
+ item = QtGui.QStandardItem("%s (Database)" % dbName)
+ item.setData(dbFile)
+ self.ui.dbList_listView.model().appendRow(item)
def addMZVaultRepository(self, events):
dbFile = QtWidgets.QFileDialog.getOpenFileName(
@@ -10547,6 +12339,64 @@ def generateDBTemplate(self, events):
QtWidgets.QMessageBox.Ok,
)
+ def testDBs(self, events=None):
+ dbFiles = []
+ for entryInd in range(self.ui.dbList_listView.model().rowCount()):
+ dbFile = str(self.ui.dbList_listView.model().item(entryInd, 0).data())
+ dbFiles.append(dbFile)
+
+ if not dbFiles:
+ QtWidgets.QMessageBox.information(self, "MetExtract", "No database files added.", QtWidgets.QMessageBox.Ok)
+ return
+
+ # Show indeterminate progress dialog while importing
+ progress = QtWidgets.QProgressDialog("Importing database files, please wait…", None, 0, 0, self)
+ progress.setWindowTitle("Test DBs")
+ progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
+ progress.setMinimumDuration(0)
+ progress.setValue(0)
+ progress.show()
+ QtWidgets.QApplication.processEvents()
+
+ worker = _DBTestWorker(dbFiles, parent=self)
+
+ def _on_finished(results):
+ progress.close()
+ self._showDBTestResults(results)
+
+ worker.finished.connect(_on_finished)
+ worker.start()
+
+ def _showDBTestResults(self, results):
+ dialog = QtWidgets.QDialog(self)
+ dialog.setWindowTitle("Database Import Test Results")
+ dialog.resize(750, 450)
+ layout = QtWidgets.QVBoxLayout(dialog)
+
+ tree = QtWidgets.QTreeWidget(dialog)
+ tree.setHeaderLabels(["Database / Message"])
+ tree.setColumnCount(1)
+ tree.header().setStretchLastSection(True)
+
+ for result in results:
+ has_errors = result["not_imported"] > 0 or len(result["errors"]) > 0
+ status = "Issues found" if has_errors else "OK"
+ top_item = QtWidgets.QTreeWidgetItem(
+ tree,
+ [f"{result['db_name']} — Imported: {result['imported']}, Errors: {result['not_imported']} [{status}]"],
+ )
+ if not has_errors:
+ QtWidgets.QTreeWidgetItem(top_item, [f"Successfully imported {result['imported']} entries"])
+ for err in result["errors"]:
+ QtWidgets.QTreeWidgetItem(top_item, [err])
+ top_item.setExpanded(has_errors)
+
+ layout.addWidget(tree)
+ btn_close = QtWidgets.QPushButton("Close")
+ btn_close.clicked.connect(dialog.accept)
+ layout.addWidget(btn_close)
+ dialog.exec_()
+
# initialise main interface, triggers and command line parameters
def __init__(self, module="TracExtract", parent=None, silent=False, disableR=False):
super(Ui_MainWindow, self).__init__()
@@ -10974,6 +12824,7 @@ def __init__(self, module="TracExtract", parent=None, silent=False, disableR=Fal
self.ui.saveGroups.clicked.connect(self.saveGroupsClicked)
self.ui.loadGroups.clicked.connect(self.loadGroupsClicked)
self.ui.removeGroup.clicked.connect(self.remGrp)
+ self.ui.showFileStats.clicked.connect(self.showFileStatsPopup)
self.ui.groupsList.doubleClicked.connect(self.editGroup)
self.ui.groupsList.itemChanged.connect(self._onGroupTableItemChanged)
@@ -11012,6 +12863,11 @@ def __init__(self, module="TracExtract", parent=None, silent=False, disableR=Fal
self.ui.resultsExperimentNormaliseXICs_checkBox.stateChanged.connect(self._refreshExperimentEICs)
self.ui.resultsExperimentNormaliseXICsSeparately_checkBox.stateChanged.connect(self._refreshExperimentEICs)
self.ui.showLegend_experiment.stateChanged.connect(self._refreshExperimentEICs)
+ self.ui.comboBox_abundancePlotType.currentIndexChanged.connect(self._refreshExperimentAbundancePlot)
+ self.ui.comboBox_abundanceScale.currentIndexChanged.connect(self._refreshExperimentAbundancePlot)
+ self.ui.comboBox_abundanceScalingMode.currentIndexChanged.connect(self._refreshExperimentAbundancePlot)
+ self.ui.pushButton_samplePeaksPrev.clicked.connect(self._samplePeaksPrevPage)
+ self.ui.pushButton_samplePeaksNext.clicked.connect(self._samplePeaksNextPage)
self.ui.eicSmoothingWindow.currentIndexChanged.connect(self.smoothingWindowChanged)
self.smoothingWindowChanged()
@@ -11045,6 +12901,8 @@ def __init__(self, module="TracExtract", parent=None, silent=False, disableR=Fal
self.ui.addmzVaultRep_pushButton.clicked.connect(self.addMZVaultRepository)
self.ui.removeDB_pushButton.clicked.connect(self.removeDB)
self.ui.generateDBTemplate_pushButton.clicked.connect(self.generateDBTemplate)
+ self.ui.testDBs_pushButton.clicked.connect(self.testDBs)
+ self.ui.actionDownloadDBTemplate.triggered.connect(self.generateDBTemplate)
# setup result plots
# Setup first plot
@@ -11221,6 +13079,41 @@ def __init__(self, module="TracExtract", parent=None, silent=False, disableR=Fal
vbox.addWidget(self.ui.resultsExperimentMSScanPeaks_plot.canvas)
self.ui.resultsExperimentMSScan_widget.setLayout(vbox)
+ # Setup experiment abundance plot
+ self.ui.resultsExperimentAbundance_plot = QtCore.QObject()
+ self.ui.resultsExperimentAbundance_plot.dpi = 50
+ self.ui.resultsExperimentAbundance_plot.fig = Figure((5.0, 4.0), dpi=self.ui.resultsExperimentAbundance_plot.dpi, facecolor="white")
+ self.ui.resultsExperimentAbundance_plot.fig.subplots_adjust(left=0.08, bottom=0.15, right=0.99, top=0.95)
+ self.ui.resultsExperimentAbundance_plot.canvas = FigureCanvas(self.ui.resultsExperimentAbundance_plot.fig)
+ self.ui.resultsExperimentAbundance_plot.canvas.setParent(self.ui.resultsExperimentAbundance_widget)
+ self.ui.resultsExperimentAbundance_plot.axes = self.ui.resultsExperimentAbundance_plot.fig.add_subplot(111)
+ simpleaxis(self.ui.resultsExperimentAbundance_plot.axes)
+ self.ui.resultsExperimentAbundance_plot.twinxs = [self.ui.resultsExperimentAbundance_plot.axes]
+ self.ui.resultsExperimentAbundance_plot.mpl_toolbar = NavigationToolbar(
+ self.ui.resultsExperimentAbundance_plot.canvas,
+ self.ui.resultsExperimentAbundance_widget,
+ )
+
+ vbox = QtWidgets.QVBoxLayout()
+ vbox.addWidget(self.ui.resultsExperimentAbundance_plot.mpl_toolbar)
+ vbox.addWidget(self.ui.resultsExperimentAbundance_plot.canvas)
+ self.ui.resultsExperimentAbundance_widget.setLayout(vbox)
+
+ # Setup sample peaks plot (dynamic per-sample subplot grid)
+ self.ui.resultsExperimentSamplePeaks_plot = QtCore.QObject()
+ self.ui.resultsExperimentSamplePeaks_plot.dpi = 72
+ self.ui.resultsExperimentSamplePeaks_plot.fig = Figure(facecolor="white")
+ self.ui.resultsExperimentSamplePeaks_plot.canvas = FigureCanvas(self.ui.resultsExperimentSamplePeaks_plot.fig)
+ self.ui.resultsExperimentSamplePeaks_plot.canvas.setParent(self.ui.resultsExperimentSamplePeaks_widget)
+ self.ui.resultsExperimentSamplePeaks_plot.mpl_toolbar = NavigationToolbar(
+ self.ui.resultsExperimentSamplePeaks_plot.canvas,
+ self.ui.resultsExperimentSamplePeaks_widget,
+ )
+ vbox_sp = QtWidgets.QVBoxLayout()
+ vbox_sp.addWidget(self.ui.resultsExperimentSamplePeaks_plot.mpl_toolbar)
+ vbox_sp.addWidget(self.ui.resultsExperimentSamplePeaks_plot.canvas)
+ self.ui.resultsExperimentSamplePeaks_widget.setLayout(vbox_sp)
+
# Setup experiment MSMS plot - multiple MS/MS spectra subplots
self.ui.plMSMS_exp = QtCore.QObject()
self.ui.plMSMS_exp.dpi = 50
@@ -11291,6 +13184,27 @@ def __init__(self, module="TracExtract", parent=None, silent=False, disableR=Fal
# resultsExperimentChangedNew is used only by showCustomFeature (loads raw mzXML);
# normal tree selection uses resultsExperimentChanged (reads pre-computed per-file DBs)
self.ui.msms_SpectraList_exp.itemSelectionChanged.connect(self.plotSelectedMSMSSpectra_exp)
+ self.ui.msms_SpectraList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.ui.msms_SpectraList_exp.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.ui.msms_SpectraList.customContextMenuRequested.connect(lambda pos: self._show_msms_context_menu(self.ui.msms_SpectraList, pos))
+ self.ui.msms_SpectraList_exp.customContextMenuRequested.connect(lambda pos: self._show_msms_context_menu(self.ui.msms_SpectraList_exp, pos))
+
+ # Additional MS/MS controls in experiment-results tab
+ self.ui.msms_controls_exp = QtWidgets.QHBoxLayout()
+ self.ui.btn_msms_similarity_native = QtWidgets.QPushButton("Native similarity")
+ self.ui.btn_msms_similarity_labeled = QtWidgets.QPushButton("Labeled similarity")
+ self.ui.btn_msms_overview = QtWidgets.QPushButton("MS/MS overview")
+ self.ui.btn_msms_export_mgf = QtWidgets.QPushButton("Export MGF")
+ self.ui.msms_controls_exp.addWidget(self.ui.btn_msms_similarity_native)
+ self.ui.msms_controls_exp.addWidget(self.ui.btn_msms_similarity_labeled)
+ self.ui.msms_controls_exp.addWidget(self.ui.btn_msms_overview)
+ self.ui.msms_controls_exp.addWidget(self.ui.btn_msms_export_mgf)
+ self.ui.msms_controls_exp.addStretch(1)
+ self.ui.verticalLayout_msms_exp.insertLayout(1, self.ui.msms_controls_exp)
+ self.ui.btn_msms_similarity_native.clicked.connect(lambda: self._show_msms_similarity_dialog("native"))
+ self.ui.btn_msms_similarity_labeled.clicked.connect(lambda: self._show_msms_similarity_dialog("labeled"))
+ self.ui.btn_msms_overview.clicked.connect(self._show_msms_overview)
+ self.ui.btn_msms_export_mgf.clicked.connect(self._export_msms_mgf)
self.ui.showCustomFeature_pushButton.clicked.connect(self.showCustomFeature)
@@ -11421,7 +13335,9 @@ def main():
+ "
"
+ f"If importing mzML files results in the error
of missing files, please find the correct version at
{OBO_DOWNLOAD_URL}.
"
+ "Please download the corresponding obo-file and
save it to the folder in the error message.
"
- + "You can also open this page via the menu
('Tools'->'Download OBO files').",
+ + "You can also open this page via the menu
('Tools'->'Download OBO files').
"
+ + "
"
+ + "To generate a template for a database, select
'Download Database Template' from the 'Tools' menu.",
QtWidgets.QMessageBox.Ok,
)
diff --git a/src/annotateResultMatrix.py b/src/annotateResultMatrix.py
index 104ec0f..6bd286f 100644
--- a/src/annotateResultMatrix.py
+++ b/src/annotateResultMatrix.py
@@ -153,6 +153,7 @@ def annotateWithDatabases(
processedElement,
pwMaxSet=None,
pwValSet=None,
+ db_info_messages=None,
):
"""
Annotate metabolites by searching in databases using PolarsDB.
@@ -167,7 +168,7 @@ def annotateWithDatabases(
Args:
file: Path to the results file (PolarsDB format)
- sheet_name: Name of the sheet to read from (e.g., "4_Reintegrated")
+ sheet_name: Name of the sheet to read from (e.g., "3_Reintegrated")
new_sheet_name: Name of the sheet to write to (e.g., "6_Annotated")
dbFiles: List of database file paths
useAdducts: List of adduct definitions [[name, mzoffset, polarity, charge, mCount], ...]
@@ -180,6 +181,8 @@ def annotateWithDatabases(
processedElement: Element to check in formulas (e.g., "C")
pwMaxSet: Progress callback for max value
pwValSet: Progress callback for current value
+ db_info_messages: Optional list; if provided, import summary messages are appended to it
+ (used to write the DB_info log sheet)
Returns:
List of annotation column names added
@@ -203,21 +206,31 @@ def annotateWithDatabases(
dbNames = []
# Import database files and collect database names
- logging.info(f"Importing {len(dbFiles)} database file(s)")
+ logging.info(f"\n\n#########################################\nImporting {len(dbFiles)} database file(s)")
for dbFile in dbFiles:
+ logging.info(f"\n-------------------------\nImporting database file: {dbFile}")
dbName = dbFile[dbFile.rfind("/") + 1 : dbFile.rfind(".")]
+ dbNames.append(dbName)
+ errors = []
try:
- imported, notImported = db.addEntriesFromFile(dbName, dbFile)
- if notImported > 0:
- logging.warning(f"Warning: {notImported} entries from database '{dbName}' were not imported successfully")
- dbNames.append(dbName)
- logging.info(f" Imported {imported} entries from database '{dbName}'")
+ imported, not_imported = db.addEntriesFromFile(dbName, dbFile, error_collector=errors)
+ if db_info_messages is not None:
+ db_info_messages.append(f"Database: {dbName} (file: {dbFile})")
+ db_info_messages.append(f" Imported: {imported} entries successfully")
+ if not_imported > 0:
+ db_info_messages.append(f" Not imported: {not_imported} entries due to errors")
+ for err in errors:
+ db_info_messages.append(f" {err}")
except IOError as e:
- logging.error(f"Cannot open database file '{dbName}' at '{dbFile}': {e}")
+ logging.error(f" - Cannot process database file '{dbName}' at '{dbFile}': {e}")
+ if db_info_messages is not None:
+ db_info_messages.append(f"Database: {dbName} (file: {dbFile})")
+ db_info_messages.append(f" Fatal error: {e}")
continue
+ logging.info(f"\nFinished importing databases. Total imported entries: MZ: {len(db.dbEntriesMZ)}, Neutral: {len(db.dbEntriesNeutral)}")
# Optimize database for searching
- logging.info("Optimizing database for searching")
+ logging.info("\nOptimizing database for searching")
db.optimizeDB()
# Add all necessary annotation columns BEFORE searching
@@ -363,7 +376,7 @@ def searchForRow(self, row):
if pwMaxSet is not None:
pwMaxSet(total_rows)
- logging.info(f"Searching database hits for {total_rows} metabolites")
+ logging.info(f"Searching database hits for {total_rows} metabolites, parameters are ppm: {ppm}, correctppmPosMode: {correctppmPosMode}, correctppmNegMode: {correctppmNegMode}, rtError: {rtError}, useRt: {useRt}, checkXnInHits: {checkXnInHits}, processedElement: {processedElement}")
# Collect all hits for compound-focused sheet
all_compound_hits = []
@@ -378,6 +391,8 @@ def searchForRow(self, row):
# Search for database hits
hits_per_db, hit_objects = searcher.searchForRow(row)
+ print(f"Row {row_idx + 1}/{total_rows}, mz {row.get('MZ')}, rt {row.get('RT')}, charge {row.get('Charge')}, polarity {row.get('Ionisation_Mode')}, Xn {row.get('Xn')}\n - Found hits in {len(hits_per_db)} databases")
+
# Update only if there are hits
if hits_per_db:
for dbName, hit_data in hits_per_db.items():
@@ -483,6 +498,39 @@ def searchForRow(self, row):
return annotationColumns
+def testDatabaseImports(dbFiles):
+ """
+ Test-import database files and return per-db import results without writing to any output file.
+
+ Args:
+ dbFiles: List of database file paths
+
+ Returns:
+ List of dicts: [{'db_name': str, 'db_file': str, 'imported': int, 'not_imported': int, 'errors': list[str]}, ...]
+ """
+ results = []
+ for dbFile in dbFiles:
+ dbName = dbFile[dbFile.rfind("/") + 1 : dbFile.rfind(".")]
+ errors = []
+ db = searchDatabases.DBSearch()
+ try:
+ imported, not_imported = db.addEntriesFromFile(dbName, dbFile, error_collector=errors)
+ except Exception as e:
+ errors.insert(0, f"Fatal error: {e}")
+ imported = 0
+ not_imported = 0
+ results.append(
+ {
+ "db_name": dbName,
+ "db_file": dbFile,
+ "imported": imported,
+ "not_imported": not_imported,
+ "errors": errors,
+ }
+ )
+ return results
+
+
def annotateWithSumFormulas(
file,
sheet_name,
diff --git a/src/bracketResults.py b/src/bracketResults.py
index df23fe6..273e436 100644
--- a/src/bracketResults.py
+++ b/src/bracketResults.py
@@ -1,17 +1,20 @@
from __future__ import absolute_import, division, print_function
import base64
import datetime
+import hashlib
import json
import logging
import os
import statistics
import time
import uuid
+import xml.etree.ElementTree as _ET
from collections import OrderedDict, defaultdict
-from math import isnan
+from math import isnan, log
from multiprocessing import Manager, Pool
from operator import itemgetter
from pickle import loads as pickle_loads
+import numpy as np
import polars as pl
from reportlab.graphics import renderPDF
from reportlab.graphics.charts.lineplots import LinePlot
@@ -22,6 +25,7 @@
from .Chromatogram import Chromatogram
from .MZHCA import HierarchicalClustering, cutTreeSized
from .PolarsDB import PolarsDB
+from .metaboliteGrouping import split_group_by_relative_abundance
from .utils import ChromPeakPair
from .utils import (
Bunch,
@@ -34,7 +38,55 @@
sd,
)
from .XICAlignment import XICAlignment
-from .utils import mapArrayToRefTimes
+from .utils import mapArrayToRefTimes, get_app_folder
+
+
+def log10(x):
+ return log(x, 10)
+
+
+# ---------------------------------------------------------------------------
+# Per-file sample-stats cache helpers
+# ---------------------------------------------------------------------------
+
+
+def _get_stats_cache_dir():
+ cache_dir = os.path.join(get_app_folder(), "sampleStats")
+ os.makedirs(cache_dir, exist_ok=True)
+ return cache_dir
+
+
+def _load_cached_stats(filepath, scan_event_key):
+ """Return cached stats dict for *filepath* or None if missing / stale."""
+ try:
+ fp = os.path.abspath(filepath)
+ key = hashlib.md5(fp.encode("utf-8")).hexdigest()
+ cache_path = os.path.join(_get_stats_cache_dir(), f"{key}.json")
+ if not os.path.exists(cache_path):
+ return None
+ mtime = os.path.getmtime(fp)
+ with open(cache_path, "r", encoding="utf-8") as fh:
+ cached = json.load(fh)
+ if cached.get("_mtime") != mtime:
+ return None
+ if cached.get("_scan_event_key") != scan_event_key:
+ return None
+ return cached.get("data")
+ except Exception:
+ return None
+
+
+def _save_cached_stats(filepath, data, scan_event_key):
+ """Write stats dict to the per-file cache."""
+ try:
+ fp = os.path.abspath(filepath)
+ key = hashlib.md5(fp.encode("utf-8")).hexdigest()
+ cache_path = os.path.join(_get_stats_cache_dir(), f"{key}.json")
+ mtime = os.path.getmtime(fp)
+ with open(cache_path, "w", encoding="utf-8") as fh:
+ json.dump({"_mtime": mtime, "_scan_event_key": scan_event_key, "data": data}, fh)
+ except Exception as e:
+ logging.debug(f"Could not save stats cache for {filepath}: {e}")
# HELPER METHOD for writing first page of PDF (unused)
@@ -117,6 +169,204 @@ def writeConfigToDB(
db.insert_row("config", {"key": "FPBRACK_nPolynom", "value": str(nPolynom)})
+def _get_mzml_metadata(filepath):
+ """Extract startTimeStamp and specific cvParams from an mzML header via streaming XML parsing."""
+ result = {"startTimeStamp": None, "MS:1000073": None, "MS:1000079": None}
+ ns = {"mzml": "http://psi.hupo.org/ms/mzml"}
+ try:
+ for event, elem in _ET.iterparse(filepath, events=("start",)):
+ tag = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
+ if tag == "run":
+ result["startTimeStamp"] = elem.get("startTimeStamp")
+ elif tag == "cvParam":
+ acc = elem.get("accession", "")
+ if acc in ("MS:1000073", "MS:1000079"):
+ result[acc] = elem.get("name", acc)
+ # Stop after the first spectrum to avoid reading the whole file
+ elif tag == "spectrum":
+ break
+ except Exception as e:
+ logging.debug(f"Could not parse mzML metadata from {filepath}: {e}")
+ return result
+
+
+def _percentile_stats(diffs):
+ """Return a dict of percentile statistics for a list of float values."""
+ if not diffs:
+ return {k: None for k in ("min", "p10", "p25", "median", "mean", "p75", "p90", "max", "sd")}
+ arr = np.array(diffs, dtype=float)
+ return {
+ "min": float(np.min(arr)),
+ "p10": float(np.percentile(arr, 10)),
+ "p25": float(np.percentile(arr, 25)),
+ "median": float(np.median(arr)),
+ "mean": float(np.mean(arr)),
+ "p75": float(np.percentile(arr, 75)),
+ "p90": float(np.percentile(arr, 90)),
+ "max": float(np.max(arr)),
+ "sd": float(np.std(arr, ddof=1)) if len(arr) > 1 else 0.0,
+ }
+
+
+def _intensity_percentile_stats(intensities):
+ """Return intensity distribution stats (min, p10, p25, median, p75, p90–p99, max)."""
+ _keys = ("min", "p10", "p25", "median", "p75", "p90", "p91", "p92", "p93", "p94", "p95", "p96", "p97", "p98", "p99", "max")
+ if not intensities:
+ return {k: None for k in _keys}
+ arr = np.array(intensities, dtype=float)
+ return {
+ "min": float(np.min(arr)),
+ "p10": float(np.percentile(arr, 10)),
+ "p25": float(np.percentile(arr, 25)),
+ "median": float(np.median(arr)),
+ "p75": float(np.percentile(arr, 75)),
+ "p90": float(np.percentile(arr, 90)),
+ "p91": float(np.percentile(arr, 91)),
+ "p92": float(np.percentile(arr, 92)),
+ "p93": float(np.percentile(arr, 93)),
+ "p94": float(np.percentile(arr, 94)),
+ "p95": float(np.percentile(arr, 95)),
+ "p96": float(np.percentile(arr, 96)),
+ "p97": float(np.percentile(arr, 97)),
+ "p98": float(np.percentile(arr, 98)),
+ "p99": float(np.percentile(arr, 99)),
+ "max": float(np.max(arr)),
+ }
+
+
+def compute_sample_stats(all_files, positiveScanEvent=None, negativeScanEvent=None):
+ """Compute per-file sample statistics for a list of raw LC-MS/MS files.
+
+ Returns a list of dicts, one per file, with keys:
+ file, ms1_pos, ms1_neg, ms2_pos, ms2_neg, last_rt,
+ ms1_timediff_*, ms2_timediff_*,
+ startTimeStamp, MS:1000073, MS:1000079
+ """
+ rows = []
+ for filepath in all_files:
+ filepath = str(filepath).replace("\\", "/")
+ # v3: compute MS1 intensity stats from actual signal data (intensity_list)
+ scan_event_key = f"{positiveScanEvent}|{negativeScanEvent}|v6"
+
+ # Try cache first
+ cached_row = _load_cached_stats(filepath, scan_event_key)
+ if cached_row is not None:
+ rows.append(cached_row)
+ continue
+
+ try:
+ chrom = Chromatogram()
+ chrom.parse_file(filepath, ignoreCharacterData=False)
+ except Exception as e:
+ logging.warning(f"compute_sample_stats: could not parse {filepath}: {e}")
+ rows.append({"file": os.path.basename(filepath)})
+ continue
+
+ pos_se = positiveScanEvent if positiveScanEvent and positiveScanEvent != "None" else None
+ neg_se = negativeScanEvent if negativeScanEvent and negativeScanEvent != "None" else None
+
+ # MS1 scan counts per polarity / selected scan event
+ ms1_pos_scans = [s for s in chrom.MS1_list if s.polarity == "+" and (pos_se is None or s.filter_line == pos_se)]
+ ms1_neg_scans = [s for s in chrom.MS1_list if s.polarity == "-" and (neg_se is None or s.filter_line == neg_se)]
+ ms1_pos = len(ms1_pos_scans)
+ ms1_neg = len(ms1_neg_scans)
+
+ # MS2 scan counts per polarity
+ ms2_pos = sum(1 for s in chrom.MS2_list if s.polarity == "+")
+ ms2_neg = sum(1 for s in chrom.MS2_list if s.polarity == "-")
+
+ # Adjacent MS1 time differences (selected scan events only)
+ ms1_times = sorted(s.retention_time for s in chrom.MS1_list if (pos_se is None or s.filter_line == pos_se or s.polarity == "+") and (neg_se is None or s.filter_line == neg_se or s.polarity == "-"))
+ if pos_se:
+ ms1_times = sorted(s.retention_time for s in chrom.MS1_list if s.filter_line == pos_se or s.filter_line == neg_se)
+ ms1_diffs = [ms1_times[i + 1] - ms1_times[i] for i in range(len(ms1_times) - 1)]
+
+ # Adjacent MS2 time differences
+ ms2_times = sorted(s.retention_time for s in chrom.MS2_list)
+ ms2_diffs = [ms2_times[i + 1] - ms2_times[i] for i in range(len(ms2_times) - 1)]
+
+ # Last scan RT (max of all scans)
+ all_rts = [s.retention_time for s in chrom.MS1_list] + [s.retention_time for s in chrom.MS2_list]
+ last_rt = max(all_rts) / 60.0 if all_rts else None
+
+ ms1_stats = _percentile_stats(ms1_diffs)
+ ms2_stats = _percentile_stats(ms2_diffs)
+
+ # Collect ALL individual signal intensities from the actual peak lists
+ ms1_signalInt_pos = _intensity_percentile_stats([log10(v) for s in ms1_pos_scans for v in s.intensity_list.tolist()])
+ ms1_signalInt_neg = _intensity_percentile_stats([log10(v) for s in ms1_neg_scans for v in s.intensity_list.tolist()])
+
+ # mzML-specific metadata
+ mzml_meta = {"startTimeStamp": None, "MS:1000073": None, "MS:1000079": None}
+ if filepath.lower().endswith(".mzml"):
+ mzml_meta = _get_mzml_metadata(filepath)
+
+ row = {
+ "file": os.path.basename(filepath),
+ "startTimeStamp": mzml_meta["startTimeStamp"],
+ "ms1_pos": ms1_pos,
+ "ms1_neg": ms1_neg,
+ "ms2_pos": ms2_pos,
+ "ms2_neg": ms2_neg,
+ "last_rt_min": last_rt,
+ "ms1_dt_min": ms1_stats["min"],
+ "ms1_dt_p10": ms1_stats["p10"],
+ "ms1_dt_p25": ms1_stats["p25"],
+ "ms1_dt_median": ms1_stats["median"],
+ "ms1_dt_mean": ms1_stats["mean"],
+ "ms1_dt_p75": ms1_stats["p75"],
+ "ms1_dt_p90": ms1_stats["p90"],
+ "ms1_dt_max": ms1_stats["max"],
+ "ms1_dt_sd": ms1_stats["sd"],
+ "ms2_dt_min": ms2_stats["min"],
+ "ms2_dt_p10": ms2_stats["p10"],
+ "ms2_dt_p25": ms2_stats["p25"],
+ "ms2_dt_median": ms2_stats["median"],
+ "ms2_dt_mean": ms2_stats["mean"],
+ "ms2_dt_p75": ms2_stats["p75"],
+ "ms2_dt_p90": ms2_stats["p90"],
+ "ms2_dt_max": ms2_stats["max"],
+ "ms2_dt_sd": ms2_stats["sd"],
+ "ms1_signalInt_pos_min": ms1_signalInt_pos["min"],
+ "ms1_signalInt_pos_p10": ms1_signalInt_pos["p10"],
+ "ms1_signalInt_pos_p25": ms1_signalInt_pos["p25"],
+ "ms1_signalInt_pos_median": ms1_signalInt_pos["median"],
+ "ms1_signalInt_pos_p75": ms1_signalInt_pos["p75"],
+ "ms1_signalInt_pos_p90": ms1_signalInt_pos["p90"],
+ "ms1_signalInt_pos_p91": ms1_signalInt_pos["p91"],
+ "ms1_signalInt_pos_p92": ms1_signalInt_pos["p92"],
+ "ms1_signalInt_pos_p93": ms1_signalInt_pos["p93"],
+ "ms1_signalInt_pos_p94": ms1_signalInt_pos["p94"],
+ "ms1_signalInt_pos_p95": ms1_signalInt_pos["p95"],
+ "ms1_signalInt_pos_p96": ms1_signalInt_pos["p96"],
+ "ms1_signalInt_pos_p97": ms1_signalInt_pos["p97"],
+ "ms1_signalInt_pos_p98": ms1_signalInt_pos["p98"],
+ "ms1_signalInt_pos_p99": ms1_signalInt_pos["p99"],
+ "ms1_signalInt_pos_max": ms1_signalInt_pos["max"],
+ "ms1_signalInt_neg_min": ms1_signalInt_neg["min"],
+ "ms1_signalInt_neg_p10": ms1_signalInt_neg["p10"],
+ "ms1_signalInt_neg_p25": ms1_signalInt_neg["p25"],
+ "ms1_signalInt_neg_median": ms1_signalInt_neg["median"],
+ "ms1_signalInt_neg_p75": ms1_signalInt_neg["p75"],
+ "ms1_signalInt_neg_p90": ms1_signalInt_neg["p90"],
+ "ms1_signalInt_neg_p91": ms1_signalInt_neg["p91"],
+ "ms1_signalInt_neg_p92": ms1_signalInt_neg["p92"],
+ "ms1_signalInt_neg_p93": ms1_signalInt_neg["p93"],
+ "ms1_signalInt_neg_p94": ms1_signalInt_neg["p94"],
+ "ms1_signalInt_neg_p95": ms1_signalInt_neg["p95"],
+ "ms1_signalInt_neg_p96": ms1_signalInt_neg["p96"],
+ "ms1_signalInt_neg_p97": ms1_signalInt_neg["p97"],
+ "ms1_signalInt_neg_p98": ms1_signalInt_neg["p98"],
+ "ms1_signalInt_neg_p99": ms1_signalInt_neg["p99"],
+ "ms1_signalInt_neg_max": ms1_signalInt_neg["max"],
+ "MS:1000073": mzml_meta["MS:1000073"],
+ "MS:1000079": mzml_meta["MS:1000079"],
+ }
+ _save_cached_stats(filepath, row, scan_event_key)
+ rows.append(row)
+ return rows
+
+
# bracket results
def bracketResults(
indGroups,
@@ -1084,6 +1334,20 @@ def bracketResults(
excel_file = file.replace(".tsv", ".xlsx")
plDB = PolarsDB(excel_file, format="xlsx")
+
+ # Collect all file paths for sample stats
+ all_files = []
+ for key in natSort(indGroups.keys()):
+ for ident in indGroups[key]:
+ all_files.append(ident)
+ sample_stats_rows = compute_sample_stats(all_files, positiveScanEvent, negativeScanEvent)
+ if sample_stats_rows:
+ # Build a uniform schema across all rows
+ all_keys = list(sample_stats_rows[0].keys())
+ stats_dict = {k: [row.get(k) for row in sample_stats_rows] for k in all_keys}
+ sample_stats_df = pl.DataFrame(stats_dict)
+ plDB.insert_table("0_sampleStats", sample_stats_df)
+
plDB.insert_table("Parameters", params_df)
plDB.insert_table("1_Bracketed", df)
plDB.commit()
@@ -1159,6 +1423,8 @@ def calculateMetaboliteGroups(
minConnectionsInFiles=1,
minConnectionRate=0.4,
minPeakCorrelation=0.85,
+ useAbundanceSimilarity=True,
+ abundanceSimilarityThreshold=None,
useRatio=False,
runIdentificationInstance=None,
pwMaxSet=None,
@@ -1448,6 +1714,28 @@ def checkSubCluster(tree, hca, corrThreshold, cutOffMinRatio):
done = done + 1
+ if useAbundanceSimilarity:
+ logging.info("Refining feature groups by abundance profile similarity")
+ # use per-file native peak abundances to compare feature profile similarity across samples
+ abundance_cols = [col for col in table_df.columns if col.endswith("_Abundance_N")]
+ abundance_vectors = {}
+ if len(abundance_cols) > 0:
+ for row in table_df.select(["Num"] + abundance_cols).iter_rows(named=True):
+ abundance_vectors[row["Num"]] = [row.get(col) for col in abundance_cols]
+
+ effective_abundance_threshold = minPeakCorrelation if abundanceSimilarityThreshold is None else abundanceSimilarityThreshold
+ refined_groups = []
+ for tGroup in groups:
+ refined_groups.extend(
+ split_group_by_relative_abundance(
+ tGroup,
+ abundance_vectors,
+ min_peak_correlation=effective_abundance_threshold,
+ min_connection_rate=minConnectionRate,
+ )
+ )
+ groups = refined_groups
+
# Separate feature pair clusters; softer
curGroup = 1
for tGroup in groups:
@@ -1524,6 +1812,14 @@ def checkSubCluster(tree, hca, corrThreshold, cutOffMinRatio):
resDB.conn.insert_row("Parameters", {"Parameter": "MEConvoluting_minConnectionsInFiles", "Value": f"{minConnectionsInFiles}"})
resDB.conn.insert_row("Parameters", {"Parameter": "MEConvoluting_minConnectionRate", "Value": f"{minConnectionRate}"})
resDB.conn.insert_row("Parameters", {"Parameter": "MEConvoluting_minPeakCorrelation", "Value": f"{minPeakCorrelation}"})
+ resDB.conn.insert_row("Parameters", {"Parameter": "MEConvoluting_useAbundanceSimilarity", "Value": f"{useAbundanceSimilarity}"})
+ resDB.conn.insert_row(
+ "Parameters",
+ {
+ "Parameter": "MEConvoluting_abundanceSimilarityThreshold",
+ "Value": f"{minPeakCorrelation if abundanceSimilarityThreshold is None else abundanceSimilarityThreshold}",
+ },
+ )
resDB.conn.set_table(new_sheet_name, table_df)
if double_peaks_df is not None and len(double_peaks_df) > 0:
resDB.conn.insert_table(new_sheet_name + "_doublePeaks", double_peaks_df)
diff --git a/src/findIsoPairs.py b/src/findIsoPairs.py
index c9b9e8b..f969ac5 100644
--- a/src/findIsoPairs.py
+++ b/src/findIsoPairs.py
@@ -31,7 +31,7 @@
from .PolarsDB import PolarsDB
from .findIsoPairs_matchPartners import matchPartners
from .SGR import SGRGenerator
-from .chromPeakPicking.peakpickers import filter_peaks
+from .chromPeakPicking.peakpickers import BasePeakPicker, filter_peaks
import numpy as np
import polars as pl
import scipy
@@ -1168,6 +1168,22 @@ def findChromatographicPeaksAndWriteToDB(self, mzbins, mzxml, tracerID, reportFu
peaksN = filter_peaks(peaksN, config=self.peak_filter_config)
peaksL = filter_peaks(peaksL, config=self.peak_filter_config)
+ # Recalculate SNR, FWHM, area from raw (unsmoothed) EICs
+ # Peak boundaries are kept from smoothed-EIC detection, only metrics are recomputed
+ times_arr = np.asarray(times)
+ eic_raw_arr = np.asarray(eic)
+ eicL_raw_arr = np.asarray(eicL)
+ for pk in peaksN:
+ pk.snr = BasePeakPicker.compute_snr(eic_raw_arr, pk.apex_index, pk.start_index, pk.end_index)
+ pk.fwhm = BasePeakPicker.compute_fwhm(times_arr, eic_raw_arr, pk.apex_index, pk.start_index, pk.end_index)
+ pk.area = BasePeakPicker.compute_area(times_arr, eic_raw_arr, pk.start_index, pk.end_index)
+ pk.apex_intensity = float(eic_raw_arr[pk.apex_index])
+ for pk in peaksL:
+ pk.snr = BasePeakPicker.compute_snr(eicL_raw_arr, pk.apex_index, pk.start_index, pk.end_index)
+ pk.fwhm = BasePeakPicker.compute_fwhm(times_arr, eicL_raw_arr, pk.apex_index, pk.start_index, pk.end_index)
+ pk.area = BasePeakPicker.compute_area(times_arr, eicL_raw_arr, pk.start_index, pk.end_index)
+ pk.apex_intensity = float(eicL_raw_arr[pk.apex_index])
+
# get EICs of M+1, M'-1 and M'+1 for the database
eicfirstiso, timesL, scanIdsL, mzsfirstiso = mzxml.getEIC(
meanmz + 1.00335484 / loading,
@@ -1846,6 +1862,12 @@ def annotateFeaturePairs(self, chromPeaks, mzxml, tracer, reportFunction=None):
peak = chromPeaks[i]
## Annotate hetero atoms
+ scanEvent = ""
+ if peak.ionMode == "+":
+ scanEvent = self.positiveScanEvent
+ elif peak.ionMode == "-":
+ scanEvent = self.negativeScanEvent
+
for pIso in self.heteroAtoms:
pIsotope = self.heteroAtoms[pIso]
@@ -1861,12 +1883,6 @@ def annotateFeaturePairs(self, chromPeaks, mzxml, tracer, reportFunction=None):
else:
refMz = peak.lmz
- scanEvent = ""
- if peak.ionMode == "+":
- scanEvent = self.positiveScanEvent
- elif peak.ionMode == "-":
- scanEvent = self.negativeScanEvent
-
for haCount in range(pIsotope.minCount, pIsotope.maxCount + 1):
if haCount == 0:
continue
@@ -2683,6 +2699,11 @@ def groupFeaturePairsUntargetedAndWriteToDB(self, chromPeaks, mzxml, tracer, tra
allPeaks[peak.id] = peak
peak.correlationsToOthers = []
+ # sort by NPeakCenter once so the inner loop can break early on RT distance
+ chromPeaks = sorted(chromPeaks, key=lambda p: p.NPeakCenter)
+
+ ff_rows = [] # accumulated featurefeatures rows for a single bulk insert
+
# compare all detected feature pairs at approximately the same retention time
for piA in range(len(chromPeaks)):
peakA = chromPeaks[piA]
@@ -2695,12 +2716,19 @@ def groupFeaturePairsUntargetedAndWriteToDB(self, chromPeaks, mzxml, tracer, tra
if peakA.id not in correlations.keys():
correlations[peakA.id] = {}
- for peakB in chromPeaks:
+ for piB in range(piA + 1, len(chromPeaks)):
+ peakB = chromPeaks[piB]
+
+ # peaks are sorted by NPeakCenter; once the RT gap exceeds the threshold
+ # all remaining peaks are even further away — safe to stop early
+ if peakB.NPeakCenter - peakA.NPeakCenter >= self.peakCenterError:
+ break
+
if peakB.id not in correlations.keys():
correlations[peakB.id] = {}
if peakA.mz < peakB.mz:
- if abs(peakA.NPeakCenter - peakB.NPeakCenter) < self.peakCenterError:
+ if True: # RT check already handled by the sorted early-exit above
bmin = int(
max(
0,
@@ -2767,10 +2795,16 @@ def groupFeaturePairsUntargetedAndWriteToDB(self, chromPeaks, mzxml, tracer, tra
logging.error("Error while convoluting two feature pairs, skipping.. (%s)" % str(e))
try:
- db_con.insert_row("featurefeatures", {"fID1": peakA.id, "fID2": peakB.id, "corr": pb, "silRatioValue": silRatiosFold})
+ ff_rows.append({"fID1": peakA.id, "fID2": peakB.id, "corr": pb, "silRatioValue": silRatiosFold})
except Exception as e:
logging.error("Error while convoluting two feature pairs, skipping.. (%s)" % str(e))
- db_con.insert_row("featurefeatures", {"fID1": peakA.id, "fID2": peakB.id, "corr": 0, "silRatioValue": 0})
+ ff_rows.append({"fID1": peakA.id, "fID2": peakB.id, "corr": 0, "silRatioValue": 0})
+
+ # bulk-insert all featurefeatures rows at once instead of one per pair
+ if ff_rows:
+ _ff_schema = db_con.tables["featurefeatures"].schema
+ _ff_new = pl.DataFrame(ff_rows, schema=_ff_schema)
+ db_con.tables["featurefeatures"] = pl.concat([db_con.tables["featurefeatures"], _ff_new], how="vertical")
self.postMessageToProgressWrapper("text", "%s: Convoluting feature groups" % tracer.name)
@@ -2780,11 +2814,7 @@ def groupFeaturePairsUntargetedAndWriteToDB(self, chromPeaks, mzxml, tracer, tra
delattr(peak, "times")
for k in nodes.keys():
- uniq = []
- for u in nodes[k]:
- if u not in uniq:
- uniq.append(u)
- nodes[k] = uniq
+ nodes[k] = list(set(nodes[k]))
# get subgraphs from the feature pair graph. Each subgraph represents one convoluted
# feature group
@@ -2858,8 +2888,8 @@ def checkSubCluster(tree, hca, corrThreshold, cutOffMinRatio):
# print("HCA with", len(cGroups))
gGroup = cGroups.pop(0)
- ## TODO optimize this code, it recalculates the computationally expensive HCA too often for a high number of features
- if False and len(gGroup) > 100:
+ ## skip expensive HCA splitting for very large groups — keep them as-is
+ if len(gGroup) > 100:
groups.append(gGroup)
continue
@@ -2909,24 +2939,57 @@ def checkSubCluster(tree, hca, corrThreshold, cutOffMinRatio):
"%s: Annotating feature groups (%d/%d done)" % (tracer.name, done, len(groups)),
)
+ # collect all per-peak update data and apply in a single batch join-based update
+ _update_ids = []
+ _update_adducts = []
+ _update_fDesc = []
+ _update_corrToOthers = []
+ _update_heteroAtoms = []
+
for peak in chromPeaks:
adds = countEntries(peak.adducts)
peak.adducts = list(adds.keys())
- # Update chromPeaks
- db_con.tables["chromPeaks"] = db_con.tables["chromPeaks"].with_columns(
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.adducts)).decode("utf-8"))).otherwise(pl.col("adducts")).alias("adducts"),
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.fDesc)).decode("utf-8"))).otherwise(pl.col("fDesc")).alias("fDesc"),
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.correlationsToOthers)).decode("utf-8"))).otherwise(pl.col("correlationsToOthers")).alias("correlationsToOthers"),
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.heteroAtomsFeaturePairs)).decode("utf-8"))).otherwise(pl.col("heteroAtomsFeaturePairs")).alias("heteroAtomsFeaturePairs"),
+ _update_ids.append(peak.id)
+ _update_adducts.append(base64.b64encode(dumps(peak.adducts)).decode("utf-8"))
+ _update_fDesc.append(base64.b64encode(dumps(peak.fDesc)).decode("utf-8"))
+ _update_corrToOthers.append(base64.b64encode(dumps(peak.correlationsToOthers)).decode("utf-8"))
+ _update_heteroAtoms.append(base64.b64encode(dumps(peak.heteroAtomsFeaturePairs)).decode("utf-8"))
+
+ _upd_df = pl.DataFrame(
+ {
+ "id": _update_ids,
+ "_adducts": _update_adducts,
+ "_fDesc": _update_fDesc,
+ "_corrToOthers": _update_corrToOthers,
+ "_heteroAtoms": _update_heteroAtoms,
+ }
+ )
+
+ # single join-based update for chromPeaks
+ db_con.tables["chromPeaks"] = (
+ db_con.tables["chromPeaks"]
+ .join(_upd_df, on="id", how="left")
+ .with_columns(
+ pl.coalesce([pl.col("_adducts"), pl.col("adducts")]).alias("adducts"),
+ pl.coalesce([pl.col("_fDesc"), pl.col("fDesc")]).alias("fDesc"),
+ pl.coalesce([pl.col("_corrToOthers"), pl.col("correlationsToOthers")]).alias("correlationsToOthers"),
+ pl.coalesce([pl.col("_heteroAtoms"), pl.col("heteroAtomsFeaturePairs")]).alias("heteroAtomsFeaturePairs"),
)
+ .drop(["_adducts", "_fDesc", "_corrToOthers", "_heteroAtoms"])
+ )
- # Update allChromPeaks
- db_con.tables["allChromPeaks"] = db_con.tables["allChromPeaks"].with_columns(
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.adducts)).decode("utf-8"))).otherwise(pl.col("adducts")).alias("adducts"),
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.fDesc)).decode("utf-8"))).otherwise(pl.col("fDesc")).alias("fDesc"),
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.heteroAtomsFeaturePairs)).decode("utf-8"))).otherwise(pl.col("heteroAtomsFeaturePairs")).alias("heteroAtomsFeaturePairs"),
+ # single join-based update for allChromPeaks (no correlationsToOthers column there)
+ db_con.tables["allChromPeaks"] = (
+ db_con.tables["allChromPeaks"]
+ .join(_upd_df.select(["id", "_adducts", "_fDesc", "_heteroAtoms"]), on="id", how="left")
+ .with_columns(
+ pl.coalesce([pl.col("_adducts"), pl.col("adducts")]).alias("adducts"),
+ pl.coalesce([pl.col("_fDesc"), pl.col("fDesc")]).alias("fDesc"),
+ pl.coalesce([pl.col("_heteroAtoms"), pl.col("heteroAtomsFeaturePairs")]).alias("heteroAtomsFeaturePairs"),
)
+ .drop(["_adducts", "_fDesc", "_heteroAtoms"])
+ )
# store feature group in the database
for group in sorted(
diff --git a/src/findIsoPairs_matchPartners.py b/src/findIsoPairs_matchPartners.py
index a45951e..d0da9c1 100644
--- a/src/findIsoPairs_matchPartners.py
+++ b/src/findIsoPairs_matchPartners.py
@@ -17,19 +17,26 @@
from copy import deepcopy
+import traceback
from .formulaTools import formulaTools
from .utils import Bunch, getNormRatio
maxSub = 5
-def getSubstitutionArray(purity, xMax, maxSub):
+def calculate_theoretical_ratios(isotopic_enrichment, Xn_max, maxSub):
+ """
+ calculates the theoretical isotopolog distribution for a given isotopic enrichment and number of atoms
+ isotopic_enrichment: isotopic enrichment of the isotope element (e.g. 0.9893 for 12C)
+ Xn_max: maximum number of atoms that can be exchanged (e.g. 3 for 3 carbon atoms)
+ maxSub: maximum number of substitutions
+ """
ret = []
- ret.append([-1 for n in range(0, maxSub + 1)])
- for i in range(1, xMax + 1):
+ ret.append([])
+ for Xn in range(1, Xn_max + 1):
cur = []
- for j in range(0, maxSub + 1):
- cur.append(getNormRatio(purity, i, j))
+ for cur_exchange_number in range(0, min(Xn, maxSub) + 1):
+ cur.append(getNormRatio(isotopic_enrichment, Xn, cur_exchange_number))
ret.append(cur)
return ret
@@ -144,14 +151,13 @@ def matchPartners(
reportFunction=None,
writeExtendedDiagnostics=True,
):
- scanRTRange = stopTime - startTime
+ scan_rt_range = stopTime - startTime
- cValidationOffset = 1.00335484 # mass difference between 12C and 13C
+ c13c12_offset = 1.00335484 # mass difference between 12C and 13C
+ ori_Xoffset = xOffset
- detectedIonPairs = []
-
- oriXOffset = xOffset
- oriCValidationOffset = cValidationOffset
+ detected_signal_pairs = []
+ fT = formulaTools()
if labellingElement == "C":
useCIsotopePatternValidation = 0
@@ -159,135 +165,140 @@ def matchPartners(
# when the labelling element is carbon (to some extend that's logical)
# substitution arrays for checking the number of carbon atoms
- purityNArray = getSubstitutionArray(purityN, max(xCounts) + 1, maxSub) # native metabolite
- purityLArray = getSubstitutionArray(purityL, max(xCounts) + 1, maxSub) # labelled metabolite
-
- labelingElements = {}
- labelingElements["C"] = Bunch(
- nativeIsotope="12C",
- labelingIsotope="13C",
- massNative=12.0,
- massLabeled=13.00335,
- isotopicEnrichmentNative=0.9893,
- isotopicEnrichmentLabeled=0.995,
- minXn=1,
- maxXn=3,
- )
- labelingElements["H"] = Bunch(
- nativeIsotope="1H",
- labelingIsotope="2H",
- massNative=1.0078250,
- massLabeled=2.0141018,
- isotopicEnrichmentNative=0.9999,
- isotopicEnrichmentLabeled=0.96,
- minXn=0,
- maxXn=9,
- )
-
- ## combinations of labeling elements
- tempCombs = getCombinationsOfLabel(["C", "H"], labelingElements, 2, 12)
- combs = []
- for comb in tempCombs:
- ## remove implausible labeling combinations
- if True:
- if "C" in comb.atoms.keys() and comb.atoms["C"] == 1 and ("H" not in comb.atoms.keys() or ("H" in comb.atoms.keys() and comb.atoms["H"] in [1, 2, 3])):
- combs.append(comb)
- if "C" in comb.atoms.keys() and comb.atoms["C"] == 2 and ("H" not in comb.atoms.keys() or ("H" in comb.atoms.keys() and comb.atoms["H"] in [1, 2, 3, 4, 5, 6])):
- combs.append(comb)
- if "C" in comb.atoms.keys() and comb.atoms["C"] == 3 and ("H" not in comb.atoms.keys() or ("H" in comb.atoms.keys() and comb.atoms["H"] in [1, 2, 3, 4, 5, 6, 7, 8, 9])):
- combs.append(comb)
-
- fT = formulaTools()
+ theoretical_ratios_native = calculate_theoretical_ratios(purityN, max(xCounts) + 1, maxSub) # native metabolite
+ theoretical_ratios_labeled = calculate_theoretical_ratios(purityL, max(xCounts) + 1, maxSub) # labelled metabolite
+ # beta-feature, support for two labeling elements at once (e.g., custom tracers)
useDoubleLabelingCombinations = False
-
if useDoubleLabelingCombinations:
- print("The following labeling configurations are used:")
- for comb in combs:
- print(comb)
+ labelingElements = {}
+ labelingElements["C"] = Bunch(
+ nativeIsotope="12C",
+ labelingIsotope="13C",
+ massNative=12.0,
+ massLabeled=13.00335,
+ isotopicEnrichmentNative=0.9893,
+ isotopicEnrichmentLabeled=0.995,
+ minXn=1,
+ maxXn=3,
+ )
+ labelingElements["H"] = Bunch(
+ nativeIsotope="1H",
+ labelingIsotope="2H",
+ massNative=1.0078250,
+ massLabeled=2.0141018,
+ isotopicEnrichmentNative=0.9999,
+ isotopicEnrichmentLabeled=0.96,
+ minXn=0,
+ maxXn=9,
+ )
+
+ ## combinations of labeling elements
+ tempCombs = getCombinationsOfLabel(["C", "H"], labelingElements, 2, 12)
+ combs = []
+ for comb in tempCombs:
+ ## remove implausible labeling combinations
+ if True:
+ if "C" in comb.atoms.keys() and comb.atoms["C"] == 1 and ("H" not in comb.atoms.keys() or ("H" in comb.atoms.keys() and comb.atoms["H"] in [1, 2, 3])):
+ combs.append(comb)
+ if "C" in comb.atoms.keys() and comb.atoms["C"] == 2 and ("H" not in comb.atoms.keys() or ("H" in comb.atoms.keys() and comb.atoms["H"] in [1, 2, 3, 4, 5, 6])):
+ combs.append(comb)
+ if "C" in comb.atoms.keys() and comb.atoms["C"] == 3 and ("H" not in comb.atoms.keys() or ("H" in comb.atoms.keys() and comb.atoms["H"] in [1, 2, 3, 4, 5, 6, 7, 8, 9])):
+ combs.append(comb)
+ if useDoubleLabelingCombinations:
+ print("The following labeling configurations are used:")
+ for comb in combs:
+ print(comb)
+
+ # other parameters
+ max_other_iso_intensity_ratio = 0.25
# iterate over all MS scans (lvl. 1)
- curScanIndex = 0
- for j in range(0, len(mzXMLData.MS1_list)):
+ cur_native_scan_index = 0
+ next_signal_pair_id = 1
+ for cur_scan_index in range(0, len(mzXMLData.MS1_list)):
try:
- curScan = mzXMLData.MS1_list[j]
- curScanDetectedIonPairs = []
+ cur_scan = mzXMLData.MS1_list[cur_scan_index]
+ cur_scan_detected_signal_pairs = []
# check for correct filterline and scan time
- if curScan.filter_line == filterLine:
- if startTime <= (curScan.retention_time / 60.0) <= stopTime:
+ if cur_scan.filter_line == filterLine:
+ if startTime <= (cur_scan.retention_time / 60.0) <= stopTime:
if reportFunction is not None:
reportFunction(
- (curScan.retention_time / 60.0 - startTime) / scanRTRange,
- "RT %.2f, found patterns: %d" % (curScan.retention_time / 60.0, len(detectedIonPairs)),
+ (cur_scan.retention_time / 60.0 - startTime) / scan_rt_range,
+ "RT %.2f, found patterns: %d" % (cur_scan.retention_time / 60.0, len(detected_signal_pairs)),
)
+
# Compute the scan used to search for labeled signals; may be offset relative to the native scan.
# Falls back to curScan when the offset puts the index out of bounds or on a different filter line.
# but make sure that the offset is scanIndexOffset scans of the correct filter_line
- cj = j
- cur_scan_Index_Offset = 0
- direction = 1 if scanIndexOffset >= 0 else -1
- while cj > 0:
- if mzXMLData.MS1_list[cj].filter_line == filterLine:
- if cur_scan_Index_Offset == scanIndexOffset:
- break
- else:
- cur_scan_Index_Offset += direction
- cj += direction
-
- labScanJ = cj
+ lab_scan_index = cur_scan_index
+ if scanIndexOffset != 0:
+ cur_scan_Index_Offset = 0
+ direction = 1 if scanIndexOffset >= 0 else -1
+ while lab_scan_index > 0:
+ if mzXMLData.MS1_list[lab_scan_index].filter_line == filterLine:
+ if cur_scan_Index_Offset == scanIndexOffset:
+ break
+ else:
+ cur_scan_Index_Offset += direction
+ lab_scan_index += direction
+
+ # use labeled scan, test if it is valid (correct filter line, within bounds)
+ labScanJ = lab_scan_index
if 0 <= labScanJ < len(mzXMLData.MS1_list) and mzXMLData.MS1_list[labScanJ].filter_line == filterLine:
labScan = mzXMLData.MS1_list[labScanJ]
else:
- raise RuntimeError(f"ERROR: Could not find a suitable lab scan for scan index {j} with offset {scanIndexOffset}. ")
+ raise RuntimeError(f"ERROR: Could not find a suitable lab scan for scan index {cur_scan_index} with offset {scanIndexOffset}. ")
dontUsePeakIndices = []
# assume each peak to be a valid M (monoisotopic, native metabolite ion)
# and verify this assumption (search for (partially) labelled pendant)
- for currentPeakIndex in range(0, len(curScan.mz_list)):
+ for currentPeakIndex in range(0, len(cur_scan.mz_list)):
if currentPeakIndex not in dontUsePeakIndices:
- curPeakmz = curScan.mz_list[currentPeakIndex]
- curPeakIntensity = curScan.intensity_list[currentPeakIndex]
+ isoM_mz = cur_scan.mz_list[currentPeakIndex]
+ isoM_int = cur_scan.intensity_list[currentPeakIndex]
- curPeakDetectedIonPairs = []
+ cur_signal_detected_partners = []
# only consider peaks above the threshold
- if curPeakIntensity >= intensityThres:
- skipOtherLoadings = False
+ if isoM_int >= intensityThres:
+ skip_other_loadings = False
## do not process peaks that are likely isotopologs
- backIsos = []
- for l in range(maxLoading, 0, -1):
- iso = curScan.findMZ(curPeakmz - oriCValidationOffset / l, ppm)
- iso = curScan.getMostIntensePeak(iso[0], iso[1])
-
- if iso != -1 and curScan.intensity_list[iso] > curPeakIntensity:
- backIsos.append(l)
- if len(backIsos) > 0:
+ m_negative_C_isotopologs = []
+ for current_charge in range(maxLoading, 0, -1):
+ iso = cur_scan.findMZ(isoM_mz - c13c12_offset / current_charge, ppm)
+ iso = cur_scan.getMostIntensePeak(iso[0], iso[1])
+
+ if iso != -1 and cur_scan.intensity_list[iso] > isoM_int * max_other_iso_intensity_ratio:
+ m_negative_C_isotopologs.append(current_charge)
+ if len(m_negative_C_isotopologs) > 0:
continue
- possibleLoadings = []
+ possible_charges = []
## figure out possible loadings
- for l in range(maxLoading, 0, -1):
- iso = curScan.findMZ(
- curPeakmz + oriCValidationOffset / l,
+ for current_charge in range(maxLoading, 0, -1):
+ iso = cur_scan.findMZ(
+ isoM_mz + c13c12_offset / current_charge,
ppm,
start=currentPeakIndex,
)
- iso = curScan.getMostIntensePeak(iso[0], iso[1])
+ iso = cur_scan.getMostIntensePeak(iso[0], iso[1])
if iso != -1:
- possibleLoadings.append(l)
+ possible_charges.append(current_charge)
break ## skip other loadings
- if len(possibleLoadings) == 0:
- possibleLoadings = [1]
+ if len(possible_charges) == 0:
+ possible_charges = [1]
- for curLoading in possibleLoadings:
- if not skipOtherLoadings:
- xOffset = oriXOffset / float(curLoading)
- cValidationOffset = oriCValidationOffset / float(curLoading)
+ for current_charge in possible_charges:
+ if not skip_other_loadings:
+ xOffset_charged = ori_Xoffset / float(current_charge)
+ c13c12_offset_charged = c13c12_offset / float(current_charge)
# C-isotope distribution validation for labelling with N, S, ... (useCValidation == 2)
# The carbon distribution of both isotopologs is checked for equality
@@ -301,63 +312,63 @@ def matchPartners(
# EXPERIMENTAL: has not been tested with real data (not N or S labelled sample material
# was available)
if not useDoubleLabelingCombinations and useCIsotopePatternValidation != 0:
- isoM_m1 = curScan.findMZ(curPeakmz - cValidationOffset, ppm * 2)
- isoM_m1 = curScan.getMostIntensePeak(isoM_m1[0], isoM_m1[1])
- if isoM_m1 != -1:
- obRatio = curScan.intensity_list[isoM_m1] / curPeakIntensity
- if obRatio >= 0.5:
+ isoM_m1_index = cur_scan.findMZ(isoM_mz - c13c12_offset_charged, ppm * 2)
+ isoM_m1_index = cur_scan.getMostIntensePeak(isoM_m1_index[0], isoM_m1_index[1])
+ if isoM_m1_index != -1:
+ obs_ratio_Mm1_to_M = cur_scan.intensity_list[isoM_m1_index] / isoM_int
+ if obs_ratio_Mm1_to_M >= 0.5:
continue
# When lowAbundanceIsotopeCutoff is enabled, also check M-1 abundance
# to verify the detected peak is truly M and not an isotopolog artifact
- if lowAbundanceIsotopeCutoff and isoM_m1 != -1:
- isoM_m1_Intensity = curScan.intensity_list[isoM_m1]
- if isoM_m1_Intensity > curPeakIntensity:
+ if lowAbundanceIsotopeCutoff and isoM_m1_index != -1:
+ isoM_m1_Intensity = cur_scan.intensity_list[isoM_m1_index]
+ if isoM_m1_Intensity > isoM_int:
# M-1 is more intense than M: likely not the monoisotopic peak
continue
# find M+1 peak
- isoM_p1 = curScan.findMZ(
- curPeakmz + cValidationOffset,
+ isoM_p1_index = cur_scan.findMZ(
+ isoM_mz + c13c12_offset_charged,
ppm,
start=currentPeakIndex,
)
- isoM_p1 = curScan.getMostIntensePeak(isoM_p1[0], isoM_p1[1])
- if isoM_p1 != -1 or peakCountLeft == 1 or lowAbundanceIsotopeCutoff:
+ isoM_p1_index = cur_scan.getMostIntensePeak(isoM_p1_index[0], isoM_p1_index[1])
+ if isoM_p1_index != -1 or peakCountLeft == 1 or lowAbundanceIsotopeCutoff:
# test certain number of labelled carbon atoms
- for xCount in sorted(xCounts, reverse=True):
+ for current_Cn in sorted(xCounts, reverse=True):
# find corresponding M' peak (search in labScan, which may be offset)
- isoM_pX = labScan.findMZ(
- curPeakmz + xCount * xOffset,
+ isoMP_index = labScan.findMZ(
+ isoM_mz + current_Cn * c13c12_offset_charged,
ppm,
)
- isoM_pX = labScan.getMostIntensePeak(
- isoM_pX[0],
- isoM_pX[1],
+ isoMP_index = labScan.getMostIntensePeak(
+ isoMP_index[0],
+ isoMP_index[1],
intensityThres,
)
- if isoM_pX != -1:
- labPeakmz = labScan.mz_list[isoM_pX]
- labPeakIntensity = labScan.intensity_list[isoM_pX]
+ if isoMP_index != -1:
+ isoMP_mz = labScan.mz_list[isoMP_index]
+ isoMP_int = labScan.intensity_list[isoMP_index]
# find M'-1 peak
- isoM_pXm1 = labScan.findMZ(
- curPeakmz + xCount * xOffset - cValidationOffset,
+ isoMP_m1_index = labScan.findMZ(
+ isoM_mz + current_Cn * c13c12_offset_charged - c13c12_offset_charged,
ppm * 2,
)
- isoM_pXm1 = labScan.getMostIntensePeak(
- isoM_pXm1[0],
- isoM_pXm1[1],
+ isoMP_m1_index = labScan.getMostIntensePeak(
+ isoMP_m1_index[0],
+ isoMP_m1_index[1],
)
- if isoM_pXm1 != -1:
- obRatio = labScan.intensity_list[isoM_pXm1] / labScan.intensity_list[isoM_pX]
- if obRatio >= 0.5:
+ if isoMP_m1_index != -1:
+ obs_ratio_Mm1_to_M = labScan.intensity_list[isoMP_m1_index] / labScan.intensity_list[isoMP_index]
+ if obs_ratio_Mm1_to_M >= 0.5:
continue
# (1.) check if M' and M ratio are as expected
if checkRatio:
- rat = curPeakIntensity / labPeakIntensity
+ rat = isoM_int / isoMP_int
if minRatio <= rat <= maxRatio:
pass ## ratio check passed
else:
@@ -365,93 +376,93 @@ def matchPartners(
## no isotopolog verification needs to be performed
if peakCountLeft == 1 and peakCountRight == 1:
- curPeakDetectedIonPairs.append(
+ cur_signal_detected_partners.append(
mzFeature(
- id=len(curPeakDetectedIonPairs),
- mz=curPeakmz,
- lmz=labPeakmz,
- tmz=xCount * xOffset,
- xCount=xCount,
- scanIndex=curScanIndex,
- scanid=curScan.id,
- scantime=curScan.retention_time,
- loading=curLoading,
- nIntensity=curPeakIntensity,
- lIntensity=labPeakIntensity,
+ id=len(cur_signal_detected_partners),
+ mz=isoM_mz,
+ lmz=isoMP_mz,
+ tmz=current_Cn * c13c12_offset_charged,
+ xCount=current_Cn,
+ scanIndex=cur_native_scan_index,
+ scanid=cur_scan.id,
+ scantime=cur_scan.retention_time,
+ loading=current_charge,
+ nIntensity=isoM_int,
+ lIntensity=isoMP_int,
ionMode=ionMode,
)
)
- skipOtherLoadings = True
+ skip_other_loadings = True
continue
# find M'+1 peak
- isoM_pXp1 = labScan.findMZ(
- curPeakmz + xCount * xOffset + cValidationOffset,
+ isoMP_p1_index = labScan.findMZ(
+ isoM_mz + current_Cn * c13c12_offset_charged + c13c12_offset_charged,
ppm,
)
- isoM_pXp1 = labScan.getMostIntensePeak(
- isoM_pXp1[0],
- isoM_pXp1[1],
+ isoMP_p1_index = labScan.getMostIntensePeak(
+ isoMP_p1_index[0],
+ isoMP_p1_index[1],
)
# calculate the ratio of M+1/M (native, always in curScan)
- isoPeakIntensity = curScan.intensity_list[isoM_p1]
+ isoPeakIntensity = cur_scan.intensity_list[isoM_p1_index]
if peakCountLeft == 1:
ratioN = None
elif peakCountLeft > 1 and lowAbundanceIsotopeCutoff and isoPeakIntensity <= isotopologIntensityThres:
ratioN = None
else:
- ratioN = isoPeakIntensity / curPeakIntensity
+ ratioN = isoPeakIntensity / isoM_int
# calculate the ratio of M'+1/M' (labeled, in labScan)
- isoLabPeakIntensity = labScan.intensity_list[isoM_pXp1]
+ isoLabPeakIntensity = labScan.intensity_list[isoMP_p1_index]
if peakCountRight == 1:
ratioL = None
elif peakCountRight > 1 and lowAbundanceIsotopeCutoff and isoLabPeakIntensity <= isotopologIntensityThres:
ratioL = None
else:
- ratioL = isoLabPeakIntensity / labPeakIntensity
+ ratioL = isoLabPeakIntensity / isoMP_int
# 2. check if the observed M'+1/M' ratio and the M+1/M ratio are approximately equal
if (ratioN is not None and ratioL is not None) and abs(ratioN - ratioL) <= intensityErrorL:
- curPeakDetectedIonPairs.append(
+ cur_signal_detected_partners.append(
mzFeature(
- id=len(curPeakDetectedIonPairs),
- mz=curPeakmz,
- lmz=labPeakmz,
- tmz=xCount * xOffset,
- xCount=xCount,
- scanIndex=curScanIndex,
- scanid=curScan.id,
- scantime=curScan.retention_time,
- loading=curLoading,
- nIntensity=curPeakIntensity,
- lIntensity=labPeakIntensity,
+ id=len(cur_signal_detected_partners),
+ mz=isoM_mz,
+ lmz=isoMP_mz,
+ tmz=current_Cn * c13c12_offset_charged,
+ xCount=current_Cn,
+ scanIndex=cur_native_scan_index,
+ scanid=cur_scan.id,
+ scantime=cur_scan.retention_time,
+ loading=current_charge,
+ nIntensity=isoM_int,
+ lIntensity=isoMP_int,
ionMode=ionMode,
)
)
- skipOtherLoadings = True
+ skip_other_loadings = True
continue
elif lowAbundanceIsotopeCutoff and (ratioN is None or ratioL is None):
- curPeakDetectedIonPairs.append(
+ cur_signal_detected_partners.append(
mzFeature(
- id=len(curPeakDetectedIonPairs),
- mz=curPeakmz,
- lmz=labPeakmz,
- tmz=xCount * xOffset,
- xCount=xCount,
- scanIndex=curScanIndex,
- scanid=curScan.id,
- scantime=curScan.retention_time,
- loading=curLoading,
- nIntensity=curPeakIntensity,
- lIntensity=labPeakIntensity,
+ id=len(cur_signal_detected_partners),
+ mz=isoM_mz,
+ lmz=isoMP_mz,
+ tmz=current_Cn * c13c12_offset_charged,
+ xCount=current_Cn,
+ scanIndex=cur_native_scan_index,
+ scanid=cur_scan.id,
+ scantime=cur_scan.retention_time,
+ loading=current_charge,
+ nIntensity=isoM_int,
+ lIntensity=isoMP_int,
ionMode=ionMode,
)
)
- skipOtherLoadings = True
+ skip_other_loadings = True
continue
# endregion
@@ -468,63 +479,61 @@ def matchPartners(
# region
# (0.) verify that peak is M and not something else (e.g. M+1, M+1...)
## TODO improve me. Use seven golden rules or the number of carbon atoms
- isoM_m1 = curScan.findMZ(curPeakmz - cValidationOffset, ppm)
- isoM_m1 = curScan.getMostIntensePeak(isoM_m1[0], isoM_m1[1])
- if isoM_m1 != -1:
- obRatio = curScan.intensity_list[isoM_m1] / curPeakIntensity
- if obRatio >= 0.5:
+ isoM_m1_index = cur_scan.findMZ(isoM_mz - c13c12_offset_charged, ppm)
+ isoM_m1_index = cur_scan.getMostIntensePeak(isoM_m1_index[0], isoM_m1_index[1])
+ if isoM_m1_index != -1:
+ obs_ratio_Mm1_to_M = cur_scan.intensity_list[isoM_m1_index] / isoM_int
+ if obs_ratio_Mm1_to_M >= max_other_iso_intensity_ratio:
continue
# find M+1 peak
- isoM_p1 = curScan.findMZ(
- curPeakmz + cValidationOffset,
+ isoM_p1_index = cur_scan.findMZ(
+ isoM_mz + c13c12_offset_charged,
ppm,
start=currentPeakIndex,
)
- isoM_p1 = curScan.getMostIntensePeak(isoM_p1[0], isoM_p1[1])
- if isoM_p1 != -1 or peakCountLeft == 1 or lowAbundanceIsotopeCutoff:
+ isoM_p1_index = cur_scan.getMostIntensePeak(isoM_p1_index[0], isoM_p1_index[1])
+ if isoM_p1_index != -1 or peakCountLeft == 1 or lowAbundanceIsotopeCutoff:
# test certain number of labelled carbon atoms
- for xCount in sorted(xCounts, reverse=True):
+ for current_Cn in sorted(xCounts, reverse=True):
# stop for impossible carbon atom number
- if xCount > curPeakmz * curLoading / 12:
- continue
# find corresponding M' peak (search in labScan, which may be offset)
- isoM_pX = labScan.findMZ(
- curPeakmz + xCount * cValidationOffset,
+ isoMP_index = labScan.findMZ(
+ isoM_mz + current_Cn * c13c12_offset_charged,
ppm,
)
- isoM_pX = labScan.getMostIntensePeak(
- isoM_pX[0],
- isoM_pX[1],
+ isoMP_index = labScan.getMostIntensePeak(
+ isoMP_index[0],
+ isoMP_index[1],
intensityThres,
)
- if isoM_pX != -1:
- labPeakmz = labScan.mz_list[isoM_pX]
- labPeakIntensity = labScan.intensity_list[isoM_pX]
+ if isoMP_index != -1:
+ isoMP_mz = labScan.mz_list[isoMP_index]
+ isoMP_int = labScan.intensity_list[isoMP_index]
# (0.) verify that peak is M' and not something else (e.g. M'-1, M'-2)
# only for AllExtract experiments
- adjRatio = 0
- isoM_pXp1 = labScan.findMZ(
- curPeakmz + (xCount + 1) * cValidationOffset,
+ obs_ratio_MPp1_to_MP = 0
+ isoMP_p1_index = labScan.findMZ(
+ isoM_mz + (current_Cn + 1) * c13c12_offset_charged,
ppm,
)
- isoM_pXp1 = labScan.getMostIntensePeak(
- isoM_pXp1[0],
- isoM_pXp1[1],
+ isoMP_p1_index = labScan.getMostIntensePeak(
+ isoMP_p1_index[0],
+ isoMP_p1_index[1],
)
- if isoM_pXp1 != -1:
- adjRatio = labScan.intensity_list[isoM_pXp1] / labPeakIntensity
+ if isoMP_p1_index != -1:
+ obs_ratio_MPp1_to_MP = labScan.intensity_list[isoMP_p1_index] / isoMP_int
if not metabolisationExperiment:
- if adjRatio >= 0.5:
+ if obs_ratio_MPp1_to_MP >= max_other_iso_intensity_ratio:
continue
# (1.) check if M' and M ratio are as expected
if checkRatio:
- rat = curPeakIntensity / labPeakIntensity
+ rat = isoM_int / isoMP_int
if minRatio <= rat <= maxRatio:
pass ## ratio check passed
else:
@@ -532,52 +541,52 @@ def matchPartners(
## no isotopolog verification needs to be performed
if peakCountLeft == 1 and peakCountRight == 1:
- curPeakDetectedIonPairs.append(
+ cur_signal_detected_partners.append(
mzFeature(
- id=len(curPeakDetectedIonPairs),
- mz=curPeakmz,
- lmz=labPeakmz,
- tmz=xCount * cValidationOffset,
- xCount=xCount,
- scanIndex=curScanIndex,
- scanid=curScan.id,
- scantime=curScan.retention_time,
- loading=curLoading,
- nIntensity=curPeakIntensity,
- lIntensity=labPeakIntensity,
+ id=next_signal_pair_id,
+ mz=isoM_mz,
+ lmz=isoMP_mz,
+ tmz=isoMP_mz - isoM_mz,
+ xCount=current_Cn,
+ scanIndex=cur_native_scan_index,
+ scanid=cur_scan.id,
+ scantime=cur_scan.retention_time,
+ loading=current_charge,
+ nIntensity=isoM_int,
+ lIntensity=isoMP_int,
ionMode=ionMode,
)
)
-
- skipOtherLoadings = True
+ next_signal_pair_id += 1
+ skip_other_loadings = True
continue
# find M'-1 peak
- isoM_pXm1 = labScan.findMZ(
- curPeakmz + (xCount - 1) * cValidationOffset,
+ isoMP_m1_index = labScan.findMZ(
+ isoM_mz + (current_Cn - 1) * c13c12_offset_charged,
ppm,
)
- isoM_pXm1 = labScan.getMostIntensePeak(
- isoM_pXm1[0],
- isoM_pXm1[1],
+ isoMP_m1_index = labScan.getMostIntensePeak(
+ isoMP_m1_index[0],
+ isoMP_m1_index[1],
)
- normRatioL = purityLArray[xCount][1]
- normRatioN = purityNArray[xCount][1]
+ theoretical_ratio_native = theoretical_ratios_native[current_Cn][1]
+ theoretical_ratio_labeled = theoretical_ratios_labeled[current_Cn][1]
# 2. check if the observed M'-1/M' ratio fits the theoretical one
if peakCountRight == 1:
pass
- elif isoM_pXm1 == -1:
- if lowAbundanceIsotopeCutoff and labPeakIntensity * normRatioL <= isotopologIntensityThres:
+ elif isoMP_m1_index == -1:
+ if lowAbundanceIsotopeCutoff and isoMP_int * theoretical_ratio_labeled <= isotopologIntensityThres:
pass
else:
continue
- elif isoM_pXm1 != -1:
- isoM_pXm1_Intensity = labScan.intensity_list[isoM_pXm1]
- observedRatioMp = isoM_pXm1_Intensity / labPeakIntensity
- if abs(normRatioL - observedRatioMp) <= intensityErrorL:
+ elif isoMP_m1_index != -1:
+ isoMP_m1_int = labScan.intensity_list[isoMP_m1_index]
+ observed_ratio = isoMP_m1_int / isoMP_int
+ if abs(theoretical_ratio_labeled - observed_ratio) <= intensityErrorL:
pass
- elif lowAbundanceIsotopeCutoff and isoM_pXm1_Intensity <= isotopologIntensityThres:
+ elif lowAbundanceIsotopeCutoff and isoMP_m1_int <= isotopologIntensityThres:
pass
else:
continue
@@ -587,17 +596,17 @@ def matchPartners(
# 3. check if the observed M+1/M ratio fits the theoretical one (native, always in curScan)
if peakCountLeft == 1:
pass
- elif isoM_p1 == -1:
- if lowAbundanceIsotopeCutoff and curPeakIntensity * (normRatioN + adjRatio) <= isotopologIntensityThres:
+ elif isoM_p1_index == -1:
+ if lowAbundanceIsotopeCutoff and isoM_int * (theoretical_ratio_native + obs_ratio_MPp1_to_MP) <= isotopologIntensityThres:
pass
else:
continue
- elif isoM_p1 != -1:
- isoM_p1_Intensity = curScan.intensity_list[isoM_p1]
- observedRatioM = isoM_p1_Intensity / curPeakIntensity
- if abs((normRatioN + adjRatio) - observedRatioM) <= intensityErrorN:
+ elif isoM_p1_index != -1:
+ isoM_p1_int = cur_scan.intensity_list[isoM_p1_index]
+ observed_ratio = isoM_p1_int / isoM_int
+ if abs(theoretical_ratio_native + obs_ratio_MPp1_to_MP - observed_ratio) <= intensityErrorN:
pass
- elif lowAbundanceIsotopeCutoff and isoM_p1_Intensity <= isotopologIntensityThres:
+ elif lowAbundanceIsotopeCutoff and isoM_p1_int <= isotopologIntensityThres:
pass
else:
continue
@@ -606,23 +615,24 @@ def matchPartners(
# All verification criteria are passed, store the ion signal pair
# for further processing
- curPeakDetectedIonPairs.append(
+ cur_signal_detected_partners.append(
mzFeature(
- id=len(curPeakDetectedIonPairs),
- mz=curPeakmz,
- lmz=labPeakmz,
- tmz=xCount * cValidationOffset,
- xCount=xCount,
- scanIndex=curScanIndex,
- scanid=curScan.id,
- scantime=curScan.retention_time,
- loading=curLoading,
- nIntensity=curPeakIntensity,
- lIntensity=labPeakIntensity,
+ id=next_signal_pair_id,
+ mz=isoM_mz,
+ lmz=isoMP_mz,
+ tmz=isoMP_mz - isoM_mz,
+ xCount=current_Cn,
+ scanIndex=cur_native_scan_index,
+ scanid=cur_scan.id,
+ scantime=cur_scan.retention_time,
+ loading=current_charge,
+ nIntensity=isoM_int,
+ lIntensity=isoMP_int,
ionMode=ionMode,
)
)
- skipOtherLoadings = True
+ next_signal_pair_id += 1
+ skip_other_loadings = True
# endregion
# labeling patterns derived from one or more labeling-elements (e.g. 13C and D)
@@ -635,65 +645,65 @@ def matchPartners(
# NOTE: The option must be activated and the other two options must be deactivated
if useDoubleLabelingCombinations:
# find M+1 peak
- isoM_p1 = curScan.findMZ(
- curPeakmz + cValidationOffset / curLoading,
+ isoM_p1_index = cur_scan.findMZ(
+ isoM_mz + c13c12_offset / current_charge,
ppm,
start=currentPeakIndex,
)
- isoM_p1 = curScan.getMostIntensePeak(isoM_p1[0], isoM_p1[1])
+ isoM_p1_index = cur_scan.getMostIntensePeak(isoM_p1_index[0], isoM_p1_index[1])
intIsoM_p1 = 0
- if isoM_p1 != -1:
- intIsoM_p1 = curScan.intensity_list[isoM_p1]
+ if isoM_p1_index != -1:
+ intIsoM_p1 = cur_scan.intensity_list[isoM_p1_index]
- isoM_m1 = curScan.findMZ(
- curPeakmz - cValidationOffset / curLoading,
+ isoM_m1_index = cur_scan.findMZ(
+ isoM_mz - c13c12_offset / current_charge,
ppm,
start=currentPeakIndex,
)
- isoM_m1 = curScan.getMostIntensePeak(isoM_m1[0], isoM_m1[1])
+ isoM_m1_index = cur_scan.getMostIntensePeak(isoM_m1_index[0], isoM_m1_index[1])
intIsoM_m1 = 0
- if isoM_m1 != -1:
- intIsoM_m1 = curScan.intensity_list[isoM_m1]
+ if isoM_m1_index != -1:
+ intIsoM_m1 = cur_scan.intensity_list[isoM_m1_index]
- if intIsoM_p1 < curPeakIntensity and intIsoM_m1 < curPeakIntensity and (isoM_p1 != -1 or peakCountLeft == 1 or lowAbundanceIsotopeCutoff):
+ if intIsoM_p1 < isoM_int and intIsoM_m1 < isoM_int and (isoM_p1_index != -1 or peakCountLeft == 1 or lowAbundanceIsotopeCutoff):
# test certain number of labelled carbon atoms
for comb in combs:
# find corresponding M' peak (search in labScan, which may be offset)
- isoM_pX = labScan.findMZ(
- curPeakmz + comb.mzdelta / curLoading,
+ isoMP_index = labScan.findMZ(
+ isoM_mz + comb.mzdelta / current_charge,
ppm,
)
- isoM_pX = labScan.getMostIntensePeak(
- isoM_pX[0],
- isoM_pX[1],
+ isoMP_index = labScan.getMostIntensePeak(
+ isoMP_index[0],
+ isoMP_index[1],
intensityThres,
)
- if isoM_pX != -1:
- labPeakmz = labScan.mz_list[isoM_pX]
- labPeakIntensity = labScan.intensity_list[isoM_pX]
+ if isoMP_index != -1:
+ isoMP_mz = labScan.mz_list[isoMP_index]
+ isoMP_int = labScan.intensity_list[isoMP_index]
# (1.) check if M' and M ratio are as expected
if False:
- rat = curPeakIntensity / labPeakIntensity
+ rat = isoM_int / isoMP_int
if minRatio <= rat <= maxRatio:
pass ## ratio check passed
else:
continue ## ratio check not passed
# find M'-1 peak
- isoM_pXm1 = labScan._findMZGeneric(
- curPeakmz + (comb.mzdelta - 1.00628 * (1.0 + curPeakmz * ppm / 1000000)) / curLoading,
- curPeakmz + (comb.mzdelta - 1.00335 * (1.0 - curPeakmz * ppm / 1000000)) / curLoading,
+ isoMP_m1_index = labScan._findMZGeneric(
+ isoM_mz + (comb.mzdelta - 1.00628 * (1.0 + isoM_mz * ppm / 1000000)) / current_charge,
+ isoM_mz + (comb.mzdelta - 1.00335 * (1.0 - isoM_mz * ppm / 1000000)) / current_charge,
)
- isoM_pXm1 = labScan.getMostIntensePeak(
- isoM_pXm1[0],
- isoM_pXm1[1],
+ isoMP_m1_index = labScan.getMostIntensePeak(
+ isoMP_m1_index[0],
+ isoMP_m1_index[1],
)
- if isoM_pXm1 != -1:
- isoPeakIntensity = labScan.intensity_list[isoM_pXm1]
- rat = isoPeakIntensity / labPeakIntensity
+ if isoMP_m1_index != -1:
+ isoPeakIntensity = labScan.intensity_list[isoMP_m1_index]
+ rat = isoPeakIntensity / isoMP_int
if rat <= 0.75:
pass
@@ -701,18 +711,18 @@ def matchPartners(
continue
# find M'+1 peak
- isoM_pXp1 = labScan._findMZGeneric(
- curPeakmz + (comb.mzdelta + 1.00335 * (1.0 - curPeakmz * ppm / 1000000)) / curLoading,
- curPeakmz + (comb.mzdelta + 1.00628 * (1.0 + curPeakmz * ppm / 1000000)) / curLoading,
+ isoMP_p1_index = labScan._findMZGeneric(
+ isoM_mz + (comb.mzdelta + 1.00335 * (1.0 - isoM_mz * ppm / 1000000)) / current_charge,
+ isoM_mz + (comb.mzdelta + 1.00628 * (1.0 + isoM_mz * ppm / 1000000)) / current_charge,
)
- isoM_pXp1 = labScan.getMostIntensePeak(
- isoM_pXp1[0],
- isoM_pXp1[1],
+ isoMP_p1_index = labScan.getMostIntensePeak(
+ isoMP_p1_index[0],
+ isoMP_p1_index[1],
)
- if isoM_pXp1 != -1:
- isoPeakIntensity = labScan.intensity_list[isoM_pXp1]
- rat = isoPeakIntensity / labPeakIntensity
+ if isoMP_p1_index != -1:
+ isoPeakIntensity = labScan.intensity_list[isoMP_p1_index]
+ rat = isoPeakIntensity / isoMP_int
if rat <= 0.9:
pass
@@ -721,65 +731,33 @@ def matchPartners(
# All verification criteria are passed, store the ion signal pair
# for further processing
- curPeakDetectedIonPairs.append(
+ cur_signal_detected_partners.append(
mzFeature(
- id=len(curPeakDetectedIonPairs),
- mz=curPeakmz,
- lmz=labScan.mz_list[isoM_pX],
- tmz=comb.mzdelta / curLoading,
+ id=len(cur_signal_detected_partners),
+ mz=isoM_mz,
+ lmz=labScan.mz_list[isoMP_index],
+ tmz=comb.mzdelta / current_charge,
xCount=fT.flatToString(comb.atoms),
- scanIndex=curScanIndex,
- scanid=curScan.id,
- scantime=curScan.retention_time,
- loading=curLoading,
- nIntensity=curPeakIntensity,
- lIntensity=labPeakIntensity,
+ scanIndex=cur_native_scan_index,
+ scanid=cur_scan.id,
+ scantime=cur_scan.retention_time,
+ loading=current_charge,
+ nIntensity=isoM_int,
+ lIntensity=isoMP_int,
ionMode=ionMode,
)
)
- skipOtherLoadings = True
+ skip_other_loadings = True
# endregion
- if False: ## select best fit
- if len(curPeakDetectedIonPairs) > 0:
- bestFit = None
- bestFitPPMDiff = 1000000000
-
- ## TODO select best fit based on isotopic pattern (e.g. intensity)
-
- for mt in curPeakDetectedIonPairs:
- if abs(mt.lmz - mt.mz - mt.xCount * 1.00335) * 1000000.0 / mt.mz < bestFitPPMDiff:
- bestFit = mt
- bestFitPPMDiff = abs(mt.lmz - mt.mz - mt.xCount * 1.00335) * 1000000.0 / mt.mz
-
- curScanDetectedIonPairs.append(bestFit)
- else: ## use all peak pairs
- curScanDetectedIonPairs.extend(curPeakDetectedIonPairs)
-
- if len(curScanDetectedIonPairs) > 0 and False:
- from .utils import printObjectsAsTable
-
- print("\n")
- print(curScan.retention_time / 60.0)
- printObjectsAsTable(
- curScanDetectedIonPairs,
- attrs=[
- "mz",
- "xCount",
- "loading",
- "nIntensity",
- "lIntensity",
- "ionMode",
- ],
- )
+ cur_scan_detected_signal_pairs.extend(cur_signal_detected_partners)
- detectedIonPairs.extend(curScanDetectedIonPairs)
- curScanIndex += 1
+ detected_signal_pairs.extend(cur_scan_detected_signal_pairs)
+ cur_native_scan_index += 1
except Exception:
- import traceback
-
+ print(f"Error in findIsoPairs_matchPartners for scan {cur_scan.id} (index {cur_native_scan_index}):")
traceback.print_exc()
- return detectedIonPairs
+ return detected_signal_pairs
diff --git a/src/mePyGuis/PeakPickingSettingsDialog.py b/src/mePyGuis/PeakPickingSettingsDialog.py
index ca82f57..2c83fc2 100644
--- a/src/mePyGuis/PeakPickingSettingsDialog.py
+++ b/src/mePyGuis/PeakPickingSettingsDialog.py
@@ -177,10 +177,13 @@ def _build_ui(self):
self.btnAddEic.setToolTip("Add a new EIC definition row")
self.btnRemoveEic = QtWidgets.QPushButton("Remove Selected")
self.btnRemoveEic.setToolTip("Remove selected EIC definition(s)")
+ self.btnDuplicateEic = QtWidgets.QPushButton("Duplicate Selected")
+ self.btnDuplicateEic.setToolTip("Duplicate the selected EIC definition row")
self.btnPickEic = QtWidgets.QPushButton("Pick Peaks")
self.btnPickEic.setToolTip("Run peak picking on defined EICs and show results below")
eic_btn_layout.addWidget(self.btnAddEic)
eic_btn_layout.addWidget(self.btnRemoveEic)
+ eic_btn_layout.addWidget(self.btnDuplicateEic)
eic_btn_layout.addStretch()
eic_btn_layout.addWidget(self.btnPickEic)
eic_layout.addLayout(eic_btn_layout)
@@ -222,12 +225,13 @@ def _build_ui(self):
self.btnDefaults.clicked.connect(self._restore_defaults)
self.btnAddEic.clicked.connect(self._add_eic_row)
self.btnRemoveEic.clicked.connect(self._remove_eic_rows)
+ self.btnDuplicateEic.clicked.connect(self._duplicate_eic_row)
self.btnPickEic.clicked.connect(self._run_preview)
def _resize_eic_columns(self, event):
- """Keep EIC table column widths at 60/20/10/10 ratio."""
+ """Keep EIC table column widths at 45/25/15/15 ratio (File column 25% narrower)."""
total = self.eicTable.viewport().width()
- ratios = [0.60, 0.20, 0.10, 0.10]
+ ratios = [0.45, 0.25, 0.15, 0.15]
hdr = self.eicTable.horizontalHeader()
for col, ratio in enumerate(ratios):
hdr.resizeSection(col, max(1, int(total * ratio)))
@@ -1019,6 +1023,72 @@ def _remove_eic_rows(self):
for r in rows:
self.eicTable.removeRow(r)
+ def _duplicate_eic_row(self):
+ """Duplicate the selected EIC row, inserting a copy directly below it."""
+ selected_rows = sorted({idx.row() for idx in self.eicTable.selectedIndexes()})
+ if not selected_rows:
+ return
+ # Duplicate the last selected row (insert after it)
+ src_row = selected_rows[-1]
+ insert_at = src_row + 1
+
+ # Read current widget values from the source row
+ fileW = self.eicTable.cellWidget(src_row, 0)
+ filterW = self.eicTable.cellWidget(src_row, 1)
+ mzW = self.eicTable.cellWidget(src_row, 2)
+ ppmW = self.eicTable.cellWidget(src_row, 3)
+
+ src_file_idx = fileW.currentIndex() if fileW else 0
+ src_filter_idx = filterW.currentIndex() if filterW else 0
+ src_mz = mzW.value() if mzW else 100.0
+ src_ppm = ppmW.value() if ppmW else 5.0
+
+ # Insert new row at position
+ self.eicTable.insertRow(insert_at)
+
+ # File combo
+ newFileCombo = QtWidgets.QComboBox()
+ if self._sample_files:
+ for group_name, fpath in self._sample_files:
+ fname = os.path.basename(fpath)
+ newFileCombo.addItem(f"{group_name} \u2014 {fname}", userData=fpath)
+ else:
+ newFileCombo.addItem("(no files loaded)", userData=None)
+ newFileCombo.setCurrentIndex(src_file_idx)
+ self.eicTable.setCellWidget(insert_at, 0, newFileCombo)
+
+ # Filter combo
+ newFilterCombo = QtWidgets.QComboBox()
+ all_filter_lines = []
+ for pol_key in ("+", "-"):
+ for fl in self._scan_events.get(pol_key, []):
+ if fl and fl != "None":
+ all_filter_lines.append(fl)
+ if all_filter_lines:
+ for fl in all_filter_lines:
+ newFilterCombo.addItem(fl)
+ else:
+ newFilterCombo.addItem("(no filter lines)")
+ newFilterCombo.setCurrentIndex(src_filter_idx)
+ self.eicTable.setCellWidget(insert_at, 1, newFilterCombo)
+
+ # m/z
+ newMzSpin = QtWidgets.QDoubleSpinBox()
+ newMzSpin.setRange(0, 99999.0)
+ newMzSpin.setDecimals(5)
+ newMzSpin.setValue(src_mz)
+ self.eicTable.setCellWidget(insert_at, 2, newMzSpin)
+
+ # ppm
+ newPpmSpin = QtWidgets.QDoubleSpinBox()
+ newPpmSpin.setRange(0.1, 1000.0)
+ newPpmSpin.setDecimals(1)
+ newPpmSpin.setValue(src_ppm)
+ self.eicTable.setCellWidget(insert_at, 3, newPpmSpin)
+
+ # Select the duplicated row
+ self.eicTable.selectRow(insert_at)
+
def _run_preview(self):
"""Attempt to load EICs and run peak picking for preview."""
# Gather EIC definitions
diff --git a/src/mePyGuis/ResultsSummaryDialog.py b/src/mePyGuis/ResultsSummaryDialog.py
new file mode 100644
index 0000000..a8d6b6a
--- /dev/null
+++ b/src/mePyGuis/ResultsSummaryDialog.py
@@ -0,0 +1,278 @@
+"""
+Results Summary Dialog for MetExtract II.
+
+Replaces the scrollable text overview with two structured tables:
+- Table 1: Per-file rows (one row per file × ionMode), coloured by group, sortable.
+- Table 2: Bracketing/convoluted results summary.
+"""
+
+from PySide6 import QtCore, QtGui, QtWidgets
+
+
+class ResultsSummaryDialog(QtWidgets.QDialog):
+ """Dialog showing a tabular overview of MetExtract II processing results."""
+
+ def __init__(self, parent=None, file_rows=None, summary_data=None):
+ """
+ Parameters
+ ----------
+ file_rows : list[dict]
+ Each dict has keys:
+ group_name, group_color, file, ion_mode,
+ n_mzs, n_mz_bins, n_features, n_metabolites,
+ mz_delta_mean, mz_delta_std,
+ avg_ratio_signals, avg_ratio_signals_std,
+ avg_ratio_area, avg_ratio_abundance,
+ avg_enrichment, avg_enrichment_std,
+ error (str or None)
+ summary_data : dict or None
+ Keys:
+ n_features, n_metabolites,
+ features_1, features_2, features_3, features_4, features_5,
+ features_5_to_10, features_10_to_20, features_gt20,
+ pos_only, neg_only, both_modes,
+ omitted_features (int or None)
+ """
+ super().__init__(parent)
+ self.setWindowTitle("Processing Results Overview")
+ self.resize(1300, 700)
+
+ self._file_rows = file_rows or []
+ self._summary_data = summary_data or {}
+
+ self._build_ui()
+ self._populate_file_table()
+ self._populate_summary_table()
+
+ # ------------------------------------------------------------------
+ # UI construction
+ # ------------------------------------------------------------------
+
+ def _build_ui(self):
+ root = QtWidgets.QVBoxLayout(self)
+
+ # ── Note label ────────────────────────────────────────────────
+ note = QtWidgets.QLabel("Note: All values are based on the last data processing run and are influenced by the chosen parameters. Use only as a quick overview.")
+ note.setWordWrap(True)
+ root.addWidget(note)
+
+ splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
+ root.addWidget(splitter, stretch=1)
+
+ # ── Table 1: per-file results ──────────────────────────────────
+ top_group = QtWidgets.QGroupBox("Results of individual files")
+ top_layout = QtWidgets.QVBoxLayout(top_group)
+ top_layout.setContentsMargins(4, 4, 4, 4)
+
+ self.fileTable = QtWidgets.QTableWidget(0, 12)
+ self.fileTable.setHorizontalHeaderLabels(
+ [
+ "Group",
+ "File",
+ "Ion mode",
+ "Signal pairs",
+ "MZ bins",
+ "Feature pairs",
+ "Metabolites",
+ "m/z Δ (ppm mean)",
+ "m/z Δ (ppm std)",
+ "M:M' ratio (signals)",
+ "M:M' ratio (area / abund.)",
+ "L-Enrichment (%)",
+ ]
+ )
+ self.fileTable.setSortingEnabled(True)
+ self.fileTable.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+ self.fileTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
+ self.fileTable.horizontalHeader().setStretchLastSection(True)
+ self.fileTable.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Interactive)
+ self.fileTable.verticalHeader().setVisible(False)
+ top_layout.addWidget(self.fileTable)
+ splitter.addWidget(top_group)
+
+ # ── Table 2: convoluted / bracketing summary ───────────────────
+ bottom_group = QtWidgets.QGroupBox("Bracketing / convoluted results summary")
+ bottom_layout = QtWidgets.QVBoxLayout(bottom_group)
+ bottom_layout.setContentsMargins(4, 4, 4, 4)
+
+ self.summaryTable = QtWidgets.QTableWidget(0, 2)
+ self.summaryTable.setHorizontalHeaderLabels(["Metric", "Value"])
+ self.summaryTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
+ self.summaryTable.horizontalHeader().setStretchLastSection(True)
+ self.summaryTable.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
+ self.summaryTable.verticalHeader().setVisible(False)
+ bottom_layout.addWidget(self.summaryTable)
+ splitter.addWidget(bottom_group)
+
+ splitter.setStretchFactor(0, 2)
+ splitter.setStretchFactor(1, 1)
+
+ # ── Close button ───────────────────────────────────────────────
+ btn_close = QtWidgets.QPushButton("Close")
+ btn_close.clicked.connect(self.accept)
+ btn_row = QtWidgets.QHBoxLayout()
+ btn_row.addStretch()
+ btn_row.addWidget(btn_close)
+ root.addLayout(btn_row)
+
+ # ------------------------------------------------------------------
+ # Population helpers
+ # ------------------------------------------------------------------
+
+ def _make_item(self, text, align=QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter):
+ item = QtWidgets.QTableWidgetItem(str(text))
+ item.setTextAlignment(align)
+ item.setFlags(item.flags() & ~QtCore.Qt.ItemIsEditable)
+ return item
+
+ def _make_num_item(self, value):
+ """Create a table item that sorts numerically."""
+ if value is None:
+ item = QtWidgets.QTableWidgetItem("")
+ else:
+ item = QtWidgets.QTableWidgetItem()
+ item.setData(QtCore.Qt.DisplayRole, value)
+ item.setTextAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+ item.setFlags(item.flags() & ~QtCore.Qt.ItemIsEditable)
+ return item
+
+ def _populate_file_table(self):
+ self.fileTable.setSortingEnabled(False)
+ self.fileTable.setRowCount(len(self._file_rows))
+
+ # Track group → background colour so adjacent rows of the same group share the same tint
+ _group_colors = {}
+
+ for row_idx, row in enumerate(self._file_rows):
+ group_name = row.get("group_name", "")
+ group_color = row.get("group_color", "")
+
+ # Resolve background colour for this group row
+ bg_color = None
+ if group_color:
+ try:
+ qc = QtGui.QColor(group_color)
+ if qc.isValid():
+ # Use a lighter tint so text remains legible
+ qc.setAlpha(60)
+ bg_color = qc
+ except Exception:
+ pass
+
+ if bg_color is None and group_name:
+ # Assign a deterministic pastel from a fixed palette
+ if group_name not in _group_colors:
+ _pastel = [
+ QtGui.QColor(200, 220, 255, 80),
+ QtGui.QColor(200, 255, 220, 80),
+ QtGui.QColor(255, 220, 200, 80),
+ QtGui.QColor(240, 200, 255, 80),
+ QtGui.QColor(255, 255, 200, 80),
+ QtGui.QColor(200, 255, 255, 80),
+ ]
+ _group_colors[group_name] = _pastel[len(_group_colors) % len(_pastel)]
+ bg_color = _group_colors[group_name]
+
+ error = row.get("error")
+
+ cells = [
+ self._make_item(group_name),
+ self._make_item(row.get("file", "")),
+ self._make_item(row.get("ion_mode", "")),
+ ]
+
+ if error:
+ cells.append(self._make_item(f"Error: {error}"))
+ # Fill remaining columns with empty items
+ while len(cells) < 12:
+ cells.append(self._make_item(""))
+ else:
+ n_mzs = row.get("n_mzs")
+ n_mz_bins = row.get("n_mz_bins")
+ n_features = row.get("n_features")
+ n_metabolites = row.get("n_metabolites")
+ mz_mean = row.get("mz_delta_mean")
+ mz_std = row.get("mz_delta_std")
+ ratio_sig = row.get("avg_ratio_signals")
+ ratio_sig_std = row.get("avg_ratio_signals_std")
+ ratio_area = row.get("avg_ratio_area")
+ ratio_abund = row.get("avg_ratio_abundance")
+ enr = row.get("avg_enrichment")
+ enr_std = row.get("avg_enrichment_std")
+
+ cells.append(self._make_num_item(n_mzs))
+ cells.append(self._make_num_item(n_mz_bins))
+ cells.append(self._make_num_item(n_features))
+ cells.append(self._make_num_item(n_metabolites))
+ cells.append(self._make_item("%.2f" % mz_mean if mz_mean is not None else ""))
+ cells.append(self._make_item("%.2f" % mz_std if mz_std is not None else ""))
+ cells.append(self._make_item("%.2f ± %.2f" % (ratio_sig, ratio_sig_std) if ratio_sig is not None else ""))
+ cells.append(
+ self._make_item(
+ "%s / %s"
+ % (
+ "%.2f" % ratio_area if ratio_area is not None else "-",
+ "%.2f" % ratio_abund if ratio_abund is not None else "-",
+ )
+ )
+ )
+ cells.append(self._make_item("%.2f ± %.2f" % (100 * enr, 100 * enr_std) if enr is not None and enr_std is not None else ("%.2f" % (100 * enr) if enr is not None else "")))
+
+ for col_idx, cell in enumerate(cells):
+ if bg_color is not None:
+ cell.setBackground(QtGui.QBrush(bg_color))
+ self.fileTable.setItem(row_idx, col_idx, cell)
+
+ self.fileTable.setSortingEnabled(True)
+ self.fileTable.resizeColumnsToContents()
+
+ def _add_summary_row(self, label, value):
+ row = self.summaryTable.rowCount()
+ self.summaryTable.insertRow(row)
+ self.summaryTable.setItem(row, 0, self._make_item(label))
+ self.summaryTable.setItem(row, 1, self._make_num_item(value) if isinstance(value, (int, float)) else self._make_item(str(value) if value is not None else ""))
+
+ def _add_summary_section(self, title):
+ """Add a bold header row."""
+ row = self.summaryTable.rowCount()
+ self.summaryTable.insertRow(row)
+ header_item = QtWidgets.QTableWidgetItem(title)
+ header_item.setFlags(header_item.flags() & ~QtCore.Qt.ItemIsEditable)
+ font = header_item.font()
+ font.setBold(True)
+ header_item.setFont(font)
+ header_item.setBackground(QtGui.QBrush(QtGui.QColor(230, 230, 230)))
+ self.summaryTable.setItem(row, 0, header_item)
+ empty = QtWidgets.QTableWidgetItem("")
+ empty.setBackground(QtGui.QBrush(QtGui.QColor(230, 230, 230)))
+ empty.setFlags(empty.flags() & ~QtCore.Qt.ItemIsEditable)
+ self.summaryTable.setItem(row, 1, empty)
+
+ def _populate_summary_table(self):
+ d = self._summary_data
+ if not d:
+ self._add_summary_row("No bracketing results available", "")
+ return
+
+ self._add_summary_section("Overall")
+ self._add_summary_row("Feature pairs", d.get("n_features", ""))
+ self._add_summary_row("Metabolites (OGroups)", d.get("n_metabolites", ""))
+ if d.get("omitted_features") is not None:
+ self._add_summary_row("Omitted feature pairs", d.get("omitted_features", ""))
+
+ self._add_summary_section("Metabolites by number of feature pairs")
+ self._add_summary_row("1 feature pair", d.get("features_1", ""))
+ self._add_summary_row("2 feature pairs", d.get("features_2", ""))
+ self._add_summary_row("3 feature pairs", d.get("features_3", ""))
+ self._add_summary_row("4 feature pairs", d.get("features_4", ""))
+ self._add_summary_row("5 feature pairs", d.get("features_5", ""))
+ self._add_summary_row(">5 and ≤10 feature pairs", d.get("features_5_to_10", ""))
+ self._add_summary_row(">10 and ≤20 feature pairs", d.get("features_10_to_20", ""))
+ self._add_summary_row(">20 feature pairs", d.get("features_gt20", ""))
+
+ self._add_summary_section("Metabolites by ionization mode")
+ self._add_summary_row("Positive mode only", d.get("pos_only", ""))
+ self._add_summary_row("Negative mode only", d.get("neg_only", ""))
+ self._add_summary_row("Both modes", d.get("both_modes", ""))
+
+ self.summaryTable.resizeColumnsToContents()
diff --git a/src/mePyGuis/guis/FE_mainWindow.ui b/src/mePyGuis/guis/FE_mainWindow.ui
deleted file mode 100644
index a8b612f..0000000
--- a/src/mePyGuis/guis/FE_mainWindow.ui
+++ /dev/null
@@ -1,1186 +0,0 @@
-
-
- MainWindow
-
-
-
- 0
- 0
- 1391
- 840
-
-
-
- true
-
-
- MainWindow
-
-
- QTabWidget::Triangular
-
-
-
- -
-
-
- true
-
-
- QTabWidget::West
-
-
- 1
-
-
-
- Input
-
-
-
-
-
-
-
-
-
-
-
-
-
- 200
- 0
-
-
-
- color: rgb(90, 90, 90);
-font: 7pt;
-
-
- <html><head/><body><p align="justify">Please specify the performed LC-HRMSMS mesurements of M and M'. Specify each target in single row using the button 'Add MS/MS target(s)'. Load an LC-HRMSMS measurement multiple times if it contains more than one target. Select the appropriate scan events for the native and the labeled metabolite ions.</p></body></html>
-
-
- true
-
-
-
- -
-
-
- font: 10pt;
-
-
- <html><head/><body><p align="right">Define MS/MS targets</p></body></html>
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Maximum
-
-
-
- 0
- 5
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
-
-
- -
-
-
-
-
-
- Save compilation
-
-
-
- -
-
-
- Load compilation
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Add MS/MS target(s)
-
-
-
- -
-
-
- Delete current MS/MS target
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Preferred
-
-
-
- 3
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Maximum
-
-
-
- 3
- 0
-
-
-
-
- -
-
-
- <html><head/><body><p align="right">Define/Edit group</p></body></html>
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
-
-
-
- false
-
-
- QAbstractItemView::MultiSelection
-
-
- QAbstractItemView::SelectRows
-
-
-
- -
-
-
-
-
-
- Configure adducts
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Process
-
-
- -
-
-
-
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Preferred
-
-
-
- 3
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Preferred
-
-
-
- 3
- 0
-
-
-
-
- -
-
-
-
-
-
- Qt::Vertical
-
-
- QSizePolicy::Maximum
-
-
-
- 0
- 5
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- font: 10pt;
-
-
- <html><head/><body><p align="right">Processing settings</p></body></html>
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 200
- 0
-
-
-
- color: rgb(90, 90, 90);
-font: 7pt;
-
-
- <html><head/><body><p>Please specify the settings for processing the defined MS/MS targets. First, the two MS/MS spectra will be insepcted for corresponding native and U-<span style=" vertical-align:super;">13</span>C-labeled peaks. These peaks will be annotated with putative sum formulas using the determined number of carbon atoms. </p></body></html>
-
-
- true
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Preferred
-
-
-
- 20
- 40
-
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
-
-
-
- font: 10pt;
-
-
- <html><head/><body><p align="right">Run tasks</p></body></html>
-
-
-
-
-
- -
-
-
-
-
-
- CPU cores
-
-
-
- -
-
-
- 1
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Keep one core unused
-
-
-
- -
-
-
- Start
-
-
-
-
-
- -
-
-
-
-
-
- Scan selection
-
-
-
-
-
-
- Full scan EIC ppm
-
-
-
- -
-
-
- 5.000000000000000
-
-
-
- -
-
-
- Intensity threshold
-
-
-
- -
-
-
- 0
-
-
- 999999999.000000000000000
-
-
- 10000.000000000000000
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Preferred
-
-
-
- 0
- 0
-
-
-
-
-
-
-
- -
-
-
- Peak matching
-
-
-
-
-
-
- Matching max. PPM error
-
-
-
- -
-
-
- Max. relative intensity error
-
-
-
- -
-
-
- 9999.000000000000000
-
-
- 30.000000000000000
-
-
-
- -
-
-
- 100.000000000000000
-
-
- 20.000000000000000
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Preferred
-
-
-
- 0
- 0
-
-
-
-
-
-
-
- -
-
-
-
-
-
- Scan pre-processing
-
-
-
-
-
-
- Min. scaled peak intensity [%]
-
-
-
- -
-
-
- 1
-
-
- 0.000000000000000
-
-
- 0.500000000000000
-
-
-
- -
-
-
- false
-
-
-
- 0
- 0
-
-
-
- Scale to precursor mz
-
-
- true
-
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Ignored
-
-
-
- 0
- 20
-
-
-
-
-
-
- -
-
-
- Fragment annotation
-
-
-
-
-
-
-
-
-
- Used elements
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::MinimumExpanding
-
-
-
- 0
- 0
-
-
-
-
-
-
- -
-
-
- Max. ppm error
-
-
-
- -
-
-
- Allow additional Cn (e.g. biotransformation products)
-
-
-
- -
-
-
-
-
-
- 1
-
-
- 1000.000000000000000
-
-
- 5.000000000000000
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::MinimumExpanding
-
-
-
- 0
- 0
-
-
-
-
-
-
- -
-
-
-
-
-
- Min. number of labeling atoms
-
-
-
- -
-
-
-
-
- -
-
-
- Use zero labeling atoms (for biotransformation products)
-
-
-
-
-
-
- -
-
-
- Parent annotation
-
-
-
-
-
-
- Apply parent-fragment consistency rule
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Preferred
-
-
-
- 20
- 40
-
-
-
-
-
-
-
- -
-
-
- Save results
-
-
-
-
-
-
- Save as TSV
-
-
- true
-
-
-
- -
-
-
- Save as PDF
-
-
-
- -
-
-
- Export as SIRIUS 4.0 MS format
-
-
-
-
-
-
- -
-
-
- MassBank search
-
-
-
-
-
-
- 1
-
-
- 1.000000000000000
-
-
- 5.000000000000000
-
-
-
- -
-
-
- 0
-
-
- 1.000000000000000
-
-
-
- -
-
-
- m/z error (ppm)
-
-
-
- -
-
-
- Minimal relative abundance
-
-
-
- -
-
-
- Hits to load
-
-
-
- -
-
-
- 999
-
-
- 10
-
-
-
- -
-
-
- Minimal score
-
-
-
- -
-
-
- 1.000000000000000
-
-
- 0.010000000000000
-
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Qt::Vertical
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Expanding
-
-
-
- 40
- 20
-
-
-
-
-
-
-
-
- Sample results
-
-
- -
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Processed file
-
-
-
- -
-
-
-
- 150
- 0
-
-
-
-
- -
-
-
- Open externally
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- Target name
-
-
-
-
- MZ
-
-
-
-
- Cn
-
-
-
-
- Sum formula
-
-
-
-
- Charge
-
-
-
-
- Scan num native isotopolog
-
-
-
-
- Scan num labeled isotopolog
-
-
-
-
- Full scan event
-
-
-
-
- MS2 scan event native
-
-
-
-
- MS2 scan event labeled isotopolog
-
-
-
-
-
-
- -
-
-
-
-
-
- Plot labeled MS2-scan
-
-
- true
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 0
- 0
-
-
-
-
- -
-
-
- Plot cleaned scan(s)
-
-
- true
-
-
-
- -
-
-
- Plot native MS2-scan
-
-
- true
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 3
- 0
-
-
-
-
- -
-
-
-
- 1
- 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Load Settings
-
-
-
-
- Save Settings
-
-
-
-
- Exit
-
-
-
-
- Help
-
-
-
-
- About
-
-
-
-
-
-
diff --git a/src/mePyGuis/guis/FTICRwindow.ui b/src/mePyGuis/guis/FTICRwindow.ui
deleted file mode 100644
index 2e4a886..0000000
--- a/src/mePyGuis/guis/FTICRwindow.ui
+++ /dev/null
@@ -1,747 +0,0 @@
-
-
- MainWindow
-
-
-
- 0
- 0
- 1369
- 907
-
-
-
- true
-
-
- MetExtract II - FT-ICR module
-
-
-
-
-
-
- -
-
-
- 1
-
-
-
-
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
-
- 32
-
-
-
- Load samples (.tsv format)
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- <html><head/><body><p align="right">
-
-<span style=" font-size:18pt;">Load sample files</span></p>
-
-<p align="right">First load the sample files in the tab-separated-values (.tsv) format.<br>
-Two columns, one specifying the m/z value and one the observed intensity, must be present.<br>
-The respective columns can be specified later in a separate dialog.</p>
-
-</body></html>
-
-
-
- -
-
-
-
- 32
-
-
-
- Load samples (.mzXML format)
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- <html><head/><body><p align="right">
-
-<span style=" font-size:18pt;">Processing parameters</span></p>
-
-<p align="right">Please specify all necessary processing parameters for the loaded FT-ICR MS scans.</p>
-
-</body></html>
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Process samples
-
-
-
-
-
-
- Process
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Intensity threshold
-
-
-
- -
-
-
- Match ppm
-
-
-
- -
-
-
- 100000000000.000000000000000
-
-
- 1000000.000000000000000
-
-
-
- -
-
-
- 2.000000000000000
-
-
-
- -
-
-
- Bracketing ppm
-
-
-
- -
-
-
- M+1 and M'-1 must be present
-
-
- true
-
-
-
- -
-
-
- Calibrate with average MZ error based
-on generated, unique sum formulas
-
-
- true
-
-
-
- -
-
-
- 0.500000000000000
-
-
-
- -
-
-
- Labeling enrichment
-
-
-
-
-
-
- <sup>13</sup>C-labeled
-
-
-
- -
-
-
- Native
-
-
-
- -
-
-
- 4
-
-
- 0.001000000000000
-
-
- 0.989300000000000
-
-
-
- -
-
-
- 4
-
-
- 0.001000000000000
-
-
- 0.990000000000000
-
-
-
- -
-
-
- Maximum deviation +-
-
-
-
- -
-
-
- 0.150000000000000
-
-
-
-
-
-
- -
-
-
- Sum formula generation
-
-
-
-
-
-
- Elements for sum formula generation
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Min
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- CH
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Max
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- CH1000N10O100S10P10
-
-
-
-
-
-
- -
-
-
- Used adducts
-
-
-
-
-
-
- [M+K-2H]-
-
-
-
- -
-
-
- [M+Na-2H]-
-
-
-
- -
-
-
- [M+Cl]-
-
-
- true
-
-
-
- -
-
-
- [M-H]-
-
-
- true
-
-
-
- -
-
-
- [M+FA-H]-
-
-
-
- -
-
-
- [M+Br]-
-
-
-
-
-
-
- -
-
-
- Use Seven Golden Rules (Kind et al. 2007)
-
-
- true
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Annotation ppm
-
-
-
- -
-
-
- 0.500000000000000
-
-
-
- -
-
-
- Sumformula databases
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 0
- 40
-
-
-
-
- -
-
-
- Add database
-
-
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Carbon atoms to search for
-
-
-
- -
-
-
- 3-60
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Results
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
-
-
-
- -
-
-
-
-
-
- Loaded samples and results
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 0
- 0
-
-
-
- QAbstractItemView::MultiSelection
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Samples
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Results
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 350
- 0
-
-
-
- QAbstractItemView::DropOnly
-
-
-
- MZ
-
-
-
-
- Cn
-
-
-
-
- Sumformula
-
-
-
-
-
-
-
-
-
- -
-
-
- <html><head/><body><p align="right">
-
-<span style=" font-size:18pt;">Detected signal pairs</span></p>
-
-<p align="right">The data processing results can be quickly visualized here.</p>
-
-</body></html>
-
-
-
-
-
-
-
-
-
-
-
-
-
- Load samples
-
-
-
-
- Save results
-
-
-
-
- Exit
-
-
-
-
- Load results
-
-
-
-
- New processing
-
-
-
-
-
-
diff --git a/src/mePyGuis/guis/ModuleSelectionWindow.ui b/src/mePyGuis/guis/ModuleSelectionWindow.ui
deleted file mode 100644
index 8e689d3..0000000
--- a/src/mePyGuis/guis/ModuleSelectionWindow.ui
+++ /dev/null
@@ -1,442 +0,0 @@
-
-
- MainWindow
-
-
-
- 0
- 0
- 638
- 698
-
-
-
- MainWindow
-
-
-
- :/MEIcon/resources/MEIcon.ico:/MEIcon/resources/MEIcon.ico
-
-
- background-color: rgb(255, 255, 255);
-
-
-
- -
-
-
-
-
-
- Combine results
-(AllExtract and TracExtract)
-
-
-
-
- -
-
-
-
- 90
- 90
-
-
-
-
- 90
- 90
-
-
-
- background-image: url(:/TracExtract/resources/TracExtract.png);
-
-
-
-
-
- true
-
-
-
- -
-
-
-
- 90
- 90
-
-
-
-
- 90
- 90
-
-
-
- background-image: url(:/FragExtract/resources/FragExtract.png);
-
-
-
-
-
- true
-
-
-
- -
-
-
- FragExtract
-(LC-HRMS)
-
-
-
-
- -
-
-
- Documentation
-
-
-
- -
-
-
- QFrame::Sunken
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
-
-
- AllExtract
-(LC-HRMS)
-
-
-
- -
-
-
- QFrame::Sunken
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
- 90
- 90
-
-
-
-
- 90
- 90
-
-
-
- background-image: url(:/Documentation/resources/Documentation.png);
-
-
-
-
-
- true
-
-
-
- -
-
-
- TracExtract
-(LC-HRMS)
-
-
-
- -
-
-
-
- 90
- 90
-
-
-
-
- 90
- 90
-
-
-
- background-image: url(:/AllExtract/resources/AllExtract.png);
-
-
-
-
-
- true
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
- 90
- 90
-
-
-
- background-image: url(:/combineResults/resources/combineResults.png);
-
-
-
-
-
- true
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- QFrame::Sunken
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
- 90
- 90
-
-
-
- background-image: url(:/FTICRExtract/resources/FTICRExtract.png);
-
-
-
-
-
- true
-
-
-
- -
-
-
- FTICRExtract
-(FT-ICR-MS)
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
-
-
-
- Qt::Vertical
-
-
-
- 0
- 0
-
-
-
-
- -
-
-
- color: slategrey;
-
-
-
-
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Expanding
-
-
-
- 0
- 0
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 80
- 80
-
-
-
- image: url(:/MEIcon_Large/resources/MEIcon_Large.png);
-
-
-
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 0
- 0
-
-
-
-
-
-
-
-
-
-
-
- Calculate isotopic enrichment
-
-
-
-
-
-
-
-
diff --git a/src/mePyGuis/guis/ProcessingWizard.ui b/src/mePyGuis/guis/ProcessingWizard.ui
deleted file mode 100644
index c08e447..0000000
--- a/src/mePyGuis/guis/ProcessingWizard.ui
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
- Wizard
-
-
-
- 0
- 0
- 674
- 536
-
-
-
- Wizard
-
-
-
-
-
- QWizard::ModernStyle
-
-
-
-
-
-
-
-
- 10
-
-
- 10
-
-
- true
-
-
- true
-
-
- true
-
-
-
diff --git a/src/mePyGuis/guis/SettingsWizard.ui b/src/mePyGuis/guis/SettingsWizard.ui
deleted file mode 100644
index 13e2a8b..0000000
--- a/src/mePyGuis/guis/SettingsWizard.ui
+++ /dev/null
@@ -1,563 +0,0 @@
-
-
- Wizard
-
-
-
- 0
- 0
- 591
- 469
-
-
-
- Wizard
-
-
- QWizard::ModernStyle
-
-
- QWizard::HaveCustomButton1
-
-
-
- New SIL-Experiment
-
-
- -
-
-
- <html><head/><body><p>Define a new stable isotope labelling assisted metabolomics experiment</p><p>Two types of experiments are supported: </p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Full metabolome labelling experiment (FML)</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Tracer meabolisation study (TMS)</li></ul><p>In a FML experiment the entire metabolome of a biological system must be availalbe as a highly stable isotope enriched form (e.g.: A fungi that is grown on <span style=" vertical-align:super;">13</span>C-glucose). </p><p>In a TMS experiment only a certain substance is labelled. If the studies stubstance is exogenous to the biological system, it needs to be provided as a non-labelled and a highly isotope enriched form (e.g. detoxification of a <span style=" vertical-align:super;">13</span>C-toxin in a plant). If, however, the substance of interst is endogenous to the studied biological system, then the substance must be provided as a highly isotope enriched form but does not need to be provided as a non-labelled form if it is available in the biological system in high enough abundance (e.g. <span style=" vertical-align:super;">13</span>C<span style=" vertical-align:sub;">6</span> phenylalanine). In either case only those metabolites, which incorporate the tracer, will be extracted.</p><p>Currently only such substances, that result in a unique and well separated isotope pattern, are supported. Using <span style=" vertical-align:super;">13</span>C-glucose as a tracer, which is onyl partly incooperated into metabolites, is not supported</p></body></html>
-
-
- true
-
-
-
- -
-
-
-
-
-
- Experiment type:
-
-
-
- -
-
-
- Full Metabolome Labelling
-
-
- true
-
-
-
- -
-
-
- Tracer Mextabolisation
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
-
-
-
- Tracer Metabolisation Experiment
-
-
- -
-
-
-
-
-
- Configure Tracers
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
-
-
-
- Full Metabolome Labelling
-
-
- -
-
-
-
-
-
- GroupBox
-
-
-
-
-
-
- 5
-
-
- 1.000000000000000
-
-
- 0.995000000000000
-
-
-
- -
-
-
- Enrichment is
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- 13C
-
-
-
- -
-
-
- mass is
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
- -
-
-
- Natural isotope
-
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::MinimumExpanding
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- 5
-
-
- 1.000000000000000
-
-
- 0.010000000000000
-
-
- 0.990000000000000
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- 12C
-
-
- 6
-
-
-
- -
-
-
- mass is
-
-
-
- -
-
-
- Enrichment is
-
-
-
-
-
-
- -
-
-
- Use carbon validation
-
-
-
-
-
-
-
-
-
- M/Z Picking
-
-
- -
-
-
-
-
-
- Consider isotopologue abundance
-
-
-
- -
-
-
- Maximum charge
-
-
-
- -
-
-
- 1
-
-
- 999.000000000000000
-
-
- 37.000000000000000
-
-
-
- -
-
-
- <html><head/><body><p align="center"> ≥</p></body></html>
-
-
-
- -
-
-
- 1
-
-
- 4
-
-
- 2
-
-
-
- -
-
-
- 999999999
-
-
-
- -
-
-
- Start
-
-
-
- -
-
-
- Intensity cutoff (import)
-
-
-
- -
-
-
- 999999999
-
-
- 5000
-
-
-
- -
-
-
- End
-
-
-
- -
-
-
- Number of atoms
-
-
-
- -
-
-
- 3
-
-
-
- -
-
-
- 70
-
-
-
- -
-
-
- Scan range [min]
-
-
-
- -
-
-
- 1
-
-
-
- -
-
-
- 1
-
-
- 999.000000000000000
-
-
- 3.000000000000000
-
-
-
- -
-
-
- min:
-
-
-
- -
-
-
- max:
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- 1
-
-
- 3
-
-
- 2
-
-
-
- -
-
-
- 1
-
-
- 3
-
-
- 2
-
-
-
- -
-
-
- Isotopologue ratio error (±)
-
-
-
- -
-
-
-
-
-
-
- -
-
-
- Intensity threshold
-
-
-
- -
-
-
- Isotopic pattern count (M)
-
-
-
- -
-
-
- Isotopic pattern count (M')
-
-
-
- -
-
-
- Mass deviation (ppm)
-
-
-
- -
-
-
- <html><head/><body><p align="center">≤ </p></body></html>
-
-
-
- -
-
-
- TextLabel
-
-
-
- -
-
-
- TextLabel
-
-
-
- -
-
-
- 0.000000000000000
-
-
- 1.000000000000000
-
-
- 0.100000000000000
-
-
- 0.100000000000000
-
-
-
- -
-
-
- 0.000000000000000
-
-
- 1.000000000000000
-
-
- 0.100000000000000
-
-
- 0.100000000000000
-
-
-
-
-
-
-
-
-
- Chromatographic separation
-
-
-
-
-
-
-
diff --git a/src/mePyGuis/guis/TSVLoaderEditor.ui b/src/mePyGuis/guis/TSVLoaderEditor.ui
deleted file mode 100644
index a017cac..0000000
--- a/src/mePyGuis/guis/TSVLoaderEditor.ui
+++ /dev/null
@@ -1,309 +0,0 @@
-
-
- Dialog
-
-
-
- 0
- 0
- 1036
- 266
-
-
-
- Dialog
-
-
-
-
-
- -
-
-
-
-
-
- Qt::Vertical
-
-
-
- -
-
-
-
-
-
- font: 10pt;
-
-
- Actions
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Preferred
-
-
-
- 0
- 0
-
-
-
-
-
-
- -
-
-
-
-
-
- font: 10pt;
-
-
- some title
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Fixed
-
-
-
- 0
- 15
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 200
- 16777215
-
-
-
- color: rgb(90, 90, 90);
-font: 7pt;
-
-
- <html><head/><body><p>some description</p></body></html>
-
-
- Qt::AlignJustify|Qt::AlignVCenter
-
-
- true
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 200
- 0
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 3
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 3
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
-
-
- Load table
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Discard
-
-
-
- -
-
-
- Accept
-
-
-
-
-
- -
-
-
-
-
-
- Qt::ScrollBarAlwaysOn
-
-
- true
-
-
-
-
- 0
- 0
- 762
- 195
-
-
-
-
-
-
-
-
-
-
- Qt::Vertical
-
-
-
- 0
- 0
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- <html><head/><body><p><span style=" text-decoration: underline;">ID</span> map to</p></body></html>
-
-
-
- -
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 0
- 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/mePyGuis/guis/TracerEditor.ui b/src/mePyGuis/guis/TracerEditor.ui
deleted file mode 100644
index d1140a3..0000000
--- a/src/mePyGuis/guis/TracerEditor.ui
+++ /dev/null
@@ -1,473 +0,0 @@
-
-
- Dialog
-
-
-
- 0
- 0
- 759
- 506
-
-
-
- Dialog
-
-
-
-
-
- -
-
-
-
-
-
- Qt::Vertical
-
-
-
- -
-
-
-
-
-
- font: 10pt;
-
-
- Actions
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Preferred
-
-
-
- 0
- 0
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
-
-
- font: 10pt;
-
-
- Tracer
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Fixed
-
-
-
- 0
- 15
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 200
- 16777215
-
-
-
- color: rgb(90, 90, 90);
-font: 7pt;
-
-
- <html><head/><body><p>Please specify the tracer added to the experiment</p><p><br/></p></body></html>
-
-
- Qt::AlignJustify|Qt::AlignVCenter
-
-
- true
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 200
- 0
-
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 3
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 3
- 0
-
-
-
-
- -
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Discard
-
-
-
- -
-
-
- Accept
-
-
-
-
-
- -
-
-
-
-
-
- Name
-
-
-
- -
-
-
- DON
-
-
-
- -
-
-
- Atom count
-
-
-
- -
-
-
- 1
-
-
- 15
-
-
-
- -
-
-
- Native isotopolog
-
-
-
- -
-
-
- Native isotopolog enrichment
-
-
-
- -
-
-
- Labeled isotopolog
-
-
-
- -
-
-
- Labeled isotopolog enrichment
-
-
-
- -
-
-
- %
-
-
- 98.930000000000007
-
-
-
- -
-
-
- %
-
-
- 99.500000000000000
-
-
-
- -
-
-
- 12C
-
-
-
- -
-
-
- 13C
-
-
-
- -
-
-
- Virtually allowed ratio for exogenous tracers only
-
-
-
-
-
-
- Amount native compound
-
-
-
- -
-
-
- Amount labeled compound
-
-
-
- -
-
-
- =
-
-
- 1.000000000000000
-
-
- 50.000000000000000
-
-
-
- -
-
-
- =
-
-
- 50.000000000000000
-
-
-
- -
-
-
- Minimum ratio deviation relative to theoretical signal ratio
-
-
-
- -
-
-
- Maximum ratio deviation relative to theoretical signal ratio
-
-
-
- -
-
-
- ≤
-
-
- %
-
-
- 100000000.000000000000000
-
-
- 160.000000000000000
-
-
-
- -
-
-
- Allowed ratios in the LC-HRMS data calculated from the isotopic enrichments and the spiked amounts:
-
-
- true
-
-
-
- -
-
-
- TextLabel
-
-
-
- -
-
-
- ≥
-
-
- %
-
-
- 62.500000000000000
-
-
-
- -
-
-
- Theoretical signal ratio
-
-
-
- -
-
-
- -
-
-
-
-
-
-
- -
-
-
- Exogenous tracer (i.e. check ratio of native and labeled tracer form)
-
-
-
- -
-
-
- Note: Activate the ratio check for exogenous tracer compounds and deactivate it for endogenous tracer compounds
-
-
- true
-
-
-
-
-
-
-
-
-
-
- tName
- tAtomCount
- tNativeIsotope
- tNativeEnrichment
- tLabeledIsotope
- tLabeledEnrichment
- tMinRatio
- tMaxRatio
- acceptButton
- discardButton
-
-
-
-
diff --git a/src/mePyGuis/guis/adductsEditor.ui b/src/mePyGuis/guis/adductsEditor.ui
deleted file mode 100644
index 047a62b..0000000
--- a/src/mePyGuis/guis/adductsEditor.ui
+++ /dev/null
@@ -1,396 +0,0 @@
-
-
- Dialog
-
-
-
- 0
- 0
- 819
- 610
-
-
-
- Relationship configuration
-
-
-
-
-
- -
-
-
-
-
-
- QFrame::StyledPanel
-
-
- QFrame::Raised
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 3
- 20
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 3
- 20
-
-
-
-
- -
-
-
-
-
-
- font: 10pt;
-
-
- Adducts
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Fixed
-
-
-
- 0
- 5
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- color: rgb(90, 90, 90);
-font: 7pt;
-
-
- <html><head/><body><p>Please specify the adducts you want to use during the non-targeted feature grouping. Each row in the table represents one adduct</p><p>The first column is the name of the adduct<br/>The second column specifies the adducts m/z difference to a neutral molecule<br/>The third column specifies in which ionisation mode the adduct may occour. Valid values are + and -</p></body></html>
-
-
- Qt::AlignJustify|Qt::AlignVCenter
-
-
- true
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 200
- 0
-
-
-
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
-
-
-
- font: 10pt;
-
-
- Actions
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Minimum
-
-
-
- 0
- 0
-
-
-
-
-
-
- -
-
-
-
-
-
- Load defaults
-
-
-
- -
-
-
- Load
-
-
-
- -
-
-
- Save
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Discard
-
-
-
- -
-
-
- Accept
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- -
-
-
- QFrame::StyledPanel
-
-
- QFrame::Raised
-
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 3
- 20
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 3
- 20
-
-
-
-
- -
-
-
-
-
-
- font: 10pt;
-
-
- Neutral loss (elements)
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Fixed
-
-
-
- 0
- 5
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 200
- 16777215
-
-
-
- color: rgb(90, 90, 90);
-font: 7pt;
-
-
- <html><head/><body><p>Please specify the elements you want to use for the non-targeted feature group annotation. Each row represents one element</p><p>The first column specifies the chemical symbol of the element<br/>The second column specifies the neutral weight of the most abundant isotope of the elment<br/>The third column specifies the number of valenz electron this element has<br/>The fourth and fifth column specify the minimal and maximal number this element can occour in non-targetd group annotation</p></body></html>
-
-
- Qt::AlignJustify|Qt::AlignVCenter
-
-
- true
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 200
- 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/mePyGuis/guis/calcIsotopeEnrichmentDialog.ui b/src/mePyGuis/guis/calcIsotopeEnrichmentDialog.ui
deleted file mode 100644
index 01f1605..0000000
--- a/src/mePyGuis/guis/calcIsotopeEnrichmentDialog.ui
+++ /dev/null
@@ -1,390 +0,0 @@
-
-
- Dialog
-
-
-
- 0
- 0
- 1195
- 1109
-
-
-
- Dialog
-
-
- background-color: white;
-
-
-
-
- 950
- 1060
- 221
- 32
-
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
- 19
- 25
- 1161
- 568
-
-
-
-
- 0
- 0
-
-
-
- background-image: url(:/EnrichmentDialog/resources/EnrichmentDialog.png)
-
-
-
-
-
-
-
-
- 20
- 600
- 1151
- 451
-
-
-
-
- #1
-
-
-
-
- #2
-
-
-
-
- #3
-
-
-
-
- #4
-
-
-
-
- #5
-
-
-
-
- #6
-
-
-
-
- #7
-
-
-
-
- #8
-
-
-
-
- #9
-
-
-
-
- #10
-
-
-
-
- #11
-
-
-
-
- #12
-
-
-
-
- #13
-
-
-
-
- #14
-
-
-
-
- #15
-
-
-
-
- #16
-
-
-
-
- #17
-
-
-
-
- #18
-
-
-
-
- #19
-
-
-
-
- #20
-
-
-
-
- #21
-
-
-
-
- #22
-
-
-
-
- #23
-
-
-
-
- #24
-
-
-
-
- #25
-
-
-
-
- #26
-
-
-
-
- #27
-
-
-
-
- #28
-
-
-
-
- #29
-
-
-
-
- #30
-
-
-
-
- #31
-
-
-
-
- #32
-
-
-
-
- #33
-
-
-
-
- #34
-
-
-
-
- #35
-
-
-
-
- #36
-
-
-
-
- #37
-
-
-
-
- #38
-
-
-
-
- #39
-
-
-
-
- #40
-
-
-
-
- #41
-
-
-
-
- #42
-
-
-
-
- #43
-
-
-
-
- #44
-
-
-
-
- #45
-
-
-
-
- #46
-
-
-
-
- #47
-
-
-
-
- #48
-
-
-
-
- #49
-
-
-
-
- #50
-
-
-
-
- Abundance M
-
-
-
-
- Abundance M+1
-
-
-
-
- Abundance M'-1
-
-
-
-
- Abundance M'
-
-
-
-
- Xn
-
-
-
-
- M enrichment
-
-
-
-
- M' enrichment
-
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- Dialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- Dialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/mePyGuis/guis/combineResultsDialog.ui b/src/mePyGuis/guis/combineResultsDialog.ui
deleted file mode 100644
index 59c5a6d..0000000
--- a/src/mePyGuis/guis/combineResultsDialog.ui
+++ /dev/null
@@ -1,505 +0,0 @@
-
-
- Dialog
-
-
-
- 0
- 0
- 602
- 750
-
-
-
- MetExtract II
-
-
- -
-
-
- <html><head/><body><p><span style=" font-size:12pt; font-weight:600;">Combine results</span></p><p>Combine the data processing results from several MetExtract II experiments. For example, if three experiments (U-13C-labeling, U-15N-labeling and a tracer-biotransformation experiment) have been carried out, the results can be combined into a singe data matrix using this tool. The tool is a very basic tool and does not perform any sort of chromatographic alignment or a correction for m/z shifts across the chromatograms. </p></body></html>
-
-
- true
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Input experiments
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 10
-
-
-
-
- -
-
-
- Load
-
-
-
- -
-
-
- Load
-
-
-
- -
-
-
- Load
-
-
-
- -
-
-
- Results experiment A
-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 10
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 10
-
-
-
-
- -
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 10
-
-
-
-
- -
-
-
- Results experiment C
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 10
-
-
-
-
- -
-
-
- Results experiment B
-
-
-
- -
-
-
- -
-
-
- Prefix experiment B
-
-
-
- -
-
-
- Prefix experiment A
-
-
-
- -
-
-
- Load
-
-
-
- -
-
-
- Prefix experiment C
-
-
-
- -
-
-
- Results experiment D
-
-
-
- -
-
-
- Prefix experiment D
-
-
-
- -
-
-
- Results experiment E
-
-
-
- -
-
-
- Prefix experiment E
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Load
-
-
-
- -
-
-
- Results experiment F
-
-
-
- -
-
-
- -
-
-
- Prefix experiment F
-
-
-
- -
-
-
- Load
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- -
-
-
- -
-
-
- <html><head/><body><p><span style=" font-size:32pt;">A</span></p></body></html>
-
-
-
- -
-
-
- <html><head/><body><p><span style=" font-size:32pt;">B</span></p></body></html>
-
-
- expBSave
-
-
-
- -
-
-
- <html><head/><body><p><span style=" font-size:32pt; color:#8b8b8b;">C</span></p></body></html>
-
-
-
- -
-
-
- <html><head/><body><p><span style=" font-size:32pt; color:#8b8b8b;">D</span></p></body></html>
-
-
-
- -
-
-
- <html><head/><body><p><span style=" font-size:32pt; color:#8b8b8b;">E</span></p></body></html>
-
-
-
- -
-
-
- <html><head/><body><p><span style=" font-size:32pt; color:#8b8b8b;">F</span></p></body></html>
-
-
-
-
-
-
- -
-
-
- QLayout::SetMinimumSize
-
-
-
-
-
- Save combined results to
-
-
-
- -
-
-
- -
-
-
- Select
-
-
-
-
-
- -
-
-
- Processing parameters
-
-
-
-
-
-
- Maximum m/z deviation (± ppm)
-
-
-
- -
-
-
- Maximum retentiontime deviation (± minutes)
-
-
-
- -
-
-
- 0.000000000000000
-
-
- 5.000000000000000
-
-
-
- -
-
-
- 0.150000000000000
-
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Expanding
-
-
-
- 0
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Fixed
-
-
-
- 20
- 20
-
-
-
-
- -
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Run
-
-
-
-
-
-
-
-
- expASave
- expALoad
- expAPrefix
- expBSave
- expBLoad
- expBPrefix
- expCSave
- expCLoad
- expCPrefix
- expDSave
- expDLoad
- expDPrefix
- expESave
- expELoad
- expEPrefix
- expFSave
- expFLoad
- expFPrefix
- maxPPMDev
- maxRTDev
- saveResults
- loadResults
- run
-
-
-
-
diff --git a/src/mePyGuis/guis/groupEditor.ui b/src/mePyGuis/guis/groupEditor.ui
deleted file mode 100644
index 68eb689..0000000
--- a/src/mePyGuis/guis/groupEditor.ui
+++ /dev/null
@@ -1,468 +0,0 @@
-
-
- GroupEditor
-
-
-
- 0
- 0
- 781
- 552
-
-
-
- Dialog
-
-
-
-
-
- -
-
-
- 9
-
-
- 9
-
-
- 9
-
-
- 9
-
-
-
-
-
-
-
-
- font: 10pt;
-
-
- Options
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Minimum
-
-
-
- 0
- 0
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 3
- 0
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 3
- 0
-
-
-
-
- -
-
-
- false
-
-
-
-
-
-
- -
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Ok
-
-
-
- -
-
-
- Cancel
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
-
-
- font: 10pt;
-
-
- Files
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Fixed
-
-
-
- 0
- 5
-
-
-
-
- -
-
-
- color: rgb(90, 90, 90);
-font: 7pt;
-
-
- <html><head/><body><p>Specify the measurement files for this group</p></body></html>
-
-
- Qt::AlignJustify|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 200
- 40
-
-
-
-
-
-
- -
-
-
- QAbstractItemView::MultiSelection
-
-
-
- -
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Add folder
-
-
-
- -
-
-
- Add file(s)
-
-
-
- -
-
-
- Remove selected
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
-
-
- font: 10pt;
-
-
- Actions
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Minimum
-
-
-
- 0
- 0
-
-
-
-
-
-
- -
-
-
-
-
-
- font: 10pt;
-
-
- Group name
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Minimum
-
-
-
- 0
- 0
-
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
-
-
-
- Use sample for MSMS targets
-
-
-
- -
-
-
- Remove (false positive)
-
-
-
- -
-
-
- Use samples for metabolite grouping
-
-
- true
-
-
-
- -
-
-
- Omit features
-
-
- true
-
-
-
-
-
-
- Minimum found
-
-
-
- -
-
-
- 1
-
-
- 999
-
-
- 1
-
-
-
-
-
-
- -
-
-
- -
-
-
- Color
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
-
-
-
-
- dialogFinished
- groupName
- groupFiles
- addFolder
- addFiles
- removeSelected
- useForMetaboliteGrouping
- removeAsFalsePositive
- useAsMSMSTarget
- dialogCanceled
-
-
-
-
diff --git a/src/mePyGuis/guis/heteroAtomEditor.ui b/src/mePyGuis/guis/heteroAtomEditor.ui
deleted file mode 100644
index 4d87592..0000000
--- a/src/mePyGuis/guis/heteroAtomEditor.ui
+++ /dev/null
@@ -1,209 +0,0 @@
-
-
- Dialog
-
-
-
- 0
- 0
- 765
- 474
-
-
-
- Relationship configuration
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- font: 10pt;
-
-
- Heteroatoms (elements)
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Fixed
-
-
-
- 0
- 5
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 200
- 16777215
-
-
-
- color: rgb(90, 90, 90);
-font: 7pt;
-
-
- <html><head/><body><p>Please specify the elements you want to use for the targeted search for elements other then the labeling element. Each row represents one element</p><p>The first column specifies the chemical symbol of the respective isotope. Use <protonNumber><ElementSymbol> (e.g. <sup>34</sup>S)<br/>The second column specifies the mass offset to the most abundant isotope of the elment (e.g. <sup>34</sup>S and <sup>32</sup>S have a mass difference of 1.9958)<br/>The third column specifies the expected intensity of this isotope in respect to the most abundant isotope of the respective element (e.g. <sup>34</sup>S occours in nature with a probability of 4.21% and <sup>32</sup>S with a probability of 95.02%. Therefore the calculated ratio for a molecule with a singe sulphur atom is 4.43%)<br/>The fourth and fifth column specify the minimal and maximal number this element can occour in non-targetd group annotation. </p></body></html>
-
-
- Qt::AlignJustify|Qt::AlignVCenter
-
-
- true
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 200
- 0
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
-
-
- Load defaults
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Discard
-
-
-
- -
-
-
- Accept
-
-
-
-
-
- -
-
-
- true
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
-
-
- font: 10pt;
-
-
- Actions
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Minimum
-
-
-
- 0
- 0
-
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
-
-
-
-
-
-
-
diff --git a/src/mePyGuis/guis/mainwindow.ui b/src/mePyGuis/guis/mainwindow.ui
index 4b047f2..489535e 100644
--- a/src/mePyGuis/guis/mainwindow.ui
+++ b/src/mePyGuis/guis/mainwindow.ui
@@ -3690,12 +3690,49 @@ font: 7pt;
true
- -
-
-
- Check retention time
-
-
+
-
+
+
-
+
+
+ Check retention time
+
+
+
+ -
+
+
+ Maximum retention time deviation
+
+
+
+ -
+
+
+ ±
+
+
+ minutes
+
+
+ 0.150000000000000
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
-
@@ -3774,43 +3811,7 @@ font: 7pt;
- -
-
-
-
-
-
- Maximum retention time deviation
-
-
-
- -
-
-
- ±
-
-
- minutes
-
-
- 0.150000000000000
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
+
-
-
@@ -4343,6 +4344,39 @@ font: 7pt;
+ -
+
+
+ Use abundance similarity
+
+
+ true
+
+
+
+ -
+
+
+ Abundance similarity threshold
+
+
+
+ -
+
+
+ ≥
+
+
+ %
+
+
+ 100.000000000000000
+
+
+ 85.000000000000000
+
+
+
@@ -5742,6 +5776,114 @@ font: 7pt;
+
+
+ Abundance profiles
+
+
+ -
+
+
-
+
+
+ Visualization
+
+
+
+ -
+
+
-
+
+ Boxplot
+
+
+ -
+
+ Line plot
+
+
+
+
+ -
+
+
+ Scale
+
+
+
+ -
+
+
-
+
+ Linear
+
+
+ -
+
+ Logarithmic
+
+
+
+
+ -
+
+
+ Normalization
+
+
+
+ -
+
+
-
+
+ None
+
+
+ -
+
+ Scale to max sample
+
+
+ -
+
+ Scale to max experimental group
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 700
+ 500
+
+
+
+
+
+
@@ -5926,6 +6068,8 @@ font: 7pt;
polynomValue
minConnectionRate
metaboliteClusterMinConnections
+ useAbundanceSimilarityForConvolution
+ abundanceSimilarityThreshold
integratedMissedPeaks
integrationMaxTimeDifference
reintegrateIntensityCutoff
diff --git a/src/mePyGuis/mainWindow.py b/src/mePyGuis/mainWindow.py
index 6abb85b..44348ae 100644
--- a/src/mePyGuis/mainWindow.py
+++ b/src/mePyGuis/mainWindow.py
@@ -60,6 +60,7 @@ def setupUi(self, MainWindow):
self.INFOLabel.setStyleSheet(_fromUtf8(""))
self.INFOLabel.setText(_fromUtf8(""))
self.INFOLabel.setObjectName(_fromUtf8("INFOLabel"))
+ self.INFOLabel.setVisible(False)
self.gridLayout_11.addWidget(self.INFOLabel, 0, 0, 1, 1)
self.tabWidget = QtWidgets.QTabWidget(self.centralWidget)
self.tabWidget.setEnabled(True)
@@ -179,6 +180,19 @@ def setupUi(self, MainWindow):
self.removeGroup.setIcon(icon2)
self.removeGroup.setObjectName(_fromUtf8("removeGroup"))
self.horizontalLayout_4.addWidget(self.removeGroup)
+ self.line_fileStats = QtWidgets.QFrame(self.inputTab)
+ self.line_fileStats.setFrameShape(QtWidgets.QFrame.VLine)
+ self.line_fileStats.setFrameShadow(QtWidgets.QFrame.Sunken)
+ self.line_fileStats.setObjectName(_fromUtf8("line_fileStats"))
+ self.horizontalLayout_4.addWidget(self.line_fileStats)
+ self.showFileStats = QtWidgets.QPushButton(self.inputTab)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.showFileStats.sizePolicy().hasHeightForWidth())
+ self.showFileStats.setSizePolicy(sizePolicy)
+ self.showFileStats.setObjectName(_fromUtf8("showFileStats"))
+ self.horizontalLayout_4.addWidget(self.showFileStats)
self.gridLayout_22.addLayout(self.horizontalLayout_4, 14, 4, 1, 1)
self.line_28 = QtWidgets.QFrame(self.inputTab)
self.line_28.setFrameShape(QtWidgets.QFrame.HLine)
@@ -1642,7 +1656,6 @@ def setupUi(self, MainWindow):
self.gridLayout_51.setObjectName(_fromUtf8("gridLayout_51"))
self.checkRTInHits_checkBox = QtWidgets.QCheckBox(self.searchDB_checkBox)
self.checkRTInHits_checkBox.setObjectName(_fromUtf8("checkRTInHits_checkBox"))
- self.gridLayout_51.addWidget(self.checkRTInHits_checkBox, 1, 0, 1, 1)
self.horizontalLayout_23 = QtWidgets.QHBoxLayout()
self.horizontalLayout_23.setObjectName(_fromUtf8("horizontalLayout_23"))
spacerItem48 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
@@ -1652,6 +1665,7 @@ def setupUi(self, MainWindow):
self.horizontalLayout_23.addWidget(self.addDB_pushButton)
self.addmzVaultRep_pushButton = QtWidgets.QPushButton(self.searchDB_checkBox)
self.addmzVaultRep_pushButton.setEnabled(False)
+ self.addmzVaultRep_pushButton.setVisible(False)
self.addmzVaultRep_pushButton.setObjectName(_fromUtf8("addmzVaultRep_pushButton"))
self.horizontalLayout_23.addWidget(self.addmzVaultRep_pushButton)
self.removeDB_pushButton = QtWidgets.QPushButton(self.searchDB_checkBox)
@@ -1665,9 +1679,17 @@ def setupUi(self, MainWindow):
self.generateDBTemplate_pushButton = QtWidgets.QPushButton(self.searchDB_checkBox)
self.generateDBTemplate_pushButton.setObjectName(_fromUtf8("generateDBTemplate_pushButton"))
self.horizontalLayout_23.addWidget(self.generateDBTemplate_pushButton)
+ self.line_37 = QtWidgets.QFrame(self.searchDB_checkBox)
+ self.line_37.setFrameShape(QtWidgets.QFrame.VLine)
+ self.line_37.setFrameShadow(QtWidgets.QFrame.Sunken)
+ self.line_37.setObjectName(_fromUtf8("line_37"))
+ self.horizontalLayout_23.addWidget(self.line_37)
+ self.testDBs_pushButton = QtWidgets.QPushButton(self.searchDB_checkBox)
+ self.testDBs_pushButton.setObjectName(_fromUtf8("testDBs_pushButton"))
+ self.horizontalLayout_23.addWidget(self.testDBs_pushButton)
self.gridLayout_51.addLayout(self.horizontalLayout_23, 3, 0, 1, 7)
self.dbList_listView = QtWidgets.QListView(self.searchDB_checkBox)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.dbList_listView.sizePolicy().hasHeightForWidth())
@@ -1676,28 +1698,31 @@ def setupUi(self, MainWindow):
self.dbList_listView.setMaximumSize(QtCore.QSize(16777215, 70))
self.dbList_listView.setObjectName(_fromUtf8("dbList_listView"))
self.gridLayout_51.addWidget(self.dbList_listView, 0, 0, 1, 7)
- self.horizontalLayout_11 = QtWidgets.QHBoxLayout()
- self.horizontalLayout_11.setObjectName(_fromUtf8("horizontalLayout_11"))
+ self.horizontalLayout_checkRT = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_checkRT.setObjectName(_fromUtf8("horizontalLayout_checkRT"))
+ self.horizontalLayout_checkRT.addWidget(self.checkRTInHits_checkBox)
self.label_79 = QtWidgets.QLabel(self.searchDB_checkBox)
self.label_79.setObjectName(_fromUtf8("label_79"))
- self.horizontalLayout_11.addWidget(self.label_79)
+ self.horizontalLayout_checkRT.addWidget(self.label_79)
self.maxRTErrorInHits_spinnerBox = QtWidgets.QDoubleSpinBox(self.searchDB_checkBox)
self.maxRTErrorInHits_spinnerBox.setProperty("value", 0.15)
self.maxRTErrorInHits_spinnerBox.setObjectName(_fromUtf8("maxRTErrorInHits_spinnerBox"))
- self.horizontalLayout_11.addWidget(self.maxRTErrorInHits_spinnerBox)
+ self.horizontalLayout_checkRT.addWidget(self.maxRTErrorInHits_spinnerBox)
spacerItem49 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
- self.horizontalLayout_11.addItem(spacerItem49)
- self.gridLayout_51.addLayout(self.horizontalLayout_11, 1, 1, 1, 1)
+ self.horizontalLayout_checkRT.addItem(spacerItem49)
+ self.gridLayout_51.addLayout(self.horizontalLayout_checkRT, 1, 0, 1, 7)
self.horizontalLayout_20 = QtWidgets.QHBoxLayout()
self.horizontalLayout_20.setObjectName(_fromUtf8("horizontalLayout_20"))
self.label_80 = QtWidgets.QLabel(self.searchDB_checkBox)
self.label_80.setObjectName(_fromUtf8("label_80"))
+ self.label_80.setVisible(False)
self.horizontalLayout_20.addWidget(self.label_80)
self.minMSMSScore_doubleSpinBox = QtWidgets.QDoubleSpinBox(self.searchDB_checkBox)
self.minMSMSScore_doubleSpinBox.setMinimumSize(QtCore.QSize(80, 0))
self.minMSMSScore_doubleSpinBox.setMaximum(1.0)
self.minMSMSScore_doubleSpinBox.setProperty("value", 0.7)
self.minMSMSScore_doubleSpinBox.setObjectName(_fromUtf8("minMSMSScore_doubleSpinBox"))
+ self.minMSMSScore_doubleSpinBox.setVisible(False)
self.horizontalLayout_20.addWidget(self.minMSMSScore_doubleSpinBox)
spacerItem50 = QtWidgets.QSpacerItem(
40,
@@ -1861,6 +1886,38 @@ def setupUi(self, MainWindow):
self.horizontalLayout_17.addWidget(self.groupingRT)
self.gridLayout_37.addLayout(self.horizontalLayout_17, 0, 1, 1, 1)
self.verticalLayout_9.addWidget(self.groupResults)
+ self.integratedMissedPeaks = QtWidgets.QGroupBox(self.frame_bracketResults)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.integratedMissedPeaks.sizePolicy().hasHeightForWidth())
+ self.integratedMissedPeaks.setSizePolicy(sizePolicy)
+ self.integratedMissedPeaks.setCheckable(True)
+ self.integratedMissedPeaks.setObjectName(_fromUtf8("integratedMissedPeaks"))
+ self.gridLayout_38 = QtWidgets.QGridLayout(self.integratedMissedPeaks)
+ self.gridLayout_38.setObjectName(_fromUtf8("gridLayout_38"))
+ self.horizontalLayout_15 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_15.setObjectName(_fromUtf8("horizontalLayout_15"))
+ self.label_55 = QtWidgets.QLabel(self.integratedMissedPeaks)
+ self.label_55.setObjectName(_fromUtf8("label_55"))
+ self.horizontalLayout_15.addWidget(self.label_55)
+ self.integrationMaxTimeDifference = QtWidgets.QDoubleSpinBox(self.integratedMissedPeaks)
+ self.integrationMaxTimeDifference.setDecimals(2)
+ self.integrationMaxTimeDifference.setProperty("value", 0.1)
+ self.integrationMaxTimeDifference.setObjectName(_fromUtf8("integrationMaxTimeDifference"))
+ self.horizontalLayout_15.addWidget(self.integrationMaxTimeDifference)
+ self.label_68 = QtWidgets.QLabel(self.integratedMissedPeaks)
+ self.label_68.setObjectName(_fromUtf8("label_68"))
+ self.horizontalLayout_15.addWidget(self.label_68)
+ self.reintegrateIntensityCutoff = QtWidgets.QSpinBox(self.integratedMissedPeaks)
+ self.reintegrateIntensityCutoff.setProperty("showGroupSeparator", True)
+ self.reintegrateIntensityCutoff.setSuffix(_fromUtf8(""))
+ self.reintegrateIntensityCutoff.setMaximum(100000000)
+ self.reintegrateIntensityCutoff.setSingleStep(1000)
+ self.reintegrateIntensityCutoff.setObjectName(_fromUtf8("reintegrateIntensityCutoff"))
+ self.horizontalLayout_15.addWidget(self.reintegrateIntensityCutoff)
+ self.gridLayout_38.addLayout(self.horizontalLayout_15, 0, 0, 1, 1)
+ self.verticalLayout_9.addWidget(self.integratedMissedPeaks)
self.convoluteResults = QtWidgets.QGroupBox(self.frame_bracketResults)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
@@ -1898,43 +1955,33 @@ def setupUi(self, MainWindow):
self.metaboliteClusterMinConnections.setProperty("value", 1)
self.metaboliteClusterMinConnections.setObjectName(_fromUtf8("metaboliteClusterMinConnections"))
self.horizontalLayout_8.addWidget(self.metaboliteClusterMinConnections)
+ self.gridLayout_14.addLayout(self.horizontalLayout_8, 0, 0, 1, 1)
+ self.horizontalLayout_sil = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_sil.setObjectName(_fromUtf8("horizontalLayout_sil"))
self.useSILRatioForConvolution = QtWidgets.QCheckBox(self.convoluteResults)
self.useSILRatioForConvolution.setObjectName(_fromUtf8("useSILRatioForConvolution"))
- self.horizontalLayout_8.addWidget(self.useSILRatioForConvolution)
- self.gridLayout_14.addLayout(self.horizontalLayout_8, 0, 0, 1, 1)
+ self.horizontalLayout_sil.addWidget(self.useSILRatioForConvolution)
+ spacerItemSil = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_sil.addItem(spacerItemSil)
+ self.gridLayout_14.addLayout(self.horizontalLayout_sil, 1, 0, 1, 1)
+ self.horizontalLayout_abundance = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_abundance.setObjectName(_fromUtf8("horizontalLayout_abundance"))
+ self.useAbundanceSimilarityForConvolution = QtWidgets.QCheckBox(self.convoluteResults)
+ self.useAbundanceSimilarityForConvolution.setChecked(True)
+ self.useAbundanceSimilarityForConvolution.setObjectName(_fromUtf8("useAbundanceSimilarityForConvolution"))
+ self.horizontalLayout_abundance.addWidget(self.useAbundanceSimilarityForConvolution)
+ self.label_122 = QtWidgets.QLabel(self.convoluteResults)
+ self.label_122.setObjectName(_fromUtf8("label_122"))
+ self.horizontalLayout_abundance.addWidget(self.label_122)
+ self.abundanceSimilarityThreshold = QtWidgets.QDoubleSpinBox(self.convoluteResults)
+ self.abundanceSimilarityThreshold.setMaximum(100.0)
+ self.abundanceSimilarityThreshold.setProperty("value", 85.0)
+ self.abundanceSimilarityThreshold.setObjectName(_fromUtf8("abundanceSimilarityThreshold"))
+ self.horizontalLayout_abundance.addWidget(self.abundanceSimilarityThreshold)
+ spacerItemAbundance = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_abundance.addItem(spacerItemAbundance)
+ self.gridLayout_14.addLayout(self.horizontalLayout_abundance, 2, 0, 1, 1)
self.verticalLayout_9.addWidget(self.convoluteResults)
- self.integratedMissedPeaks = QtWidgets.QGroupBox(self.frame_bracketResults)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.integratedMissedPeaks.sizePolicy().hasHeightForWidth())
- self.integratedMissedPeaks.setSizePolicy(sizePolicy)
- self.integratedMissedPeaks.setCheckable(True)
- self.integratedMissedPeaks.setObjectName(_fromUtf8("integratedMissedPeaks"))
- self.gridLayout_38 = QtWidgets.QGridLayout(self.integratedMissedPeaks)
- self.gridLayout_38.setObjectName(_fromUtf8("gridLayout_38"))
- self.horizontalLayout_15 = QtWidgets.QHBoxLayout()
- self.horizontalLayout_15.setObjectName(_fromUtf8("horizontalLayout_15"))
- self.label_55 = QtWidgets.QLabel(self.integratedMissedPeaks)
- self.label_55.setObjectName(_fromUtf8("label_55"))
- self.horizontalLayout_15.addWidget(self.label_55)
- self.integrationMaxTimeDifference = QtWidgets.QDoubleSpinBox(self.integratedMissedPeaks)
- self.integrationMaxTimeDifference.setDecimals(2)
- self.integrationMaxTimeDifference.setProperty("value", 0.1)
- self.integrationMaxTimeDifference.setObjectName(_fromUtf8("integrationMaxTimeDifference"))
- self.horizontalLayout_15.addWidget(self.integrationMaxTimeDifference)
- self.label_68 = QtWidgets.QLabel(self.integratedMissedPeaks)
- self.label_68.setObjectName(_fromUtf8("label_68"))
- self.horizontalLayout_15.addWidget(self.label_68)
- self.reintegrateIntensityCutoff = QtWidgets.QSpinBox(self.integratedMissedPeaks)
- self.reintegrateIntensityCutoff.setProperty("showGroupSeparator", True)
- self.reintegrateIntensityCutoff.setSuffix(_fromUtf8(""))
- self.reintegrateIntensityCutoff.setMaximum(100000000)
- self.reintegrateIntensityCutoff.setSingleStep(1000)
- self.reintegrateIntensityCutoff.setObjectName(_fromUtf8("reintegrateIntensityCutoff"))
- self.horizontalLayout_15.addWidget(self.reintegrateIntensityCutoff)
- self.gridLayout_38.addLayout(self.horizontalLayout_15, 0, 0, 1, 1)
- self.verticalLayout_9.addWidget(self.integratedMissedPeaks)
self.checkBox_expPeakArea = QtWidgets.QCheckBox(self.frame_bracketResults)
self.checkBox_expPeakArea.setChecked(True)
self.checkBox_expPeakArea.setObjectName(_fromUtf8("checkBox_expPeakArea"))
@@ -2136,6 +2183,8 @@ def setupUi(self, MainWindow):
self.gridLayout_5.setObjectName(_fromUtf8("gridLayout_5"))
self.tabWidget_2 = QtWidgets.QTabWidget(self.widget1)
self.tabWidget_2.setObjectName(_fromUtf8("tabWidget_2"))
+ self.tabWidget_2.setTabPosition(QtWidgets.QTabWidget.West)
+ self.tabWidget_2.setTabShape(QtWidgets.QTabWidget.Rounded)
self.tab = QtWidgets.QWidget()
self.tab.setObjectName(_fromUtf8("tab"))
self.gridLayout_128 = QtWidgets.QGridLayout(self.tab)
@@ -2155,6 +2204,7 @@ def setupUi(self, MainWindow):
self.horizontalLayout_10.setObjectName(_fromUtf8("horizontalLayout_10"))
self.label_95 = QtWidgets.QLabel(self.widget2)
self.label_95.setObjectName(_fromUtf8("label_95"))
+ self.label_95.setVisible(False)
self.horizontalLayout_10.addWidget(self.label_95)
spacerItem58 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_10.addItem(spacerItem58)
@@ -2264,6 +2314,7 @@ def setupUi(self, MainWindow):
self.horizontalLayout_7.setObjectName(_fromUtf8("horizontalLayout_7"))
self.label_96 = QtWidgets.QLabel(self.widget3)
self.label_96.setObjectName(_fromUtf8("label_96"))
+ self.label_96.setVisible(False)
self.horizontalLayout_7.addWidget(self.label_96)
spacerItem59 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_7.addItem(spacerItem59)
@@ -2359,7 +2410,7 @@ def setupUi(self, MainWindow):
self.label_msms.setObjectName(_fromUtf8("label_msms"))
self.gridLayout_msms_list.addWidget(self.label_msms, 0, 0, 1, 1)
- self.msms_SpectraList = QtWidgets.QListWidget(self.msms_list_container)
+ self.msms_SpectraList = QtWidgets.QTableWidget(self.msms_list_container)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -2367,6 +2418,13 @@ def setupUi(self, MainWindow):
self.msms_SpectraList.setSizePolicy(sizePolicy)
self.msms_SpectraList.setMinimumSize(QtCore.QSize(300, 0))
self.msms_SpectraList.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+ self.msms_SpectraList.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+ self.msms_SpectraList.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
+ self.msms_SpectraList.setColumnCount(4)
+ self.msms_SpectraList.setHorizontalHeaderLabels(["N/L", "Prec. Abundance", "Prec. m/z", "RT (min)"])
+ self.msms_SpectraList.horizontalHeader().setStretchLastSection(True)
+ self.msms_SpectraList.verticalHeader().setVisible(False)
+ self.msms_SpectraList.setSortingEnabled(True)
self.msms_SpectraList.setObjectName(_fromUtf8("msms_SpectraList"))
self.gridLayout_msms_list.addWidget(self.msms_SpectraList, 1, 0, 1, 1)
@@ -2421,7 +2479,25 @@ def setupUi(self, MainWindow):
self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 1353, 776))
self.scrollAreaWidgetContents.setObjectName(_fromUtf8("scrollAreaWidgetContents"))
self.gridLayout_40 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents)
+ self.gridLayout_40.setContentsMargins(0, 0, 0, 0)
self.gridLayout_40.setObjectName(_fromUtf8("gridLayout_40"))
+ # Horizontal splitter between left (tree) and right (detail) panels
+ self.splitter_exp_leftRight = QtWidgets.QSplitter(self.scrollAreaWidgetContents)
+ self.splitter_exp_leftRight.setOrientation(QtCore.Qt.Horizontal)
+ self.splitter_exp_leftRight.setObjectName(_fromUtf8("splitter_exp_leftRight"))
+ self.gridLayout_40.addWidget(self.splitter_exp_leftRight, 0, 0, 1, 1)
+ # Left panel
+ self.exp_left_panel = QtWidgets.QWidget(self.splitter_exp_leftRight)
+ self.exp_left_panel.setObjectName(_fromUtf8("exp_left_panel"))
+ self.exp_left_layout = QtWidgets.QVBoxLayout(self.exp_left_panel)
+ self.exp_left_layout.setContentsMargins(0, 0, 0, 0)
+ self.exp_left_layout.setObjectName(_fromUtf8("exp_left_layout"))
+ # Right panel
+ self.exp_right_panel = QtWidgets.QWidget(self.splitter_exp_leftRight)
+ self.exp_right_panel.setObjectName(_fromUtf8("exp_right_panel"))
+ self.exp_right_layout = QtWidgets.QGridLayout(self.exp_right_panel)
+ self.exp_right_layout.setContentsMargins(0, 0, 0, 0)
+ self.exp_right_layout.setObjectName(_fromUtf8("exp_right_layout"))
self.groupBox_options3 = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)
self.groupBox_options3.setObjectName(_fromUtf8("groupBox_options3"))
self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.groupBox_options3)
@@ -2463,11 +2539,12 @@ def setupUi(self, MainWindow):
self.showLegend_experiment.setChecked(False)
self.showLegend_experiment.setObjectName(_fromUtf8("showLegend_experiment"))
self.verticalLayout_4.addWidget(self.showLegend_experiment)
- self.gridLayout_40.addWidget(self.groupBox_options3, 2, 1, 1, 1)
+ self.exp_right_layout.addWidget(self.groupBox_options3, 1, 0, 1, 1)
self.horizontalLayout_12 = QtWidgets.QHBoxLayout()
self.horizontalLayout_12.setObjectName(_fromUtf8("horizontalLayout_12"))
self.label_97 = QtWidgets.QLabel(self.scrollAreaWidgetContents)
self.label_97.setObjectName(_fromUtf8("label_97"))
+ self.label_97.setVisible(False)
self.horizontalLayout_12.addWidget(self.label_97)
spacerItem64 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_12.addItem(spacerItem64)
@@ -2485,13 +2562,13 @@ def setupUi(self, MainWindow):
self.showCustomFeature_pushButton = QtWidgets.QPushButton(self.scrollAreaWidgetContents)
self.showCustomFeature_pushButton.setObjectName(_fromUtf8("showCustomFeature_pushButton"))
self.horizontalLayout_12.addWidget(self.showCustomFeature_pushButton)
- self.gridLayout_40.addLayout(self.horizontalLayout_12, 1, 1, 1, 1)
+ self.exp_right_layout.addLayout(self.horizontalLayout_12, 0, 0, 1, 1)
self.expFilterWidget = QtWidgets.QWidget(self.scrollAreaWidgetContents)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
self.expFilterWidget.setSizePolicy(sizePolicy)
- self.expFilterWidget.setMinimumSize(QtCore.QSize(500, 0))
+ self.expFilterWidget.setMinimumSize(QtCore.QSize(200, 0))
self.expFilterWidget.setObjectName(_fromUtf8("expFilterWidget"))
self.horizontalLayout_expFilter = QtWidgets.QHBoxLayout(self.expFilterWidget)
self.horizontalLayout_expFilter.setContentsMargins(0, 0, 0, 0)
@@ -2510,37 +2587,43 @@ def setupUi(self, MainWindow):
self.expDataFilter.setSizePolicy(sizePolicy)
self.expDataFilter.setObjectName(_fromUtf8("expDataFilter"))
self.horizontalLayout_expFilter.addWidget(self.expDataFilter)
- self.gridLayout_40.addWidget(self.expFilterWidget, 1, 0, 1, 1)
+ self.exp_left_layout.addWidget(self.expFilterWidget)
self.resultsExperiment_TreeWidget = QtWidgets.QTreeWidget(self.scrollAreaWidgetContents)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.resultsExperiment_TreeWidget.sizePolicy().hasHeightForWidth())
self.resultsExperiment_TreeWidget.setSizePolicy(sizePolicy)
- self.resultsExperiment_TreeWidget.setMinimumSize(QtCore.QSize(500, 0))
+ self.resultsExperiment_TreeWidget.setMinimumSize(QtCore.QSize(200, 0))
self.resultsExperiment_TreeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.resultsExperiment_TreeWidget.setColumnCount(6)
self.resultsExperiment_TreeWidget.setObjectName(_fromUtf8("resultsExperiment_TreeWidget"))
self.resultsExperiment_TreeWidget.header().setDefaultSectionSize(40)
- self.gridLayout_40.addWidget(self.resultsExperiment_TreeWidget, 2, 0, 4, 1)
+ self.exp_left_layout.addWidget(self.resultsExperiment_TreeWidget)
self.tabWidget_3 = QtWidgets.QTabWidget(self.scrollAreaWidgetContents)
self.tabWidget_3.setObjectName(_fromUtf8("tabWidget_3"))
+ self.tabWidget_3.setTabPosition(QtWidgets.QTabWidget.West)
+ self.tabWidget_3.setTabShape(QtWidgets.QTabWidget.Rounded)
self.tab_5 = QtWidgets.QWidget()
self.tab_5.setObjectName(_fromUtf8("tab_5"))
self.gridLayout_42 = QtWidgets.QGridLayout(self.tab_5)
self.gridLayout_42.setObjectName(_fromUtf8("gridLayout_42"))
- self.resultsExperiment_widget = QtWidgets.QWidget(self.tab_5)
+ self.gridLayout_42.setContentsMargins(0, 0, 0, 0)
+ # Vertical splitter between top XIC chart and bottom MS scan chart
+ self.splitter_exp_rawxics = QtWidgets.QSplitter(self.tab_5)
+ self.splitter_exp_rawxics.setOrientation(QtCore.Qt.Vertical)
+ self.splitter_exp_rawxics.setObjectName(_fromUtf8("splitter_exp_rawxics"))
+ self.gridLayout_42.addWidget(self.splitter_exp_rawxics, 1, 0, 1, 1)
+ self.resultsExperiment_widget = QtWidgets.QWidget(self.splitter_exp_rawxics)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.resultsExperiment_widget.sizePolicy().hasHeightForWidth())
self.resultsExperiment_widget.setSizePolicy(sizePolicy)
- self.resultsExperiment_widget.setMinimumSize(QtCore.QSize(700, 500))
+ self.resultsExperiment_widget.setMinimumSize(QtCore.QSize(700, 200))
self.resultsExperiment_widget.setObjectName(_fromUtf8("resultsExperiment_widget"))
- self.gridLayout_42.addWidget(self.resultsExperiment_widget, 1, 0, 1, 1)
- self.resultsExperimentMSScan_widget = QtWidgets.QWidget(self.tab_5)
+ self.resultsExperimentMSScan_widget = QtWidgets.QWidget(self.splitter_exp_rawxics)
self.resultsExperimentMSScan_widget.setObjectName(_fromUtf8("resultsExperimentMSScan_widget"))
- self.gridLayout_42.addWidget(self.resultsExperimentMSScan_widget, 2, 0, 1, 1)
self.tabWidget_3.addTab(self.tab_5, _fromUtf8(""))
self.tab_6 = QtWidgets.QWidget()
self.tab_6.setObjectName(_fromUtf8("tab_6"))
@@ -2594,9 +2677,16 @@ def setupUi(self, MainWindow):
self.label_msms_exp = QtWidgets.QLabel(self.msms_list_container_exp)
self.label_msms_exp.setObjectName(_fromUtf8("label_msms_exp"))
self.verticalLayout_msms_exp.addWidget(self.label_msms_exp)
- self.msms_SpectraList_exp = QtWidgets.QListWidget(self.msms_list_container_exp)
+ self.msms_SpectraList_exp = QtWidgets.QTableWidget(self.msms_list_container_exp)
self.msms_SpectraList_exp.setMinimumSize(QtCore.QSize(200, 0))
self.msms_SpectraList_exp.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+ self.msms_SpectraList_exp.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+ self.msms_SpectraList_exp.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
+ self.msms_SpectraList_exp.setColumnCount(6)
+ self.msms_SpectraList_exp.setHorizontalHeaderLabels(["N/L", "Prec. Abundance", "Prec. m/z", "RT (min)", "Group", "File"])
+ self.msms_SpectraList_exp.horizontalHeader().setStretchLastSection(True)
+ self.msms_SpectraList_exp.verticalHeader().setVisible(False)
+ self.msms_SpectraList_exp.setSortingEnabled(True)
self.msms_SpectraList_exp.setObjectName(_fromUtf8("msms_SpectraList_exp"))
self.verticalLayout_msms_exp.addWidget(self.msms_SpectraList_exp)
self.plMSMSWidget_exp = QtWidgets.QWidget(self.splitter_msms_exp)
@@ -2628,7 +2718,86 @@ def setupUi(self, MainWindow):
self.tableWidget_peakDetails_perGroup.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.gridLayout_peak_details.addWidget(self.splitter_peak_details, 0, 0, 1, 1)
self.tabWidget_3.addTab(self.tab_peak_details, _fromUtf8(""))
- self.gridLayout_40.addWidget(self.tabWidget_3, 3, 1, 1, 1)
+ self.tab_abundance_profiles = QtWidgets.QWidget()
+ self.tab_abundance_profiles.setObjectName(_fromUtf8("tab_abundance_profiles"))
+ self.gridLayout_abundance_profiles = QtWidgets.QGridLayout(self.tab_abundance_profiles)
+ self.gridLayout_abundance_profiles.setObjectName(_fromUtf8("gridLayout_abundance_profiles"))
+ self.horizontalLayout_abundance_controls = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_abundance_controls.setObjectName(_fromUtf8("horizontalLayout_abundance_controls"))
+ self.label_abundance_visualization = QtWidgets.QLabel(self.tab_abundance_profiles)
+ self.label_abundance_visualization.setObjectName(_fromUtf8("label_abundance_visualization"))
+ self.horizontalLayout_abundance_controls.addWidget(self.label_abundance_visualization)
+ self.comboBox_abundancePlotType = QtWidgets.QComboBox(self.tab_abundance_profiles)
+ self.comboBox_abundancePlotType.setObjectName(_fromUtf8("comboBox_abundancePlotType"))
+ self.comboBox_abundancePlotType.addItem(_fromUtf8(""))
+ self.comboBox_abundancePlotType.addItem(_fromUtf8(""))
+ self.horizontalLayout_abundance_controls.addWidget(self.comboBox_abundancePlotType)
+ self.label_abundance_scale = QtWidgets.QLabel(self.tab_abundance_profiles)
+ self.label_abundance_scale.setObjectName(_fromUtf8("label_abundance_scale"))
+ self.horizontalLayout_abundance_controls.addWidget(self.label_abundance_scale)
+ self.comboBox_abundanceScale = QtWidgets.QComboBox(self.tab_abundance_profiles)
+ self.comboBox_abundanceScale.setObjectName(_fromUtf8("comboBox_abundanceScale"))
+ self.comboBox_abundanceScale.addItem(_fromUtf8(""))
+ self.comboBox_abundanceScale.addItem(_fromUtf8(""))
+ self.horizontalLayout_abundance_controls.addWidget(self.comboBox_abundanceScale)
+ self.label_abundance_scaling = QtWidgets.QLabel(self.tab_abundance_profiles)
+ self.label_abundance_scaling.setObjectName(_fromUtf8("label_abundance_scaling"))
+ self.horizontalLayout_abundance_controls.addWidget(self.label_abundance_scaling)
+ self.comboBox_abundanceScalingMode = QtWidgets.QComboBox(self.tab_abundance_profiles)
+ self.comboBox_abundanceScalingMode.setObjectName(_fromUtf8("comboBox_abundanceScalingMode"))
+ self.comboBox_abundanceScalingMode.addItem(_fromUtf8(""))
+ self.comboBox_abundanceScalingMode.addItem(_fromUtf8(""))
+ self.comboBox_abundanceScalingMode.addItem(_fromUtf8(""))
+ self.comboBox_abundanceScalingMode.setCurrentIndex(2)
+ self.horizontalLayout_abundance_controls.addWidget(self.comboBox_abundanceScalingMode)
+ spacerItem74 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_abundance_controls.addItem(spacerItem74)
+ self.gridLayout_abundance_profiles.addLayout(self.horizontalLayout_abundance_controls, 0, 0, 1, 1)
+ self.resultsExperimentAbundance_widget = QtWidgets.QWidget(self.tab_abundance_profiles)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.resultsExperimentAbundance_widget.sizePolicy().hasHeightForWidth())
+ self.resultsExperimentAbundance_widget.setSizePolicy(sizePolicy)
+ self.resultsExperimentAbundance_widget.setMinimumSize(QtCore.QSize(700, 500))
+ self.resultsExperimentAbundance_widget.setObjectName(_fromUtf8("resultsExperimentAbundance_widget"))
+ self.gridLayout_abundance_profiles.addWidget(self.resultsExperimentAbundance_widget, 1, 0, 1, 1)
+ self.tabWidget_3.addTab(self.tab_abundance_profiles, _fromUtf8(""))
+
+ # Sample peaks tab
+ self.tab_sample_peaks = QtWidgets.QWidget()
+ self.tab_sample_peaks.setObjectName(_fromUtf8("tab_sample_peaks"))
+ self.gridLayout_sample_peaks = QtWidgets.QGridLayout(self.tab_sample_peaks)
+ self.gridLayout_sample_peaks.setObjectName(_fromUtf8("gridLayout_sample_peaks"))
+ # Navigation bar
+ self.horizontalLayout_sample_peaks_nav = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_sample_peaks_nav.setObjectName(_fromUtf8("horizontalLayout_sample_peaks_nav"))
+ self.pushButton_samplePeaksPrev = QtWidgets.QPushButton(self.tab_sample_peaks)
+ self.pushButton_samplePeaksPrev.setObjectName(_fromUtf8("pushButton_samplePeaksPrev"))
+ self.pushButton_samplePeaksPrev.setText(_fromUtf8("< Previous"))
+ self.pushButton_samplePeaksPrev.setEnabled(False)
+ self.horizontalLayout_sample_peaks_nav.addWidget(self.pushButton_samplePeaksPrev)
+ self.label_samplePeaksPage = QtWidgets.QLabel(self.tab_sample_peaks)
+ self.label_samplePeaksPage.setObjectName(_fromUtf8("label_samplePeaksPage"))
+ self.label_samplePeaksPage.setAlignment(QtCore.Qt.AlignCenter)
+ self.label_samplePeaksPage.setText(_fromUtf8(""))
+ self.horizontalLayout_sample_peaks_nav.addWidget(self.label_samplePeaksPage)
+ self.pushButton_samplePeaksNext = QtWidgets.QPushButton(self.tab_sample_peaks)
+ self.pushButton_samplePeaksNext.setObjectName(_fromUtf8("pushButton_samplePeaksNext"))
+ self.pushButton_samplePeaksNext.setText(_fromUtf8("Next >"))
+ self.pushButton_samplePeaksNext.setEnabled(False)
+ self.horizontalLayout_sample_peaks_nav.addWidget(self.pushButton_samplePeaksNext)
+ self.gridLayout_sample_peaks.addLayout(self.horizontalLayout_sample_peaks_nav, 0, 0, 1, 1)
+ self.scrollArea_sample_peaks = QtWidgets.QScrollArea(self.tab_sample_peaks)
+ self.scrollArea_sample_peaks.setWidgetResizable(True)
+ self.scrollArea_sample_peaks.setObjectName(_fromUtf8("scrollArea_sample_peaks"))
+ self.resultsExperimentSamplePeaks_widget = QtWidgets.QWidget()
+ self.resultsExperimentSamplePeaks_widget.setObjectName(_fromUtf8("resultsExperimentSamplePeaks_widget"))
+ self.scrollArea_sample_peaks.setWidget(self.resultsExperimentSamplePeaks_widget)
+ self.gridLayout_sample_peaks.addWidget(self.scrollArea_sample_peaks, 1, 0, 1, 1)
+ self.tabWidget_3.addTab(self.tab_sample_peaks, _fromUtf8(""))
+
+ self.exp_right_layout.addWidget(self.tabWidget_3, 2, 0, 1, 1)
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.gridLayout_2.addWidget(self.scrollArea, 0, 0, 1, 1)
self.tabWidget.addTab(self.bracketedResultsTab, _fromUtf8(""))
@@ -2659,6 +2828,7 @@ def setupUi(self, MainWindow):
self.version.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing)
self.version.setObjectName(_fromUtf8("version"))
self.gridLayout_11.addWidget(self.version, 2, 0, 1, 1)
+ self.version.setVisible(False)
MainWindow.setCentralWidget(self.centralWidget)
self.menuBar = QtWidgets.QMenuBar(MainWindow)
self.menuBar.setGeometry(QtCore.QRect(0, 0, 1954, 21))
@@ -2688,6 +2858,8 @@ def setupUi(self, MainWindow):
self.actionSet_working_directory.setObjectName(_fromUtf8("actionSet_working_directory"))
self.actionDownload_OBO_files = QtGui.QAction(MainWindow)
self.actionDownload_OBO_files.setObjectName(_fromUtf8("actionDownload_OBO_files"))
+ self.actionDownloadDBTemplate = QtGui.QAction(MainWindow)
+ self.actionDownloadDBTemplate.setObjectName(_fromUtf8("actionDownloadDBTemplate"))
self.openTempDir = QtGui.QAction(MainWindow)
self.openTempDir.setObjectName(_fromUtf8("openTempDir"))
self.actionShow_summary_of_previous_current_results = QtGui.QAction(MainWindow)
@@ -2705,6 +2877,7 @@ def setupUi(self, MainWindow):
self.menuTools.addSeparator()
self.menuTools.addAction(self.actionSet_working_directory)
self.menuTools.addAction(self.actionDownload_OBO_files)
+ self.menuTools.addAction(self.actionDownloadDBTemplate)
self.menuTools.addSeparator()
self.menuTools.addAction(self.openTempDir)
self.menuTools.addSeparator()
@@ -2791,7 +2964,9 @@ def setupUi(self, MainWindow):
MainWindow.setTabOrder(self.alignChromatograms, self.polynomValue)
MainWindow.setTabOrder(self.polynomValue, self.minConnectionRate)
MainWindow.setTabOrder(self.minConnectionRate, self.metaboliteClusterMinConnections)
- MainWindow.setTabOrder(self.metaboliteClusterMinConnections, self.integratedMissedPeaks)
+ MainWindow.setTabOrder(self.metaboliteClusterMinConnections, self.useAbundanceSimilarityForConvolution)
+ MainWindow.setTabOrder(self.useAbundanceSimilarityForConvolution, self.abundanceSimilarityThreshold)
+ MainWindow.setTabOrder(self.abundanceSimilarityThreshold, self.integratedMissedPeaks)
MainWindow.setTabOrder(self.integratedMissedPeaks, self.integrationMaxTimeDifference)
MainWindow.setTabOrder(self.integrationMaxTimeDifference, self.reintegrateIntensityCutoff)
MainWindow.setTabOrder(self.reintegrateIntensityCutoff, self.groupsSelectFile)
@@ -2822,6 +2997,7 @@ def retranslateUi(self, MainWindow):
self.loadGroups.setText(_translate("MainWindow", "Load Groups", None))
self.addGroup.setText(_translate("MainWindow", "New group", None))
self.removeGroup.setText(_translate("MainWindow", "Delete group", None))
+ self.showFileStats.setText(_translate("MainWindow", "File Stats", None))
self.label_60.setText(_translate("MainWindow", "Comments", None))
self.label_62.setText(_translate("MainWindow", "Please describe the performed experiment", None))
self.label_56.setText(_translate("MainWindow", "Positive", None))
@@ -3121,6 +3297,7 @@ def retranslateUi(self, MainWindow):
self.addmzVaultRep_pushButton.setText(_translate("MainWindow", "Add mzVault repository", None))
self.removeDB_pushButton.setText(_translate("MainWindow", "Remove annotation source", None))
self.generateDBTemplate_pushButton.setText(_translate("MainWindow", "Generate database template", None))
+ self.testDBs_pushButton.setText(_translate("MainWindow", "Test DBs", None))
self.label_79.setText(_translate("MainWindow", "Maximum retention time deviation", None))
self.maxRTErrorInHits_spinnerBox.setPrefix(_translate("MainWindow", "± ", None))
self.maxRTErrorInHits_spinnerBox.setSuffix(_translate("MainWindow", " minutes", None))
@@ -3133,7 +3310,7 @@ def retranslateUi(self, MainWindow):
self.label_98.setText(_translate("MainWindow", "positive mode", None))
self.annotation_correctMassByPPMposMode.setSuffix(_translate("MainWindow", " ppm", None))
self.label_99.setText(_translate("MainWindow", "negative mode", None))
- self.annotation_correctMassByPPMnegMode.setSuffix(_translate("MainWindow", "ppm", None))
+ self.annotation_correctMassByPPMnegMode.setSuffix(_translate("MainWindow", " ppm", None))
self.label_76.setText(_translate("MainWindow", "Use number of labeling atoms", None))
self.sumFormulasUseExactXn_ComboBox.setItemText(0, _translate("MainWindow", "Exact", None))
self.sumFormulasUseExactXn_ComboBox.setItemText(1, _translate("MainWindow", "Don't use", None))
@@ -3165,6 +3342,10 @@ def retranslateUi(self, MainWindow):
self.label_74.setText(_translate("MainWindow", "Minimum number of detections in files", None))
self.metaboliteClusterMinConnections.setPrefix(_translate("MainWindow", "≥ ", None))
self.useSILRatioForConvolution.setText(_translate("MainWindow", "Use SIL ratio", None))
+ self.useAbundanceSimilarityForConvolution.setText(_translate("MainWindow", "Use abundance similarity", None))
+ self.label_122.setText(_translate("MainWindow", "Abundance similarity threshold", None))
+ self.abundanceSimilarityThreshold.setPrefix(_translate("MainWindow", "≥ ", None))
+ self.abundanceSimilarityThreshold.setSuffix(_translate("MainWindow", " %", None))
self.integratedMissedPeaks.setTitle(_translate("MainWindow", "Integrate missing feature pairs", None))
self.label_55.setText(_translate("MainWindow", "Maximum time difference", None))
self.integrationMaxTimeDifference.setPrefix(_translate("MainWindow", "± ", None))
@@ -3309,6 +3490,24 @@ def retranslateUi(self, MainWindow):
self.tabWidget_3.indexOf(self.tab_peak_details),
_translate("MainWindow", "Peak details", None),
)
+ self.label_abundance_visualization.setText(_translate("MainWindow", "Visualization", None))
+ self.comboBox_abundancePlotType.setItemText(0, _translate("MainWindow", "Boxplot", None))
+ self.comboBox_abundancePlotType.setItemText(1, _translate("MainWindow", "Line plot", None))
+ self.label_abundance_scale.setText(_translate("MainWindow", "Scale", None))
+ self.comboBox_abundanceScale.setItemText(0, _translate("MainWindow", "Linear", None))
+ self.comboBox_abundanceScale.setItemText(1, _translate("MainWindow", "Logarithmic", None))
+ self.label_abundance_scaling.setText(_translate("MainWindow", "Normalization", None))
+ self.comboBox_abundanceScalingMode.setItemText(0, _translate("MainWindow", "None", None))
+ self.comboBox_abundanceScalingMode.setItemText(1, _translate("MainWindow", "Scale to max sample", None))
+ self.comboBox_abundanceScalingMode.setItemText(2, _translate("MainWindow", "Scale to max experimental group", None))
+ self.tabWidget_3.setTabText(
+ self.tabWidget_3.indexOf(self.tab_abundance_profiles),
+ _translate("MainWindow", "Abundance profiles", None),
+ )
+ self.tabWidget_3.setTabText(
+ self.tabWidget_3.indexOf(self.tab_sample_peaks),
+ _translate("MainWindow", "Sample peaks", None),
+ )
self.tabWidget.setTabText(
self.tabWidget.indexOf(self.bracketedResultsTab),
_translate("MainWindow", "Experiment results", None),
@@ -3330,6 +3529,7 @@ def retranslateUi(self, MainWindow):
self.actionIsotopic_enrichment.setText(_translate("MainWindow", "Isotopic enrichment", None))
self.actionSet_working_directory.setText(_translate("MainWindow", "Set working directory", None))
self.actionDownload_OBO_files.setText(_translate("MainWindow", "Download OBO files", None))
+ self.actionDownloadDBTemplate.setText(_translate("MainWindow", "Download database template", None))
self.openTempDir.setText(_translate("MainWindow", "Open temporary directory (logfile and caches)", None))
self.actionShow_summary_of_previous_current_results.setText(_translate("MainWindow", "Show overview of results", None))
diff --git a/src/mePyGuis/statisticsTab.py b/src/mePyGuis/statisticsTab.py
index ad73169..6693c62 100644
--- a/src/mePyGuis/statisticsTab.py
+++ b/src/mePyGuis/statisticsTab.py
@@ -28,6 +28,9 @@
QPushButton,
QScrollArea,
QSplitter,
+ QStyle,
+ QStyledItemDelegate,
+ QStyleOptionViewItem,
QTableWidgetItem,
QTreeWidget,
QTreeWidgetItem,
@@ -446,6 +449,20 @@ def __lt__(self, other):
return super().__lt__(other)
+class _BoldSelectedDelegate(QStyledItemDelegate):
+ """Renders selected rows bold without changing their background color."""
+
+ def paint(self, painter, option, index) -> None:
+ opt = QStyleOptionViewItem(option)
+ self.initStyleOption(opt, index)
+ if opt.state & QStyle.State_Selected:
+ opt.state &= ~QStyle.State_Selected
+ font = opt.font
+ font.setBold(True)
+ opt.font = font
+ super().paint(painter, opt, index)
+
+
class SelectedFeaturesTable(QTreeWidget):
"""Tree-like view for selected features, grouped by Group ID."""
@@ -458,7 +475,8 @@ def __init__(self, parent=None):
self.header().setSectionResizeMode(QHeaderView.ResizeToContents)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
- self.setStyleSheet("QTreeWidget::item:selected { background-color: rgba(80, 200, 80, 120); color: black; }")
+ self.setStyleSheet("")
+ self.setItemDelegate(_BoldSelectedDelegate(self))
self.setSortingEnabled(False)
self.feature_data = []
self._feature_item_by_id = {}
@@ -985,7 +1003,11 @@ def _show_rsd_plot(self):
rsd_clean = rsd[~np.isnan(rsd)]
if len(rsd_clean) > 0:
- ax.hist(rsd_clean, bins=30, alpha=0.7, color=group_colors[group_name], edgecolor="black")
+ n_bins = min(30, max(1, len(np.unique(rsd_clean))))
+ try:
+ ax.hist(rsd_clean, bins=n_bins, alpha=0.7, color=group_colors[group_name], edgecolor="black")
+ except ValueError:
+ ax.hist(rsd_clean, bins=1, alpha=0.7, color=group_colors[group_name], edgecolor="black")
ax.set_xlabel("RSD (%)", fontsize=8)
ax.set_ylabel("Frequency", fontsize=8)
@@ -1151,10 +1173,10 @@ def _show_pca(self):
ax.set_title(f"PCA Score Plot ({self.stats_data.num_features_used} features)")
ax.grid(True, alpha=0.3)
- # Add legend
+ # Add legend outside the plot area on the right side
legend_handles = [plt.Line2D([0], [0], marker="o", color="w", markerfacecolor=color, markersize=10, label=group) for group, color in group_colors.items()]
- ax.legend(handles=legend_handles, loc="best")
- canvas.fig.tight_layout()
+ ax.legend(handles=legend_handles, loc="upper left", bbox_to_anchor=(1.01, 1), borderaxespad=0, fontsize=8)
+ canvas.fig.tight_layout(rect=[0, 0, 0.82, 1])
self.viz_layout.addWidget(toolbar)
self.viz_layout.addWidget(canvas)
diff --git a/src/metaboliteGrouping.py b/src/metaboliteGrouping.py
new file mode 100644
index 0000000..0b2f54a
--- /dev/null
+++ b/src/metaboliteGrouping.py
@@ -0,0 +1,191 @@
+import numpy as np
+
+# Relative/absolute tolerances used when comparing normalized (shape-only) abundance
+# profiles in zero-variance fallback cases.
+NORMALIZED_PROFILE_RTOL = 1e-6
+NORMALIZED_PROFILE_ATOL = 1e-9
+
+
+def _coerce_abundance_profile(values):
+ profile = []
+ for value in values:
+ try:
+ numeric = float(value)
+ if np.isnan(numeric):
+ numeric = 0.0
+ except (TypeError, ValueError):
+ numeric = 0.0
+ profile.append(max(0.0, numeric))
+ return profile
+
+
+def _pearson(values_a, values_b):
+ if len(values_a) != len(values_b) or len(values_a) < 2:
+ return None
+ if np.std(values_a) == 0 or np.std(values_b) == 0:
+ max_a = max(values_a)
+ max_b = max(values_b)
+ if max_a > 0 and max_b > 0:
+ norm_a = [a / max_a for a in values_a]
+ norm_b = [b / max_b for b in values_b]
+ if np.allclose(norm_a, norm_b, rtol=NORMALIZED_PROFILE_RTOL, atol=NORMALIZED_PROFILE_ATOL):
+ return 1.0
+ return 0.0
+ corr = np.corrcoef(values_a, values_b)[0, 1]
+ if np.isnan(corr):
+ return None
+ return float(corr)
+
+
+def _presence_aware_profile_similarity(profile_a, profile_b, both_missing_weight=0.25):
+ if profile_a is None or profile_b is None:
+ return 0.0
+ if len(profile_a) < 2 or len(profile_b) < 2 or len(profile_a) != len(profile_b):
+ return 0.0
+
+ quantified_a = []
+ quantified_b = []
+ both_missing_count = 0
+
+ for value_a, value_b in zip(profile_a, profile_b):
+ has_a = value_a > 0.0
+ has_b = value_b > 0.0
+ if has_a and has_b:
+ quantified_a.append(value_a)
+ quantified_b.append(value_b)
+ elif (not has_a) and (not has_b):
+ both_missing_count += 1
+ # samples where only one feature has abundance are omitted completely
+
+ quantified_count = len(quantified_a)
+ if quantified_count == 0 and both_missing_count == 0:
+ return 0.0
+
+ quantified_corr = _pearson(quantified_a, quantified_b)
+ if quantified_corr is None:
+ quantified_corr = 1.0 if quantified_count == 1 else 0.0
+
+ weighted_sum = quantified_count * quantified_corr + both_missing_weight * both_missing_count
+ total_weight = quantified_count + both_missing_weight * both_missing_count
+ if total_weight <= 0:
+ return 0.0
+
+ return float(weighted_sum / total_weight)
+
+
+def _connected_components(group_ids, adjacency):
+ remaining = set(group_ids)
+ components = []
+ while remaining:
+ root = remaining.pop()
+ stack = [root]
+ component = {root}
+ while stack:
+ node = stack.pop()
+ for neighbor in adjacency.get(node, set()):
+ if neighbor in remaining:
+ remaining.remove(neighbor)
+ component.add(neighbor)
+ stack.append(neighbor)
+ components.append(component)
+ return components
+
+
+def _avg_similarity_to_group(node, group, similarities):
+ """Return the mean similarity from a node to all other members of a group."""
+ vals = []
+ for other in group:
+ if node == other:
+ continue
+ vals.append(similarities.get(node, {}).get(other, 0.0))
+ if len(vals) == 0:
+ return 0.0
+ return float(sum(vals) / len(vals))
+
+
+def _connection_rate_to_group(node, group, adjacency):
+ """Return the fraction of group members that are directly connected to node."""
+ if len(group) == 0:
+ return 0.0
+ degree = sum(1 for neighbor in adjacency.get(node, set()) if neighbor in group)
+ return float(degree) / float(len(group))
+
+
+def _split_component_by_dense_subclusters(component_ids, adjacency, similarities, min_connection_rate):
+ """Split a connected component into dense subclusters using connection-rate cores.
+
+ A dense core contains nodes that satisfy the minimum connection-rate criterion
+ within the full component. Remaining nodes are then assigned to the best-matching
+ core (if their connection rate to that core is sufficient), otherwise kept as
+ separate groups.
+ """
+ if len(component_ids) <= 2:
+ return [sorted(component_ids)]
+
+ component_set = set(component_ids)
+ min_required_connections = max(0.0, min_connection_rate) * (len(component_set) - 1)
+ dense_nodes = set()
+ for node in component_set:
+ degree = sum(1 for neighbor in adjacency.get(node, set()) if neighbor in component_set)
+ if degree >= min_required_connections:
+ dense_nodes.add(node)
+
+ # avoid over-splitting: if no node reaches the connection-rate threshold, keep this
+ # component intact rather than fragmenting nearly all nodes into singleton groups
+ if len(dense_nodes) == 0:
+ return [sorted(component_ids)]
+
+ dense_groups = _connected_components(dense_nodes, adjacency)
+ groups = [set(group) for group in dense_groups]
+
+ remaining = sorted(component_set - dense_nodes)
+ for node in remaining:
+ best_group = None
+ best_score = None
+ for group in groups:
+ if _connection_rate_to_group(node, group, adjacency) >= min_connection_rate:
+ score = _avg_similarity_to_group(node, group, similarities)
+ if best_score is None or score > best_score:
+ best_group = group
+ best_score = score
+ if best_group is not None:
+ best_group.add(node)
+ else:
+ groups.append({node})
+
+ return [sorted(group) for group in groups]
+
+
+def split_group_by_relative_abundance(group_ids, abundance_vectors, min_peak_correlation, min_connection_rate):
+ if len(group_ids) <= 2:
+ return [group_ids]
+
+ profiles = {}
+ for feature_id in group_ids:
+ if feature_id in abundance_vectors:
+ profiles[feature_id] = _coerce_abundance_profile(abundance_vectors[feature_id])
+
+ if len(profiles) < len(group_ids):
+ return [group_ids]
+ profile_lengths = {len(profile) for profile in profiles.values()}
+ if len(profile_lengths) != 1:
+ return [group_ids]
+
+ similarities = {feature_id: {} for feature_id in group_ids}
+ adjacency = {feature_id: set() for feature_id in group_ids}
+ for i in range(len(group_ids)):
+ for j in range(i + 1, len(group_ids)):
+ feature_a = group_ids[i]
+ feature_b = group_ids[j]
+ similarities[feature_a][feature_b] = _presence_aware_profile_similarity(profiles[feature_a], profiles[feature_b])
+ similarities[feature_b][feature_a] = similarities[feature_a][feature_b]
+ if similarities[feature_a][feature_b] >= min_peak_correlation:
+ adjacency[feature_a].add(feature_b)
+ adjacency[feature_b].add(feature_a)
+
+ components = _connected_components(group_ids, adjacency)
+ refined_groups = []
+ for component in components:
+ refined_groups.extend(_split_component_by_dense_subclusters(sorted(component), adjacency, similarities, min_connection_rate))
+
+ return sorted([sorted(group) for group in refined_groups], key=lambda group: (len(group), group), reverse=True)
diff --git a/src/reIntegration.py b/src/reIntegration.py
index ca7b03b..0884207 100644
--- a/src/reIntegration.py
+++ b/src/reIntegration.py
@@ -18,6 +18,7 @@
from __future__ import absolute_import, division, print_function
import gc
import logging
+import os
import time
import traceback
from multiprocessing import Manager, Pool
@@ -279,7 +280,7 @@ def processFile(self, params):
List of re-integration results
"""
startProc = time.time()
- logging.info(f" Reintegration started for file {self.forFile}")
+ logging.info(f" Reintegration started for file {os.path.basename(self.forFile)}")
if self.queue is not None:
self.queue.put(Bunch(pid=self.pID, mes="start"))
@@ -348,10 +349,10 @@ def processFile(self, params):
self.chromatogram.freeMe()
gc.collect()
- logging.info(f" Reintegration finished for file {self.forFile} ({(time.time() - startProc) / 60.0:.1f} minutes)")
+ logging.info(f" Reintegration finished for file {os.path.basename(self.forFile)} ({(time.time() - startProc) / 60.0:.1f} minutes)")
except Exception as e:
- logging.error(f"Error during reintegration of {self.forFile}: {e}")
+ logging.error(f"Error during reintegration of {os.path.basename(self.forFile)}: {e}")
traceback.print_exc()
finally:
diff --git a/src/resultsPostProcessing/searchDatabases.py b/src/resultsPostProcessing/searchDatabases.py
index 60afee0..bb52d4f 100644
--- a/src/resultsPostProcessing/searchDatabases.py
+++ b/src/resultsPostProcessing/searchDatabases.py
@@ -1,10 +1,11 @@
+import os
import sys
import csv
from copy import deepcopy
-from math import ceil
from ..formulaTools import formulaTools
import logging
from .. import LoggingSetup
+import polars as pl
sys.path.append("C:/development/PyMetExtract")
@@ -81,7 +82,7 @@ def __init__(self):
self.dbEntriesNeutral = []
self.dbEntriesMZ = []
- def addEntriesFromFile(self, dbName, dbFile, callBackCheckFunction=None):
+ def addEntriesFromFile(self, dbName, dbFile, callBackCheckFunction=None, error_collector=None):
imported = 0
notImported = 0
@@ -89,10 +90,26 @@ def addEntriesFromFile(self, dbName, dbFile, callBackCheckFunction=None):
fT = formulaTools()
+ # check if file exists
+
+ if not os.path.exists(dbFile):
+ logging.error("DB import error: File not found %s" % (dbFile))
+ raise Exception("DB import error: File not found %s" % (dbFile))
+
if dbFile.lower().endswith(".xlsx"):
- import polars as pl
+ df = None
+ try:
+ df = pl.read_excel(dbFile, sheet_name="Template")
+ except Exception:
+ try:
+ df = pl.read_excel(dbFile, sheet_name="Sheet 1")
+ except Exception:
+ try:
+ df = pl.read_excel(dbFile)
+ except Exception:
+ logging.error("DB import error: Could not read Excel file %s; tried sheets 'Template', 'Sheet 1' and default sheet" % (dbFile))
+ raise Exception("DB import error: Could not read Excel file %s; tried sheets 'Template', 'Sheet 1' and default sheet" % (dbFile))
- df = pl.read_excel(dbFile, sheet_name="Template")
# Build a list-of-lists interface compatible with the TSV path
header_row = list(df.columns)
data_rows = [[str(v) if v is not None else "" for v in row] for row in df.iter_rows()]
@@ -123,7 +140,14 @@ def _iter_rows():
num = row[headers["Num"]].strip().replace('"', "DOURBLEPRIME").replace("'", "PRIME").replace("\t", "TAB").replace("\n", "RETURN").replace("\r", "CarrRETURN").replace("#", "HASH")
name = row[headers["Name"]].strip().replace('"', "DOURBLEPRIME").replace("'", "PRIME").replace("\t", "TAB").replace("\n", "RETURN").replace("\r", "CarrRETURN").replace("#", "HASH")
sumFormula = row[headers["SumFormula"]].strip().replace('"', "DOURBLEPRIME").replace("'", "PRIME").replace("\t", "TAB").replace("\n", "RETURN").replace("\r", "CarrRETURN").replace("#", "HASH")
- rt_min = float(row[headers["Rt_min"]]) if row[headers["Rt_min"]] != "" else None
+ rt_min = None
+ try:
+ rt_min = float(row[headers["Rt_min"]]) if row[headers["Rt_min"]] != "" else None
+ except Exception:
+ _msg = " - Error row %d: The Rt_min value '%s' of the entry %s '%s' could not be parsed as a float, not using RT for this compound" % (rowi, row[headers["Rt_min"]], num, name)
+ logging.error(_msg)
+ if error_collector is not None:
+ error_collector.append(_msg)
mz = float(row[headers["MZ"]]) if row[headers["MZ"]] != "" else None
polarity = row[headers["IonisationMode"]].strip().replace('"', "DOURBLEPRIME").replace("'", "PRIME").replace("\t", "TAB").replace("\n", "RETURN").replace("\r", "CarrRETURN").replace("#", "HASH")
additionalInfo = {}
@@ -154,7 +178,10 @@ def _iter_rows():
entry_polarity = "+" if formula_charge > 0 else "-"
is_charged_formula = True
except Exception:
- logging.error("DB import error (%s, row: %d): The sumformula (%s) of the entry %s '%s' could not be parsed" % (dbName, rowi, sumFormula, num, name))
+ _msg = " - Error row %d: The sumformula (%s) of the entry %s '%s' could not be parsed" % (rowi, sumFormula, num, name)
+ logging.error(_msg)
+ if error_collector is not None:
+ error_collector.append(_msg)
notImported += 1
dbEntry = DBEntry(
@@ -184,19 +211,26 @@ def _iter_rows():
imported += 1
except Exception as ex:
- logging.error("DB import error: Could not import row %d (%s)" % (rowi, ex.message))
+ _msg = " - Error row %d: %s" % (dbName, rowi, ex)
+ logging.error(_msg)
+ if error_collector is not None:
+ error_collector.append(_msg)
notImported += 1
- logging.info(
- "Imported DB %s with %d entries (Current number of entries: %d)"
- % (
- dbName,
- len(self.dbEntriesMZ) + len(self.dbEntriesNeutral) - curEntriesCount,
- len(self.dbEntriesMZ) + len(self.dbEntriesNeutral),
- )
- )
if notImported > 0:
- logging.error("Not imported %d entries (see above errors)" % (notImported))
+ _summary_msg = "Warning: Not imported %d entries (see above errors)" % (notImported)
+ logging.error(_summary_msg)
+ if error_collector is not None:
+ error_collector.append(_summary_msg)
+
+ _summary_msg = " - Imported DB %s with %d entries" % (
+ dbName,
+ len(self.dbEntriesMZ) + len(self.dbEntriesNeutral) - curEntriesCount,
+ )
+ logging.info(_summary_msg)
+ if error_collector is not None:
+ error_collector.append(_summary_msg)
+
return imported, notImported
def optimizeDB(self):
@@ -205,55 +239,35 @@ def optimizeDB(self):
def _findGeneric(self, list, getValue, valueLeft, valueRight):
if len(list) == 0:
- return (-1, -1)
+ return []
- min = 0
- max = len(list)
+ # implement binary search to find a value in the sorted list between valueLeft and valueRight
+ left = 0
+ right = len(list) - 1
- while min < max and (max - min) > 1:
- cur = int(ceil((max + min) / 2.0))
+ while left <= right:
+ middle = (left + right) // 2
+ middleValue = getValue(list[middle])
- if valueLeft <= getValue(list[cur]) <= valueRight:
- leftBound = cur
- while leftBound > 0 and getValue(list[leftBound - 1]) >= valueLeft:
- leftBound -= 1
-
- rightBound = cur
- while (rightBound + 1) < len(list) and getValue(list[rightBound + 1]) <= valueRight:
- rightBound += 1
-
- return leftBound, rightBound
-
- if getValue(list[cur]) > valueRight:
- max = cur
+ if middleValue < valueLeft:
+ left = middle + 1
+ elif middleValue > valueRight:
+ right = middle - 1
else:
- min = cur
-
- cur = min
- if valueLeft <= getValue(list[cur]) <= valueRight:
- leftBound = cur
- while leftBound > 0 and getValue(list[leftBound - 1]) >= valueLeft:
- leftBound -= 1
-
- rightBound = cur
- while (rightBound + 1) < len(list) and getValue(list[rightBound + 1]) <= valueRight:
- rightBound += 1
-
- return leftBound, rightBound
-
- cur = len(list) - 1
- if valueLeft <= getValue(list[cur]) <= valueRight:
- leftBound = cur
- while leftBound > 0 and getValue(list[leftBound - 1]) >= valueLeft:
- leftBound -= 1
+ # find the leftmost and rightmost index with values between valueLeft and valueRight
+ leftIndex = middle
+ while leftIndex >= 0 and valueLeft <= getValue(list[leftIndex]) <= valueRight:
+ leftIndex -= 1
+ leftIndex += 1
- rightBound = cur
- while (rightBound + 1) < len(list) and getValue(list[rightBound + 1]) <= valueRight:
- rightBound += 1
+ rightIndex = middle
+ while rightIndex < len(list) and valueLeft <= getValue(list[rightIndex]) <= valueRight:
+ rightIndex += 1
+ rightIndex -= 1
- return leftBound, rightBound
+ return list[leftIndex : rightIndex + 1]
- return -1, -1
+ return []
def searchDBForMZ(
self,
@@ -290,15 +304,14 @@ def searchDBForMZ(
if polarity == adduct[2] and charges == adduct[3]:
mass = (mz - adduct[1]) * adduct[3] / adduct[4]
- ph = self._findGeneric(
+ entries = self._findGeneric(
self.dbEntriesNeutral,
lambda x: x.mass,
mass - mass * ppm / 1000000.0,
mass + mass * ppm / 1000000.0,
)
- if ph[0] != -1:
- for entryi in range(ph[0], ph[1] + 1):
- entry = self.dbEntriesNeutral[entryi]
+ if entries:
+ for entry in entries:
if rt_min is None or entry.rt_min is None or (abs(rt_min - entry.rt_min) <= rt_error):
elems = None
if entry.sumFormula != "":
@@ -319,16 +332,14 @@ def searchDBForMZ(
## search for charged DB entries by the provided mz value
- ph = self._findGeneric(
+ entries = self._findGeneric(
self.dbEntriesMZ,
lambda x: x.mz,
mz - mz * ppm / 1000000.0,
mz + mz * ppm / 1000000.0,
)
- if ph[0] != -1:
- for entryi in range(ph[0], ph[1] + 1):
- entry = self.dbEntriesMZ[entryi]
- print(entry)
+ if entries:
+ for entry in entries:
if entry.polarity == polarity and (rt_min is None or entry.rt_min is None or (abs(rt_min - entry.rt_min) <= rt_error)):
elems = None
if entry.sumFormula != "":
@@ -381,15 +392,14 @@ def searchDBForMass(
if charges == adduct[3]:
mz = mass * adduct[4] / adduct[3] + adduct[1]
- ph = self._findGeneric(
+ entries = self._findGeneric(
self.dbEntriesMZ,
lambda x: x.mz,
mz - mz * ppm / 1000000.0,
mz + mz * ppm / 1000000.0,
)
- if ph[0] != -1:
- for entryi in range(ph[0], ph[1] + 1):
- entry = self.dbEntriesMZ[entryi]
+ if entries:
+ for entry in entries:
if entry.polarity == adduct[2] and (rt_min is None or entry.rt_min is None or (abs(rt_min - entry.rt_min) <= rt_error)):
elems = None
if entry.sumFormula != "":
@@ -409,15 +419,14 @@ def searchDBForMass(
possibleHits.append(entry)
## search for non-charged DB entries by the provided mass
- ph = self._findGeneric(
+ entries = self._findGeneric(
self.dbEntriesNeutral,
lambda x: x.mass,
mass - mass * ppm / 1000000.0,
mass + mass * ppm / 1000000.0,
)
- if ph[0] != -1:
- for entryi in range(ph[0], ph[1] + 1):
- entry = self.dbEntriesNeutral[entryi]
+ if entries:
+ for entry in entries:
if rt_min is None or entry.rt_min is None or (abs(rt_min - entry.rt_min) <= rt_error):
elems = None
if entry.sumFormula != "":
@@ -439,15 +448,14 @@ def searchDBForMass(
## search for charged formula DB entries by directly matching the feature m/z
## (only when polarity matches; these entries were imported from formulas with explicit charge)
if mz is not None:
- ph = self._findGeneric(
+ entries = self._findGeneric(
self.dbEntriesMZ,
lambda x: x.mz,
mz - mz * ppm / 1000000.0,
mz + mz * ppm / 1000000.0,
)
- if ph[0] != -1:
- for entryi in range(ph[0], ph[1] + 1):
- entry = self.dbEntriesMZ[entryi]
+ if entries:
+ for entry in entries:
if entry.polarity == polarity and (rt_min is None or entry.rt_min is None or (abs(rt_min - entry.rt_min) <= rt_error)):
elems = None
if entry.sumFormula != "":
diff --git a/src/runIdentification.py b/src/runIdentification.py
deleted file mode 100644
index f238cbf..0000000
--- a/src/runIdentification.py
+++ /dev/null
@@ -1,3404 +0,0 @@
-# MetExtract II
-# Copyright (C) 2015
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-
-from __future__ import absolute_import, division, print_function
-import base64
-import logging
-import os
-import platform
-import time
-import traceback
-from . import HCA_general, Baseline, exportAsFeatureML
-from .utils import CallBackMethod, getNormRatio, getDBSuffix
-from .Chromatogram import Chromatogram
-from .formulaTools import formulaTools, getIsotopeMass
-from .mePyGuis.TracerEdit import ConfiguredTracer
-from .MZHCA import HierarchicalClustering, cutTreeSized
-from .PolarsDB import PolarsDB
-from .runIdentification_matchPartners import matchPartners
-from .SGR import SGRGenerator
-from .chromPeakPicking.peakpickers import filter_peaks
-import numpy as np
-import polars as pl
-import scipy
-from pickle import dumps
-from copy import copy
-from math import floor
-from .utils import (
- Bunch,
- ChromPeakPair,
- corr,
- getAtomAdd,
- getDBFormat,
- getSubGraphs,
- mean,
- sd,
- smoothDataSeries,
- weightedMean,
- weightedSd,
-)
-
-
-# returns the abbreviation for a given element
-# e.g. element="Carbon" --> return="C"
-def getShortName(element):
- fT = formulaTools()
- for i in fT.elemDetails:
- d = fT.elemDetails[i]
- if d[0] == element:
- return d[1]
- return ""
-
-
-# counts how ofter an entry i is listed in the vector x and returns it as a dictionary
-# e.g. x=[1,2,1,3,4,4,5,4] --> return={1:2, 2:1, 3:1, 4:3, 5:1}
-def countEntries(x):
- ret = {}
- for i in x:
- if i not in ret:
- ret[i] = 0
- ret[i] = ret[i] + 1
- return ret
-
-
-def getCorrelationShifted(xic1, xic2, peakStartMin, peakEndMin, rtShiftsMin=None):
- if rtShiftsMin is None:
- raise RuntimeError("parameter rtShiftsMin must be an array of retention time shifts in minutes, e.g., [-3.0, -2.6, -2.2, ... 2.2, 2.6, 3.0]")
-
- if type(rtShiftsMin) != list and type(rtShiftsMin) != np.ndarray:
- raise RuntimeError("parameter rtShiftsMin must be an array/numpy ndarray of retention time shifts in minutes, e.g., [-3.0, -2.6, -2.2, ... 2.2, 2.6, 3.0]")
-
- assert xic1.shape[0] == xic2.shape[0]
-
- corrs = []
- nonShiftCor = correlationFor(xic1, xic2, peakStartMin, peakEndMin)
-
- for rtShiftMini, rtShiftMin in enumerate(rtShiftsMin):
- ## shift second EIC by current rtShiftMin
- xic2_ = np.copy(xic2)
- xic2_[:, 0] = xic2_[:, 0] + rtShiftMin
-
- ## get new rts
- newRTs = np.sort(np.concatenate((xic1[:, 0], xic2_[:, 0])))
-
- xic1_ = interpolateToNewRTs(np.copy(xic1), np.arange(np.min(newRTs), np.max(newRTs) + 0.0001, 0.01))
- xic2_ = interpolateToNewRTs(xic2_, np.arange(np.min(newRTs), np.max(newRTs) + 0.0001, 0.01))
- ## calculate correlation using peak 1
- c = correlationFor(xic1_, xic2_, peakStartMin, peakEndMin)
- corrs.append(c)
-
- return nonShiftCor, np.max(corrs), rtShiftsMin[np.argmax(corrs)]
-
-
-def interpolateToNewRTs(xic, newRTs):
- newRTs = np.unique(newRTs)
-
- nxic = np.zeros((len(newRTs), 2), dtype=xic.dtype)
- for curPosN, rt in enumerate(newRTs):
- nxic[curPosN, 0] = rt
-
- if rt <= xic[0, 0]:
- nxic[curPosN, 1] = xic[0, 1]
-
- elif rt >= xic[xic.shape[0] - 1, 0]:
- nxic[curPosN, 1] = xic[xic.shape[0] - 1, 1]
- else:
- bestNextRtInd = np.argmin(np.abs(rt - xic[:, 0]))
-
- if xic[bestNextRtInd, 0] == rt:
- nxic[curPosN, :] = xic[bestNextRtInd, :]
-
- else:
- leftInd = None
- rightInd = None
-
- if xic[bestNextRtInd, 0] < rt:
- leftInd = bestNextRtInd
- rightInd = bestNextRtInd + 1
-
- elif xic[bestNextRtInd, 0] > rt:
- leftInd = bestNextRtInd - 1
- rightInd = bestNextRtInd
-
- assert leftInd is not None, "Branch should not exists..."
-
- nxic[curPosN, 1] = xic[leftInd, 1] + (xic[rightInd, 1] - xic[leftInd, 1]) / (xic[rightInd, 0] - xic[leftInd, 0]) * (rt - xic[leftInd, 0])
-
- return np.copy(nxic)
-
-
-def smooth(abundances):
- algorithm = "rollingaverage"
- if algorithm.lower() == "rollingaverage":
- kernel = np.ones(kernel_size) / kernel_size
- data_convolved = np.convolve(abundances, kernel, mode="same")
- return data_convolved
-
- elif algorithm.lower() == "savgol":
- window_length = self.smoothingwindow_length
- polynom_degree = self.smoothingpolynom_degree
- return scipy.signal.savgol_filter(abundances, window_length, polynom_degree)
-
- else:
- raise RuntimeError("Unknown algorithm '%s' for smoothing" % (algorithm))
-
-
-def correlationFor(xic1, xic2, peakStartMin, peakEndMin):
- algorithm = "pearson"
-
- leftInd = np.argmin(np.abs(xic1[:, 0] - peakStartMin))
- rightInd = np.argmin(np.abs(xic1[:, 0] - peakEndMin))
-
- if algorithm == "pearson":
- try:
- return corr(xic1[leftInd:rightInd, 1], xic2[leftInd:rightInd, 1])
- except Exception as ex:
- print("There was an error calculating the pearson correlation between the two provided XICs. these are")
- print("XIC 1")
- print(xic1)
- print("")
- print("XIC 2")
- print(xic2)
- print("")
- print("Peak start and end are", peakStartMin, peakEndMin)
- raise RuntimeError("Could not calcualte correlation: '%s'" % (ex))
-
- else:
- raise RuntimeError("Unknown algorithm '%s' for smoothing" % (algorithm))
-
-
-peakAbundanceUseSignals = 5
-peakAbundanceUseSignalsSides = int((peakAbundanceUseSignals - 1) / 2)
-
-
-# This class is used as a Command for each LC-HRMS file and is called by the multiprocessing module in MExtract.py
-# during the processing of each individual LC-HRMS data files
-class FindIsoPairs:
- # Constructor of the class, which stores the processing parameters
- # writeMZXML: 0001: 12C 0010: 12C-Iso 0100: 13C-Iso 1000: 13C
- def __init__(
- self,
- file,
- exOperator="",
- exExperimentID="",
- exComments="",
- exExperimentName="",
- writeFeatureML=False,
- writeTSV=False,
- writeMZXML=9,
- metabolisationExperiment=False,
- labellingisotopeA="12C",
- labellingisotopeB="13C",
- useCIsotopePatternValidation=False,
- xOffset=1.00335,
- minRatio=0,
- maxRatio=9999999,
- useRatio=False,
- configuredTracer=None,
- intensityThreshold=0,
- intensityCutoff=0,
- maxLoading=1,
- xCounts="",
- ppm=2.0,
- isotopicPatternCountLeft=2,
- isotopicPatternCountRight=2,
- lowAbundanceIsotopeCutoff=True,
- intensityThresholdIsotopologs=1000,
- intensityErrorN=0.25,
- intensityErrorL=0.25,
- purityN=0.99,
- purityL=0.99,
- minSpectraCount=1,
- clustPPM=8.0,
- chromPeakPPM=5.0,
- snrTh=1.0,
- scales=[1, 35],
- peakCenterError=5,
- peakScaleError=3,
- minPeakCorr=0.85,
- checkPeaksRatio=False,
- minPeaksRatio=0,
- maxPeaksRatio=99999999,
- checkPeakWidthFilter=False,
- minPeakWidth=0.0,
- maxPeakWidth=9999.0,
- minFWHM=0.0,
- maxFWHM=9999.0,
- calcIsoRatioNative=1,
- calcIsoRatioLabelled=-1,
- calcIsoRatioMoiety=1,
- startTime=2,
- stopTime=37,
- positiveScanEvent="None",
- negativeScanEvent="None",
- eicSmoothingWindow="None",
- eicSmoothingWindowSize=0,
- eicSmoothingPolynom=0,
- artificialMPshift_start=0,
- artificialMPshift_stop=0,
- correctCCount=True,
- minCorrelation=0.85,
- minCorrelationConnections=0.4,
- hAIntensityError=5.0,
- hAMinScans=3,
- adducts=[],
- elements=[],
- heteroAtoms=[],
- simplifyInSourceFragments=True,
- chromPeakFile=None,
- lock=None,
- queue=None,
- pID=-1,
- meVersion="NA",
- scanIndexOffset=0,
- peak_picker=None,
- peak_filter_config=None,
- ):
- # System
- self.lock = lock
- self.queue = queue
- self.pID = pID
- self.meVersion = meVersion
-
- self.peak_picker = peak_picker
- self.peak_filter_config = peak_filter_config
- # Check if configuredTracer is None when metabolisationExperiment is True
- if metabolisationExperiment and configuredTracer is None:
- self.printMessage("Metabolisation experiment requires a configured tracer, but none was provided.", type="error")
- raise Exception("Metabolisation experiment requires a configured tracer, but none was provided.")
-
- ma, ea = getIsotopeMass(configuredTracer.isotopeA if metabolisationExperiment else labellingisotopeA)
- mb, eb = getIsotopeMass(configuredTracer.isotopeB if metabolisationExperiment else labellingisotopeB)
- xOffset = mb - ma if metabolisationExperiment else xOffset
-
- if (not metabolisationExperiment and len(ea) > 0 and ea == eb) or metabolisationExperiment or (ea == "Hydrogen" and eb == "Deuterium"):
- pass
- else:
- self.printMessage("Labelling specifications are invalid..", type="warning")
- raise Exception("Labelling specifications are invalid..")
-
- if positiveScanEvent == "None" and negativeScanEvent == "None":
- self.printMessage("No scan event was specified..", type="warning")
- raise Exception("No scan event was specified..")
-
- self.file = file
- self.writeFeatureML = writeFeatureML
- self.writeTSV = writeTSV
- self.writeMZXML = writeMZXML
-
- # E. Experiment
- self.experimentOperator = exOperator
- self.experimentID = exExperimentID
- self.experimentComments = exComments
- self.experimentName = exExperimentName
-
- # 0. General
- self.startTime = startTime
- self.stopTime = stopTime
- self.positiveScanEvent = positiveScanEvent
- self.negativeScanEvent = negativeScanEvent
-
- self.metabolisationExperiment = metabolisationExperiment
- self.labellingElement = getShortName(ea)
- self.isotopeA = ma
- self.isotopeB = mb
- self.useCIsotopePatternValidation = useCIsotopePatternValidation
- self.minRatio = minRatio
- self.maxRatio = maxRatio
- self.useRatio = useRatio
-
- self.configuredTracer = configuredTracer
- if not self.metabolisationExperiment:
- self.configuredTracer = ConfiguredTracer(name="FML", id=0)
-
- # 1. Mass picking
- self.intensityThreshold = intensityThreshold
- self.intensityCutoff = intensityCutoff
- self.maxLoading = maxLoading
-
- self.xCounts = []
- self.xCountsString = xCounts
- a = xCounts.replace(" ", "").replace(";", ",").split(",")
- for j in a:
- if "-" in j:
- self.xCounts.extend(range(int(j[0 : j.find("-")]), int(j[j.find("-") + 1 : len(j)]) + 1))
- elif ":" in j:
- self.xCounts.extend(range(int(j[0 : j.find(":")]), int(j[j.find(":") + 1 : len(j)]) + 1))
- elif j != "":
- self.xCounts.append(int(j))
- self.xCounts = sorted(list(set(self.xCounts)))
-
- self.xOffset = xOffset
- self.scanIndexOffset = scanIndexOffset
- self.ppm = ppm
- self.isotopicPatternCountLeft = isotopicPatternCountLeft
- self.isotopicPatternCountRight = isotopicPatternCountRight
- self.lowAbundanceIsotopeCutoff = lowAbundanceIsotopeCutoff
- self.intensityThresholdIsotopologs = intensityThresholdIsotopologs
- self.intensityErrorN = intensityErrorN
- self.intensityErrorL = intensityErrorL
- self.purityN = purityN
- self.purityL = purityL
-
- # 2. Results clustering
- self.minSpectraCount = max(1, minSpectraCount)
- self.clustPPM = clustPPM
-
- # 3. Peak detection
- self.chromPeakPPM = chromPeakPPM
-
- if eicSmoothingWindow is None or eicSmoothingWindow == "" or eicSmoothingWindow == "none":
- eicSmoothingWindow = "None"
- self.eicSmoothingWindow = eicSmoothingWindow
- self.eicSmoothingWindowSize = eicSmoothingWindowSize
- self.eicSmoothingPolynom = eicSmoothingPolynom
- self.artificialMPshift_start = artificialMPshift_start
- self.artificialMPshift_stop = artificialMPshift_stop
- self.snrTh = snrTh
- self.scales = scales
- self.peakCenterError = peakCenterError
- self.peakScaleError = peakScaleError
- self.minPeakCorr = minPeakCorr
- self.checkPeaksRatio = checkPeaksRatio
- self.minPeaksRatio = minPeaksRatio
- self.maxPeaksRatio = maxPeaksRatio
- self.checkPeakWidthFilter = checkPeakWidthFilter
- self.minPeakWidth = minPeakWidth
- self.maxPeakWidth = maxPeakWidth
- self.minFWHM = minFWHM
- self.maxFWHM = maxFWHM
-
- self.calcIsoRatioNative = calcIsoRatioNative
- self.calcIsoRatioLabelled = calcIsoRatioLabelled
- self.calcIsoRatioMoiety = calcIsoRatioMoiety
-
- self.chromPeakFile = chromPeakFile # kept for backward compatibility (no longer used)
-
- self.performCorrectCCount = correctCCount
-
- self.minCorrelation = minCorrelation
- self.minCorrelationConnections = minCorrelationConnections
-
- self.hAIntensityError = hAIntensityError
- self.hAMinScans = hAMinScans
- self.adducts = adducts
- self.elements = {}
- for elem in elements:
- self.elements[elem.name] = elem
- self.heteroAtoms = {}
- for heteroAtom in heteroAtoms:
- self.heteroAtoms[heteroAtom.name] = heteroAtom
- self.simplifyInSourceFragments = simplifyInSourceFragments
-
- # Thread safe printing function
- def printMessage(self, message, type="info"):
- # Always print to stdout for debugging
- if self.lock is not None:
- self.lock.acquire()
- if type.lower() == "info":
- # logging.info(" %d: %s" % (self.pID, message))
- self.postMessageToProgressWrapper(mes="log", val=message)
- elif type.lower() == "warning":
- # logging.warning(" %d: %s" % (self.pID, message))
- self.postMessageToProgressWrapper(mes="log", val=message)
- elif type.lower() == "error":
- # logging.error(" %d: %s" % (self.pID, message))
- self.postMessageToProgressWrapper(mes="log", val=message)
- else:
- # logging.debug(" %d: %s" % (self.pID, message))
- self.postMessageToProgressWrapper(mes="log", val=message)
- self.lock.release()
-
- # helper function used to update the status of the current processing in the Process Dialog
- def postMessageToProgressWrapper(self, mes, val=""):
- # Always print to stdout for debugging
- if self.pID != -1 and self.queue is not None:
- if mes.lower() == "text":
- self.queue.put(Bunch(pid=self.pID, mes="text", val="%d: %s" % (self.pID, val)))
- elif mes == "value" or mes == "max":
- self.queue.put(Bunch(pid=self.pID, mes=mes, val=val))
- elif mes == "start" or mes == "end" or mes == "failed":
- self.queue.put(Bunch(pid=self.pID, mes=mes))
- elif mes == "log":
- self.queue.put(Bunch(pid=self.pID, mes=mes, val=mes))
-
- def getMostLikelyHeteroIsotope(self, foundIsotopes):
- if len(foundIsotopes) == 0:
- return
-
- for iso in foundIsotopes:
- isoD = foundIsotopes[iso]
- useCn = -1
- useCnRatio = 0.0
- useCnRatioTheo = 0.0
-
- for cn in isoD:
- cnF = isoD[cn]
-
- cnRatio = mean([x[1] for x in cnF])
- cnRatioTheo = mean([x[2] for x in cnF])
- len(cnF)
-
- if useCn == -1 or (abs(cnRatioTheo - cnRatio) < abs(useCnRatio - useCnRatioTheo)):
- useCn = cn
- useCnRatio = cnRatio
- useCnRatioTheo = cnRatioTheo
-
- foundIsotopes[iso] = {useCn: isoD[useCn]}
-
- # store configuration used for processing the LC-HRMS file into the database
- def writeConfigurationToDB(self, db_con):
- # db_con is a PolarsDB object
- # Create empty tables with appropriate schemas
- db_con.create_table("config", {"key": pl.Utf8, "value": pl.Utf8})
-
- db_con.create_table(
- "tracerConfiguration",
- {
- "id": pl.Int64,
- "name": pl.Utf8,
- "elementCount": pl.Int64,
- "natural": pl.Utf8,
- "labelling": pl.Utf8,
- "deltaMZ": pl.Float64,
- "purityN": pl.Float64,
- "purityL": pl.Float64,
- "amountN": pl.Float64,
- "amountL": pl.Float64,
- "monoisotopicRatio": pl.Float64,
- "checkRatio": pl.Utf8,
- "lowerError": pl.Float64,
- "higherError": pl.Float64,
- "tracertype": pl.Utf8,
- },
- )
-
- db_con.create_table("MZs", {"id": pl.Int64, "tracer": pl.Int64, "mz": pl.Float64, "lmz": pl.Float64, "tmz": pl.Float64, "xcount": pl.Int64, "scanid": pl.Int64, "scantime": pl.Float64, "loading": pl.Int64, "intensity": pl.Float64, "intensityL": pl.Float64, "ionMode": pl.Utf8})
-
- db_con.create_table("MZBins", {"id": pl.Int64, "mz": pl.Float64, "ionMode": pl.Utf8})
-
- db_con.create_table("MZBinsKids", {"mzbinID": pl.Int64, "mzID": pl.Int64})
-
- db_con.create_table(
- "XICs",
- {
- "id": pl.Int64,
- "avgmz": pl.Float64,
- "xcount": pl.Int64,
- "loading": pl.Int64,
- "polarity": pl.Utf8,
- "xic": pl.Utf8,
- "xic_smoothed": pl.Utf8,
- "xic_baseline": pl.Utf8,
- "xicL": pl.Utf8,
- "xicL_smoothed": pl.Utf8,
- "xicL_baseline": pl.Utf8,
- "xicfirstiso": pl.Utf8,
- "xicLfirstiso": pl.Utf8,
- "xicLfirstisoconjugate": pl.Utf8,
- "mzs": pl.Utf8,
- "mzsL": pl.Utf8,
- "mzsfirstiso": pl.Utf8,
- "mzsLfirstiso": pl.Utf8,
- "mzsLfirstisoconjugate": pl.Utf8,
- "times": pl.Utf8,
- "scanCount": pl.Int64,
- "allPeaks": pl.Utf8,
- },
- )
-
- db_con.create_table("tics", {"id": pl.Int64, "polarity": pl.Utf8, "scanevent": pl.Utf8, "times": pl.Utf8, "intensities": pl.Utf8})
-
- db_con.create_table(
- "chromPeaks",
- {
- "id": pl.Int64,
- "tracer": pl.Int64,
- "eicID": pl.Int64,
- "NPeakCenter": pl.Int64,
- "NPeakCenterMin": pl.Float64,
- "NPeakScale": pl.Float64,
- "NSNR": pl.Float64,
- "NPeakArea": pl.Float64,
- "NPeakAbundance": pl.Float64,
- "mz": pl.Float64,
- "lmz": pl.Float64,
- "tmz": pl.Float64,
- "xcount": pl.Int64,
- "xcountId": pl.Int64,
- "LPeakCenter": pl.Int64,
- "LPeakCenterMin": pl.Float64,
- "LPeakScale": pl.Float64,
- "LSNR": pl.Float64,
- "LPeakArea": pl.Float64,
- "LPeakAbundance": pl.Float64,
- "Loading": pl.Int64,
- "peaksCorr": pl.Float64,
- "heteroAtoms": pl.Utf8,
- "NBorderLeft": pl.Int64,
- "NBorderRight": pl.Int64,
- "LBorderLeft": pl.Int64,
- "LBorderRight": pl.Int64,
- "N_startRT": pl.Float64,
- "N_endRT": pl.Float64,
- "L_startRT": pl.Float64,
- "L_endRT": pl.Float64,
- "adducts": pl.Utf8,
- "heteroAtomsFeaturePairs": pl.Utf8,
- "massSpectrumID": pl.Int64,
- "ionMode": pl.Utf8,
- "assignedMZs": pl.Utf8,
- "fDesc": pl.Utf8,
- "peaksRatio": pl.Float64,
- "peaksRatioMp1": pl.Float64,
- "peaksRatioMPm1": pl.Float64,
- "isotopesRatios": pl.Utf8,
- "mzDiffErrors": pl.Utf8,
- "isotopologRatios": pl.Utf8,
- "peakType": pl.Utf8,
- "assignedName": pl.Utf8,
- "correlationsToOthers": pl.Utf8,
- "comments": pl.Utf8,
- "artificialEICLShift": pl.Int64,
- },
- )
-
- db_con.create_table(
- "allChromPeaks",
- {
- "id": pl.Int64,
- "tracer": pl.Int64,
- "eicID": pl.Int64,
- "NPeakCenter": pl.Int64,
- "NPeakCenterMin": pl.Float64,
- "NPeakScale": pl.Float64,
- "NSNR": pl.Float64,
- "NPeakArea": pl.Float64,
- "NPeakAbundance": pl.Float64,
- "mz": pl.Float64,
- "lmz": pl.Float64,
- "tmz": pl.Float64,
- "xcount": pl.Int64,
- "xcountId": pl.Int64,
- "LPeakCenter": pl.Int64,
- "LPeakCenterMin": pl.Float64,
- "LPeakScale": pl.Float64,
- "LSNR": pl.Float64,
- "LPeakArea": pl.Float64,
- "LPeakAbundance": pl.Float64,
- "Loading": pl.Int64,
- "peaksCorr": pl.Float64,
- "heteroAtoms": pl.Utf8,
- "NBorderLeft": pl.Int64,
- "NBorderRight": pl.Int64,
- "LBorderLeft": pl.Int64,
- "LBorderRight": pl.Int64,
- "N_startRT": pl.Float64,
- "N_endRT": pl.Float64,
- "L_startRT": pl.Float64,
- "L_endRT": pl.Float64,
- "adducts": pl.Utf8,
- "heteroAtomsFeaturePairs": pl.Utf8,
- "ionMode": pl.Utf8,
- "assignedMZs": pl.Int64,
- "fDesc": pl.Utf8,
- "peaksRatio": pl.Float64,
- "peaksRatioMp1": pl.Float64,
- "peaksRatioMPm1": pl.Float64,
- "isotopesRatios": pl.Utf8,
- "mzDiffErrors": pl.Utf8,
- "isotopologRatios": pl.Utf8,
- "peakType": pl.Utf8,
- "assignedName": pl.Utf8,
- "comment": pl.Utf8,
- "comments": pl.Utf8,
- "artificialEICLShift": pl.Int64,
- },
- )
-
- db_con.create_table("featureGroups", {"id": pl.Int64, "featureName": pl.Utf8, "tracer": pl.Int64})
-
- db_con.create_table("featureGroupFeatures", {"id": pl.Int64, "fID": pl.Int64, "fDesc": pl.Utf8, "fGroupID": pl.Int64})
-
- db_con.create_table("featurefeatures", {"fID1": pl.Int64, "fID2": pl.Int64, "corr": pl.Float64, "silRatioValue": pl.Float64, "desc1": pl.Utf8, "desc2": pl.Utf8, "add1": pl.Utf8, "add2": pl.Utf8})
-
- db_con.create_table("massspectrum", {"mID": pl.Int64, "fgID": pl.Int64, "time": pl.Float64, "mzs": pl.Utf8, "intensities": pl.Utf8, "ionMode": pl.Utf8})
-
- db_con.create_table("stats", {"key": pl.Utf8, "value": pl.Utf8})
-
- db_con.insert_row("config", {"key": "MetExtractVersion", "value": self.meVersion})
-
- db_con.insert_row("config", {"key": "ExperimentName", "value": self.experimentName})
- db_con.insert_row("config", {"key": "ExperimentOperator", "value": self.experimentOperator})
- db_con.insert_row("config", {"key": "ExperimentID", "value": self.experimentID})
- db_con.insert_row("config", {"key": "ExperimentComments", "value": self.experimentComments})
-
- db_con.insert_row("config", {"key": "labellingElement", "value": self.labellingElement})
- db_con.insert_row("config", {"key": "isotopeA", "value": self.isotopeA})
- db_con.insert_row("config", {"key": "isotopeB", "value": self.isotopeB})
- db_con.insert_row("config", {"key": "useCValidation", "value": self.useCIsotopePatternValidation})
- db_con.insert_row("config", {"key": "minRatio", "value": self.minRatio})
- db_con.insert_row("config", {"key": "maxRatio", "value": self.maxRatio})
- db_con.insert_row("config", {"key": "useRatio", "value": str(self.useRatio)})
- db_con.insert_row("config", {"key": "metabolisationExperiment", "value": str(self.metabolisationExperiment)})
- db_con.insert_row("config", {"key": "configuredTracer", "value": base64.b64encode(dumps(self.configuredTracer)).decode("utf-8")})
- db_con.insert_row("config", {"key": "startTime", "value": self.startTime})
- db_con.insert_row("config", {"key": "stopTime", "value": self.stopTime})
- db_con.insert_row("config", {"key": "positiveScanEvent", "value": self.positiveScanEvent})
- db_con.insert_row("config", {"key": "negativeScanEvent", "value": self.negativeScanEvent})
- db_con.insert_row("config", {"key": "intensityThreshold", "value": self.intensityThreshold})
- db_con.insert_row("config", {"key": "intensityCutoff", "value": self.intensityCutoff})
- db_con.insert_row("config", {"key": "maxLoading", "value": self.maxLoading})
- db_con.insert_row("config", {"key": "xCounts", "value": self.xCountsString})
- db_con.insert_row("config", {"key": "xOffset", "value": self.xOffset})
- db_con.insert_row("config", {"key": "scanIndexOffset", "value": self.scanIndexOffset})
- db_con.insert_row("config", {"key": "ppm", "value": self.ppm})
- db_con.insert_row("config", {"key": "isotopicPatternCountLeft", "value": self.isotopicPatternCountLeft})
- db_con.insert_row("config", {"key": "isotopicPatternCountRight", "value": self.isotopicPatternCountRight})
- db_con.insert_row("config", {"key": "lowAbundanceIsotopeCutoff", "value": str(self.lowAbundanceIsotopeCutoff)})
- db_con.insert_row("config", {"key": "intensityThresholdIsotopologs", "value": str(self.intensityThresholdIsotopologs)})
- db_con.insert_row("config", {"key": "intensityErrorN", "value": self.intensityErrorN})
- db_con.insert_row("config", {"key": "intensityErrorL", "value": self.intensityErrorL})
- db_con.insert_row("config", {"key": "purityN", "value": self.purityN})
- db_con.insert_row("config", {"key": "purityL", "value": self.purityL})
- db_con.insert_row("config", {"key": "clustPPM", "value": self.clustPPM})
- db_con.insert_row("config", {"key": "chromPeakPPM", "value": self.chromPeakPPM})
- db_con.insert_row("config", {"key": "eicSmoothing", "value": self.eicSmoothingWindow})
- db_con.insert_row("config", {"key": "eicSmoothingWindowSize", "value": self.eicSmoothingWindowSize})
- db_con.insert_row("config", {"key": "eicSmoothingPolynom", "value": self.eicSmoothingPolynom})
- db_con.insert_row("config", {"key": "artificialMPshift_start", "value": self.artificialMPshift_start})
- db_con.insert_row("config", {"key": "artificialMPshift_stop", "value": self.artificialMPshift_stop})
- db_con.insert_row("config", {"key": "peakAbundanceCriteria", "value": "Center +- %d signals (%d total)" % (peakAbundanceUseSignalsSides, peakAbundanceUseSignals)})
- db_con.insert_row("config", {"key": "minPeakCorr", "value": self.minPeakCorr})
- db_con.insert_row("config", {"key": "checkPeaksRatio", "value": str(self.checkPeaksRatio)})
- db_con.insert_row("config", {"key": "minPeaksRatio", "value": self.minPeaksRatio})
- db_con.insert_row("config", {"key": "maxPeaksRatio", "value": self.maxPeaksRatio})
- db_con.insert_row("config", {"key": "calcIsoRatioNative", "value": self.calcIsoRatioNative})
- db_con.insert_row("config", {"key": "calcIsoRatioLabelled", "value": self.calcIsoRatioLabelled})
- db_con.insert_row("config", {"key": "calcIsoRatioMoiety", "value": self.calcIsoRatioMoiety})
- db_con.insert_row("config", {"key": "minSpectraCount", "value": self.minSpectraCount})
- db_con.insert_row("config", {"key": "configuredHeteroAtoms", "value": base64.b64encode(dumps(self.heteroAtoms)).decode("utf-8")})
- db_con.insert_row("config", {"key": "haIntensityError", "value": self.hAIntensityError})
- db_con.insert_row("config", {"key": "haMinScans", "value": self.hAMinScans})
- db_con.insert_row("config", {"key": "minCorrelation", "value": self.minCorrelation})
- db_con.insert_row("config", {"key": "minCorrelationConnections", "value": self.minCorrelationConnections})
- db_con.insert_row("config", {"key": "adducts", "value": base64.b64encode(dumps(self.adducts)).decode("utf-8")})
- db_con.insert_row("config", {"key": "elements", "value": base64.b64encode(dumps(self.elements)).decode("utf-8")})
- db_con.insert_row("config", {"key": "simplifyInSourceFragments", "value": str(self.simplifyInSourceFragments)})
-
- import datetime
- import uuid
-
- self.processingUUID = "%s_%s_%s" % (
- str(uuid.uuid1()),
- str(platform.node()),
- str(datetime.datetime.now()),
- )
- db_con.insert_row("config", {"key": "processingUUID_ext", "value": base64.b64encode(str(self.processingUUID).encode("utf-8"))})
-
- i = 1
- if self.metabolisationExperiment:
- tracer = self.configuredTracer
- tracer.id = i
- db_con.insert_row(
- "tracerConfiguration",
- {
- "id": tracer.id,
- "name": tracer.name,
- "elementCount": tracer.elementCount,
- "natural": tracer.isotopeA,
- "labelling": tracer.isotopeB,
- "deltaMZ": getIsotopeMass(tracer.isotopeB)[0] - getIsotopeMass(tracer.isotopeA)[0],
- "purityN": tracer.enrichmentA,
- "purityL": tracer.enrichmentB,
- "amountN": tracer.amountA,
- "amountL": tracer.amountB,
- "monoisotopicRatio": tracer.monoisotopicRatio,
- "checkRatio": str(tracer.checkRatio),
- "lowerError": tracer.maxRelNegBias,
- "higherError": tracer.maxRelPosBias,
- "tracertype": tracer.tracerType,
- },
- )
-
- else:
- # ConfiguredTracer(name="Full metabolome labeling experiment", id=0)
- db_con.insert_row("tracerConfiguration", {"id": 0, "name": "FLE"})
- db_con.commit()
-
- def parseMzXMLFile(self):
- mzxml = Chromatogram()
- mzxml.parse_file(self.file, intensityCutoff=self.intensityCutoff)
- return mzxml
-
- # data processing step 1: searches each mass spectrum for isotope patterns of native and highly isotope enriched
- # metabolite ions. The actual calculation and processing of the data is performed in the file runIdentification_matchPartners.py.
- # The positive and negative ionisation modes are processed separately.
- def findSignalPairs(self, curProgress, mzxml, tracer, reportFunction=None):
- mzs = []
- posFound = 0
- negFound = 0
-
- checkRatio = False
- minRatio = 0.0
- maxRatio = 0.0
-
- if self.metabolisationExperiment:
- checkRatio = tracer.checkRatio
- minRatio = tracer.monoisotopicRatio * tracer.maxRelNegBias
- maxRatio = tracer.monoisotopicRatio * tracer.maxRelPosBias
- else:
- checkRatio = self.useRatio
- minRatio = self.minRatio
- maxRatio = self.maxRatio
-
- def reportFunctionHelper(curVal, text):
- reportFunction(curVal, text)
-
- if self.positiveScanEvent != "None":
- if self.negativeScanEvent != "None":
-
- def reportFunctionHelper(curVal, text):
- reportFunction(curVal / 2, text)
-
- p = matchPartners(
- mzXMLData=mzxml,
- forFile=self.file,
- labellingElement=self.labellingElement,
- useCIsotopePatternValidation=self.useCIsotopePatternValidation,
- intensityThres=self.intensityThreshold,
- isotopologIntensityThres=self.intensityThresholdIsotopologs,
- maxLoading=self.maxLoading,
- xCounts=self.xCounts,
- xOffset=self.xOffset,
- ppm=self.ppm,
- intensityErrorN=self.intensityErrorN,
- intensityErrorL=self.intensityErrorL,
- purityN=tracer.enrichmentA if self.metabolisationExperiment else self.purityN,
- purityL=tracer.enrichmentB if self.metabolisationExperiment else self.purityL,
- startTime=self.startTime,
- stopTime=self.stopTime,
- filterLine=self.positiveScanEvent,
- ionMode="+",
- peakCountLeft=self.isotopicPatternCountLeft,
- peakCountRight=self.isotopicPatternCountRight,
- lowAbundanceIsotopeCutoff=self.lowAbundanceIsotopeCutoff,
- metabolisationExperiment=self.metabolisationExperiment,
- checkRatio=checkRatio,
- minRatio=minRatio,
- maxRatio=maxRatio,
- scanIndexOffset=self.scanIndexOffset,
- reportFunction=reportFunctionHelper,
- )
- posFound = len(p)
- mzs.extend(p)
-
- def reportFunctionHelper(curVal, text):
- reportFunction(curVal, text)
-
- if self.negativeScanEvent != "None":
- if self.positiveScanEvent != "None":
-
- def reportFunctionHelper(curVal, text):
- reportFunction(0.5 + curVal / 2, text)
-
- n = matchPartners(
- mzXMLData=mzxml,
- forFile=self.file,
- labellingElement=self.labellingElement,
- useCIsotopePatternValidation=self.useCIsotopePatternValidation,
- intensityThres=self.intensityThreshold,
- isotopologIntensityThres=self.intensityThresholdIsotopologs,
- maxLoading=self.maxLoading,
- xCounts=self.xCounts,
- xOffset=self.xOffset,
- ppm=self.ppm,
- intensityErrorN=self.intensityErrorN,
- intensityErrorL=self.intensityErrorL,
- purityN=tracer.enrichmentA if self.metabolisationExperiment > 1 else self.purityN,
- purityL=tracer.enrichmentB if self.metabolisationExperiment > 1 else self.purityL,
- startTime=self.startTime,
- stopTime=self.stopTime,
- filterLine=self.negativeScanEvent,
- ionMode="-",
- peakCountLeft=self.isotopicPatternCountLeft,
- peakCountRight=self.isotopicPatternCountRight,
- lowAbundanceIsotopeCutoff=self.lowAbundanceIsotopeCutoff,
- metabolisationExperiment=self.metabolisationExperiment,
- checkRatio=checkRatio,
- minRatio=minRatio,
- maxRatio=maxRatio,
- scanIndexOffset=self.scanIndexOffset,
- reportFunction=reportFunctionHelper,
- )
- negFound = len(n)
- mzs.extend(n)
-
- return mzs, negFound, posFound
-
- # store detected signal pairs (1st data processing step) in the database
- def writeSignalPairsToDB(self, mzs, mzxml, tracerID):
- db_con = PolarsDB(self.file + getDBSuffix(), format=getDBFormat())
-
- for mz in mzs:
- mz.id = self.curMZId
- mz.tid = tracerID
-
- scanEvent = ""
- if mz.ionMode == "+":
- scanEvent = self.positiveScanEvent
- elif mz.ionMode == "-":
- scanEvent = self.negativeScanEvent
-
- db_con.insert_row(
- "MZs",
- {
- "id": mz.id,
- "tracer": mz.tid,
- "mz": mz.mz,
- "lmz": mz.lmz,
- "tmz": mz.tmz,
- "xcount": mz.xCount,
- "scanid": mzxml.getIthMS1Scan(mz.scanIndex, scanEvent).id,
- "scanTime": mzxml.getIthMS1Scan(mz.scanIndex, scanEvent).retention_time,
- "loading": mz.loading,
- "intensity": mz.nIntensity,
- "intensityL": mz.lIntensity,
- "ionMode": mz.ionMode,
- },
- )
-
- self.curMZId = self.curMZId + 1
-
- db_con.commit()
- db_con.close()
-
- # data processing step 2: cluster detected signal pairs with H
- def clusterFeaturePairs(self, mzs, reportFunction=None):
- mzbins = {}
- mzbins["+"] = []
- mzbins["-"] = []
-
- uniquexCounts = list(set([mz.xCount for mz in mzs]))
-
- # cluster each detected number of carbon atoms separately
- donei = 0
- for xCount in uniquexCounts:
- if reportFunction is not None:
- reportFunction(donei / len(uniquexCounts), "Current Xn: %s" % xCount)
- # cluster each detected number of loadings separately
- for loading in range(self.maxLoading, 0, -1):
- for ionMode in ["+", "-"]:
- xAct = sorted(
- [mz for mz in mzs if mz.ionMode == ionMode and mz.xCount == xCount and mz.loading == loading],
- key=lambda x: x.mz,
- )
-
- doClusterings = []
-
- if len(xAct) > 0:
- lastUnused = 0
- usedCount = 0
-
- # non-consecutive mz regions are prematurely separated without HCA to reduce the number of
- # signal pairs for HCA (faster processing)
- for i in range(1, len(xAct)):
- ppmDiff = (xAct[i].mz - xAct[i - 1].mz) * 1000000 / xAct[i - 1].mz
- if ppmDiff > self.clustPPM:
- doClusterings.append(xAct[lastUnused:i])
-
- usedCount = usedCount + i - lastUnused
- lastUnused = i
-
- # cluster remainign signal pairs
- if lastUnused < len(xAct):
- doClusterings.append(xAct[lastUnused : len(xAct)])
- usedCount = usedCount + len(xAct) - lastUnused
-
- assert usedCount == len(xAct)
-
- temp = doClusterings
- doClusterings = []
- for t in temp:
- xAct = sorted([mz for mz in t], key=lambda x: x.scanIndex)
-
- lastUnused = 0
- usedCount = 0
-
- for i in range(1, len(xAct)):
- scanDiff = xAct[i].scanIndex - xAct[i - 1].scanIndex
- if scanDiff > 20:
- doClusterings.append(xAct[lastUnused:i])
-
- usedCount = usedCount + i - lastUnused
- lastUnused = i
-
- # cluster remainign signal pairs
- if lastUnused < len(xAct):
- doClusterings.append(xAct[lastUnused : len(xAct)])
- usedCount = usedCount + len(xAct) - lastUnused
-
- assert usedCount == len(xAct)
-
- for doClustering in doClusterings:
- hc = HierarchicalClustering(
- doClustering,
- dist=lambda x, y: x.getValue() - y.getValue(),
- val=lambda x: x.mz,
- mean=lambda x, y: x / y,
- add=lambda x, y: x + y,
- )
-
- for n in cutTreeSized(hc.getTree(), self.clustPPM):
- mzbins[ionMode].append(n)
-
- return mzbins
-
- # store signal pair clusters (2nd data processing step) in the database
- def writeFeaturePairClustersToDB(self, mzbins):
- db_con = PolarsDB(self.file + getDBSuffix(), format=getDBFormat())
-
- for ionMode in ["+", "-"]:
- for mzbin in mzbins[ionMode]:
- db_con.insert_row("MZBins", {"id": self.curMZBinId, "mz": mzbin.getValue(), "ionMode": ionMode})
- for kid in mzbin.getKids():
- db_con.insert_row("MZBinsKids", {"mzbinID": self.curMZBinId, "mzID": kid.getObject().id})
- self.curMZBinId = self.curMZBinId + 1
-
- db_con.commit()
- db_con.close()
-
- def removeImpossibleFeaturePairClusters(self, mzbins):
- mzBinsNew = {}
- for ionMode in mzbins.keys():
- mzBinsNew[ionMode] = [mzbin for mzbin in mzbins[ionMode] if len(mzbin.getKids()) >= self.minSpectraCount]
- return mzBinsNew
-
- # EXPERIMENTAL: calulcate the mean intensity ratio of two chromatographic peaks at the same retention time
- # in two different EICs. Thus, the internal standardisation is not calculated using the peak area
- # but rather the ratio of each MS peak that contributes to the chromatographic peak.
- def getMeanRatioOfScans(self, eicA, eicB, lib, rib, perfWeighted=True, minInt=1000, minRatiosNecessary=3):
- try:
- if perfWeighted:
- sumEIC = sum(eicA[lib:rib])
- sumEICL = sum(eicB[lib:rib])
-
- normEIC = eicA
- if sumEICL > sumEIC:
- normEIC = eicB
-
- os = [o for o in range(lib, rib) if eicA[o] >= minInt and eicB[o] >= minInt and eicB[o] > 0]
- normSum = sum([normEIC[o] for o in os])
- weights = [1.0 * normEIC[o] / normSum for o in os]
- ratios = [1.0 * eicA[o] / eicB[o] for o in os if eicB[o] > minInt and eicB[o] > 0]
-
- if len(ratios) >= minRatiosNecessary:
- assert len(ratios) == len(weights)
- ratio = sum([ratios[o] * weights[o] for o in range(len(ratios))])
- sum([ratios[o] * weights[o] for o in range(len(ratios))])
- return ratio
- else:
- return -1
- else:
- ratios = [(eicA[o] / eicB[o]) for o in range(lib, rib) if eicA[o] >= minInt and eicB[o] >= minInt]
- if len(ratios) >= minRatiosNecessary:
- return mean(ratios)
- else:
- return -1
-
- except IndexError:
- return -1
-
- # data processing step 3: for each signal pair cluster extract the EICs, detect chromatographic peaks
- # present in both EICs at approximately the same retention time and very their chromatographic peak shapes.
- # if all criteria are passed, write this detected feature pair to the database
- def findChromatographicPeaksAndWriteToDB(self, mzbins, mzxml, tracerID, reportFunction=None):
- db_con = PolarsDB(self.file + getDBSuffix(), format=getDBFormat())
- chromPeaks = []
-
- totalBins = len(mzbins["+"]) + len(mzbins["-"])
-
- # process each signal pair cluster
- for ionMode in ["+", "-"]:
- if ionMode == "+":
- scanEvent = self.positiveScanEvent
- elif ionMode == "-":
- scanEvent = self.negativeScanEvent
- else:
- raise Exception("Unknown Ion Mode")
-
- if scanEvent != "None":
- for mzbin, i in zip(mzbins[ionMode], range(0, len(mzbins[ionMode]))):
- try:
- if reportFunction is not None:
- doneBins = i
- if ionMode == "-":
- doneBins += len(mzbins["+"])
-
- reportFunction(
- 1.0 * doneBins / totalBins,
- "%d mzbins remaining" % (totalBins - doneBins),
- )
-
- kids = mzbin.getKids()
- if len(kids) < self.minSpectraCount:
- continue
-
- # calulcate mean mz value for this signal pair cluster
- meanmz = weightedMean(
- [kid.getObject().mz for kid in kids],
- [kid.getObject().nIntensity for kid in kids],
- )
- meanmzLabelled = weightedMean(
- [kid.getObject().lmz for kid in kids],
- [kid.getObject().lIntensity for kid in kids],
- )
- meantmz = weightedMean(
- [kid.getObject().tmz for kid in kids],
- [kid.getObject().lIntensity for kid in kids],
- )
-
- xcount = kids[0].getObject().xCount
- assert all([kid.getObject().xCount == xcount for kid in kids])
-
- loading = kids[0].getObject().loading
- assert all([kid.getObject().loading == loading for kid in kids])
-
- # extract the EIC of the native ion and detect its chromatographic peaks
- # optionally: smoothing
- eic, times, scanIds, mzs = mzxml.getEIC(meanmz, self.chromPeakPPM, filterLine=scanEvent)
- eicBaseline = self.BL.getBaseline(copy(eic), times)
- eicSmoothed = smoothDataSeries(
- times,
- copy(eic),
- windowLen=self.eicSmoothingWindowSize,
- window=self.eicSmoothingWindow,
- polynom=self.eicSmoothingPolynom,
- )
- # extract the EIC of the labelled ion and detect its chromatographic peaks
- # optionally: smoothing
- eicL, times, scanIds, mzsL = mzxml.getEIC(meanmzLabelled, self.chromPeakPPM, filterLine=scanEvent)
- eicLBaseline = self.BL.getBaseline(copy(eicL), times)
- eicLSmoothed = smoothDataSeries(
- times,
- copy(eicL),
- windowLen=self.eicSmoothingWindowSize,
- window=self.eicSmoothingWindow,
- polynom=self.eicSmoothingPolynom,
- )
-
- ## TODO needs to be optimized
- # startIndex=max(0, minInd-int(ceil(self.scales[1]*10)))
- # endIndex=min(len(eic)-1, int(ceil(maxInd+self.scales[1]*10)))
- startIndex = 0
- endIndex = len(eic) - 1
-
- # Use unified peak picker and filter config
- peaksN = self.peak_picker.getPeaksFor(times, eicSmoothed, startIndex=startIndex, endIndex=endIndex)
- peaksL = self.peak_picker.getPeaksFor(times, eicLSmoothed, startIndex=startIndex, endIndex=endIndex)
-
- if self.peak_filter_config is not None:
- peaksN = filter_peaks(peaksN, config=self.peak_filter_config)
- peaksL = filter_peaks(peaksL, config=self.peak_filter_config)
-
- # get EICs of M+1, M'-1 and M'+1 for the database
- eicfirstiso, timesL, scanIdsL, mzsfirstiso = mzxml.getEIC(
- meanmz + 1.00335484 / loading,
- self.chromPeakPPM,
- filterLine=scanEvent,
- )
- # eicfirstisoSmoothed = smoothDataSeries(times, eicfirstiso, windowLen=self.eicSmoothingWindowSize, window=self.eicSmoothingWindow, polynom=self.eicSmoothingPolynom)
-
- eicLfirstiso, timesL, scanIdsL, mzsLfirstiso = mzxml.getEIC(
- meanmzLabelled - 1.00335484 / loading,
- self.chromPeakPPM,
- filterLine=scanEvent,
- )
- # eicLfirstisoSmoothed = smoothDataSeries(times, eicLfirstiso, windowLen=self.eicSmoothingWindowSize, window=self.eicSmoothingWindow, polynom=self.eicSmoothingPolynom)
-
- eicLfirstisoconjugate, timesL, scanIdsL, mzsLfirstisoconjugate = mzxml.getEIC(
- meanmzLabelled + 1.00335484 / loading,
- self.chromPeakPPM,
- filterLine=scanEvent,
- )
- # eicLfirstisoconjugateSmoothed = smoothDataSeries(times, eicLfirstisoconjugate, windowLen=self.eicSmoothingWindowSize, window=self.eicSmoothingWindow, polynom=self.eicSmoothingPolynom)
-
- peaksBoth = []
-
- # match detected chromatographic peaks from both EICs
- for peakN in peaksN:
- closestMatch = -1
- closestOffset = 10000000000
-
- # search closest peak pair
- for li, peakL in enumerate(peaksL):
- if abs(peakN.apex_index - peakL.apex_index) <= self.peakCenterError:
- if closestMatch == -1 or closestOffset > abs(peakN.apex_index - peakL.apex_index):
- closestMatch = li
- closestOffset = abs(peakN.apex_index - peakL.apex_index)
-
- if closestMatch != -1:
- peakL = peaksL[closestMatch]
-
- # print("M: ", int(peakN.peakIndex - peakN.peakLeftFlank), peakN.peakIndex, int(peakN.peakIndex + peakN.peakRightFlank))
- # print(f" FWHM: {fwhm_M}, area: {area_M}, SNR: {snr_M}")
- # print("Mp: ", int(peakL.peakIndex - peakL.peakLeftFlank), peakL.peakIndex, int(peakL.peakIndex + peakL.peakRightFlank))
- # print(f" FWHM: {fwhm_Mp}, area: {area_Mp}, SNR: {snr_Mp}")
-
- peak = ChromPeakPair(
- mz=meanmz,
- lmz=meanmzLabelled,
- tmz=meantmz,
- xCount=xcount,
- loading=loading,
- ionMode=ionMode,
- NPeakCenter=peakN.apex_index,
- NPeakCenterMin=peakN.apex_rt,
- NPeakScale=(peakN.apex_index - peakN.start_index + peakN.end_index - peakN.apex_index) / 2.0,
- NSNR=peakN.snr,
- NPeakArea=peakN.area,
- LPeakCenter=peakL.apex_index,
- LPeakCenterMin=peakL.apex_rt,
- LPeakScale=(peakL.apex_index - peakL.start_index + peakL.end_index - peakL.apex_index) / 2.0,
- LSNR=peakL.snr,
- LPeakArea=peakL.area,
- NXIC=eic,
- LXIC=eicL,
- times=times,
- fDesc=[],
- adducts=[],
- heteroAtomsFeaturePairs=[],
- NXICSmoothed=eicSmoothed,
- LXICSmoothed=eicLSmoothed,
- NBorderLeft=peakN.apex_index - peakN.start_index,
- NBorderRight=peakN.end_index - peakN.apex_index,
- LBorderLeft=peakL.apex_index - peakL.start_index,
- LBorderRight=peakL.end_index - peakL.apex_index,
- N_startRT=peakN.start_rt,
- N_endRT=peakN.end_rt,
- L_startRT=peakL.start_rt,
- L_endRT=peakL.end_rt,
- isotopeRatios=[],
- mzDiffErrors=Bunch(),
- comments=[],
- artificialEICLShift=0,
- )
-
- lb = int(
- max(
- 0,
- min(
- peak.NPeakCenter - peak.NBorderLeft,
- peak.LPeakCenter - peak.LBorderLeft,
- ),
- )
- )
- rb = int(
- min(
- max(
- peak.NPeakCenter + peak.NBorderRight,
- peak.LPeakCenter + peak.LBorderRight,
- )
- + 1,
- len(eic) - 1,
- )
- )
- peakN = eicSmoothed[lb:rb]
- peakL = eicLSmoothed[lb:rb]
-
- def findBestArtificialShift(eicN, eicL, lb, rb, shiftFrom=0, shiftTo=0):
- correlations = []
-
- for artShift in range(shiftFrom, shiftTo + 1):
- peakN = eicN[lb:rb]
- peakL = eicL[(lb + artShift) : (rb + artShift)]
- silRatios = [
- peakN[i] / peakL[i]
- for i in range(
- int(len(peakN) * 0.25),
- int(len(peakN) * 0.75) + 1,
- )
- if peakL[i] > 0 and peakN[i] > 0
- ]
- correlations.append(
- Bunch(
- correlation=corr(peakN, peakL),
- artificialShift=artShift,
- silRatios=silRatios,
- peakNInts=[
- peakN[i]
- for i in range(
- int(len(peakN) * 0.25),
- int(len(peakN) * 0.75) + 1,
- )
- if peakL[i] > 0 and peakN[i] > 0
- ],
- peakLInts=[
- peakL[i]
- for i in range(
- int(len(peakN) * 0.25),
- int(len(peakN) * 0.75) + 1,
- )
- if peakL[i] > 0 and peakN[i] > 0
- ],
- )
- )
- if not correlations:
- return None
- bestFit = max(correlations, key=lambda x: x.correlation)
-
- return bestFit
-
- co = findBestArtificialShift(
- eicSmoothed,
- eicLSmoothed,
- lb,
- rb,
- self.artificialMPshift_start,
- self.artificialMPshift_stop,
- )
-
- # check peak shape (Pearson correlation)
-
- if co is not None and co.correlation >= self.minPeakCorr and ((not self.checkPeaksRatio) or self.minPeaksRatio <= (peak.NPeakArea / peak.LPeakArea) <= self.maxPeaksRatio):
- peak.peaksCorr = co.correlation
- peak.silRatios = Bunch(
- silRatios=co.silRatios,
- peakNInts=co.peakNInts,
- peakLInts=co.peakLInts,
- )
- if co.artificialShift != 0:
- peak.artificialEICLShift = co.artificialShift
-
- peaksBoth.append(peak)
-
- libs = int(
- max(
- peak.NPeakCenter - floor(peak.NBorderLeft * 0.33),
- peak.LPeakCenter - floor(peak.LBorderLeft * 0.33),
- )
- )
- ribs = (
- int(
- min(
- peak.NPeakCenter + floor(peak.NBorderRight * 0.33),
- peak.LPeakCenter + floor(peak.LBorderRight * 0.33),
- )
- )
- + 1
- )
-
- peak.peaksRatio = self.getMeanRatioOfScans(
- eic,
- eicL,
- libs,
- ribs,
- minInt=self.intensityThreshold,
- )
- peak.peaksRatioMp1 = self.getMeanRatioOfScans(
- eicfirstiso,
- eic,
- libs,
- ribs,
- minInt=self.intensityThreshold,
- )
- peak.peaksRatioMPm1 = self.getMeanRatioOfScans(
- eicLfirstiso,
- eicL,
- libs,
- ribs,
- minInt=self.intensityThreshold,
- )
-
- # check, if enough MS scans fulfill the requirements (isotope patterns)
- for kid in kids:
- kido = kid.getObject()
-
- closestPeak = -1
- distance = len(eic)
- i = 0
- for peak in peaksBoth:
- if abs(kido.scanIndex - peak.NPeakCenter) < distance:
- closestPeak = i
- distance = abs(kido.scanIndex - peak.NPeakCenter)
- i = i + 1
- if closestPeak != -1 and distance < (
- min(
- peaksBoth[closestPeak].NBorderLeft,
- peaksBoth[closestPeak].NBorderRight,
- )
- * 2
- ):
- peaksBoth[closestPeak].assignedMZs.append(kido)
-
- curChromPeaks = []
- for peak in peaksBoth:
- if len(peak.assignedMZs) >= self.minSpectraCount:
- curChromPeaks.append(peak)
-
- for peak in curChromPeaks:
- lb = int(
- max(
- 0,
- min(
- peak.NPeakCenter - peakAbundanceUseSignalsSides,
- peak.LPeakCenter - peakAbundanceUseSignalsSides,
- ),
- )
- )
- rb = int(
- min(
- max(
- peak.NPeakCenter + peakAbundanceUseSignalsSides,
- peak.LPeakCenter + peakAbundanceUseSignalsSides,
- )
- + 1,
- len(eic) - 1,
- )
- )
- peakN = eic[lb:rb]
- peakL = eicL[lb:rb]
-
- peak.NPeakAbundance = max(peakN) # mean(peakN)
- peak.LPeakAbundance = max(peakL) # mean(peakL)
-
- # write detected feature pair to the database
- if len(curChromPeaks) > 0:
- assert len(eic) == len(eicL) == len(eicfirstiso) == len(eicLfirstiso) == len(eicLfirstisoconjugate) == len(times)
-
- keepScans = set()
- for cs in range(len(eic)):
- if eic[cs] > 0 or eicL[cs] > 0 or eicfirstiso[cs] > 0 or eicLfirstiso[cs] > 0 or eicLfirstisoconjugate[cs] > 0:
- keepScans.add(cs)
- keepScans.add(0)
- keepScans.add(len(eic) - 1)
-
- # save the native and labelled EICs to the database
- db_con.insert_row(
- "XICs",
- {
- "id": self.curEICId,
- "avgmz": meanmz,
- "xcount": xcount,
- "loading": kids[0].getObject().loading,
- "polarity": ionMode,
- "xic": ";".join(["%f" % i for i in eic]),
- "xicL": ";".join(["%f" % i for i in eicL]),
- "xicfirstiso": ";".join(["%f" % i for i in eicfirstiso]),
- "xicLfirstiso": ";".join(["%f" % i for i in eicLfirstiso]),
- "xicLfirstisoconjugate": ";".join(["%f" % i for i in eicLfirstisoconjugate]),
- "xic_smoothed": ";".join(["%f" % i for i in eicSmoothed]),
- "xicL_smoothed": ";".join(["%f" % i for i in eicLSmoothed]),
- "xic_baseline": ";".join(["%f" % i for i in eicBaseline]),
- "xicL_baseline": ";".join(["%f" % i for i in eicLBaseline]),
- "mzs": ";".join(["%f" % (mzs[i]) for i in range(0, len(eic))]),
- "mzsL": ";".join(["%f" % (mzsL[i]) for i in range(0, len(eic))]),
- "mzsfirstiso": ";".join(["%f" % (mzsfirstiso[i]) for i in range(0, len(eic))]),
- "mzsLfirstiso": ";".join(["%f" % (mzsLfirstiso[i]) for i in range(0, len(eic))]),
- "mzsLfirstisoconjugate": ";".join(["%f" % (mzsLfirstisoconjugate[i]) for i in range(0, len(eic))]),
- "times": ";".join(["%f" % (times[i]) for i in range(0, len(times))]),
- "scanCount": len(eic),
- "allPeaks": base64.b64encode(dumps({"peaksN": peaksN, "peaksL": peaksL})).decode("utf-8"),
- },
- )
-
- # save the detected feature pairs in these EICs to the database
- for peak in curChromPeaks:
- peak.id = self.curPeakId
- peak.eicID = self.curEICId
- adjcCount = peak.xCount
- peak.correctedXCount = peak.xCount
-
- if self.performCorrectCCount and False:
- adjcCount = adjcCount + getAtomAdd(self.purityL, peak.xCount) + getAtomAdd(self.purityN, peak.xCount)
- peak.correctedXCount = adjcCount
-
- db_con.insert_row(
- "chromPeaks",
- {
- "id": peak.id,
- "tracer": tracerID,
- "eicID": peak.eicID,
- "mz": peak.mz,
- "lmz": peak.lmz,
- "tmz": peak.tmz,
- "xcount": peak.correctedXCount,
- "xcountId": peak.xCount,
- "Loading": peak.loading,
- "ionMode": ionMode,
- "NPeakCenter": peak.NPeakCenter,
- "NPeakCenterMin": peak.NPeakCenterMin,
- "NPeakScale": peak.NPeakScale,
- "NSNR": peak.NSNR,
- "NPeakArea": peak.NPeakArea,
- "NPeakAbundance": peak.NPeakAbundance,
- "NBorderLeft": peak.NBorderLeft,
- "NBorderRight": peak.NBorderRight,
- "LPeakCenter": peak.LPeakCenter,
- "LPeakCenterMin": peak.LPeakCenterMin,
- "LPeakScale": peak.LPeakScale,
- "LSNR": peak.LSNR,
- "LPeakArea": peak.LPeakArea,
- "LPeakAbundance": peak.LPeakAbundance,
- "LBorderLeft": peak.LBorderLeft,
- "LBorderRight": peak.LBorderRight,
- "N_startRT": peak.N_startRT,
- "N_endRT": peak.N_endRT,
- "L_startRT": peak.L_startRT,
- "L_endRT": peak.L_endRT,
- "peaksCorr": peak.peaksCorr,
- "heteroAtoms": "",
- "adducts": "",
- "heteroAtomsFeaturePairs": "",
- "massSpectrumID": 0,
- "assignedMZs": base64.b64encode(dumps(peak.assignedMZs)).decode("utf-8"),
- "fDesc": base64.b64encode(dumps([])).decode("utf-8"),
- "peaksRatio": peak.peaksRatio,
- "peaksRatioMp1": peak.peaksRatioMp1,
- "peaksRatioMPm1": peak.peaksRatioMPm1,
- "peakType": "patternFound",
- "comments": base64.b64encode(dumps(peak.comments)).decode("utf-8"),
- "artificialEICLShift": peak.artificialEICLShift,
- },
- )
-
- db_con.insert_row(
- "allChromPeaks",
- {
- "id": peak.id,
- "tracer": tracerID,
- "eicID": peak.eicID,
- "mz": peak.mz,
- "lmz": peak.lmz,
- "tmz": peak.tmz,
- "xcount": peak.correctedXCount,
- "xcountId": peak.xCount,
- "Loading": peak.loading,
- "ionMode": ionMode,
- "NPeakCenter": peak.NPeakCenter,
- "NPeakCenterMin": peak.NPeakCenterMin,
- "NPeakScale": peak.NPeakScale,
- "NSNR": peak.NSNR,
- "NPeakArea": peak.NPeakArea,
- "NPeakAbundance": peak.NPeakAbundance,
- "NBorderLeft": peak.NBorderLeft,
- "NBorderRight": peak.NBorderRight,
- "LPeakCenter": peak.LPeakCenter,
- "LPeakCenterMin": peak.LPeakCenterMin,
- "LPeakScale": peak.LPeakScale,
- "LSNR": peak.LSNR,
- "LPeakArea": peak.LPeakArea,
- "LPeakAbundance": peak.LPeakAbundance,
- "LBorderLeft": peak.LBorderLeft,
- "LBorderRight": peak.LBorderRight,
- "N_startRT": peak.N_startRT,
- "N_endRT": peak.N_endRT,
- "L_startRT": peak.L_startRT,
- "L_endRT": peak.L_endRT,
- "peaksCorr": peak.peaksCorr,
- "heteroAtoms": "",
- "adducts": "",
- "heteroAtomsFeaturePairs": "",
- "assignedMZs": len(peak.assignedMZs),
- "fDesc": base64.b64encode(dumps([])).decode("utf-8"),
- "peaksRatio": peak.peaksRatio,
- "peaksRatioMp1": peak.peaksRatioMp1,
- "peaksRatioMPm1": peak.peaksRatioMPm1,
- "peakType": "patternFound",
- "comments": base64.b64encode(dumps(peak.comments)).decode("utf-8"),
- "artificialEICLShift": peak.artificialEICLShift,
- },
- )
-
- chromPeaks.append(peak)
- self.curPeakId = self.curPeakId + 1
- except Exception as e:
- print("Error processing mzbin with meanmz %f: %s" % (meanmz, str(e)))
- traceback.print_exc()
-
- self.curEICId = self.curEICId + 1
- db_con.commit()
- db_con.close()
- return chromPeaks
-
- # data processing step 4: remove those feature pairs, which represent incorrect pairings of
- # isotoplogs of either the native or the labelled or both ions. Such incorrect pairings always have
- # and increased mz value and/or a decreased number of labelled carbon atoms. Such identified
- # incorrect pairings are then removed from the database and thus the processing results
- def removeFalsePositiveFeaturePairsAndUpdateDB(self, chromPeaks, reportFunction=None):
- db_con = PolarsDB(self.file + getDBSuffix(), format=getDBFormat())
-
- todel = {}
-
- # iterate over all detected feature pairs and compare those
- # if the a) originate from the same ionisation mode
- # and b) if they are not the same feature pair
- for a in range(0, len(chromPeaks)):
- if reportFunction is not None:
- reportFunction(
- 1.0 * a / len(chromPeaks),
- "%d feature pairs remaining" % (len(chromPeaks) - a),
- )
-
- peakA = chromPeaks[a]
- for b in range(0, len(chromPeaks)):
- peakB = chromPeaks[b]
- if a != b and peakA.ionMode == peakB.ionMode and (peakA.mz < peakB.mz or abs(peakA.mz - peakB.mz) <= (peakA.mz * 2.5 * self.ppm / 1000000.0)):
- # not same feature pair but same ionisation mode
-
- if abs(peakA.NPeakCenter - peakB.NPeakCenter) <= self.peakCenterError:
- # same retention time
- if abs(peakA.mz - peakB.mz) <= (peakA.mz * 2.5 * self.ppm / 1000000.0):
- # same mz value
- if isinstance(peakA.xCount, float) and (peakB.xCount - peakA.xCount) == 2 and peakB.loading == peakA.loading and ((peakB.LPeakArea / peakA.LPeakArea) <= 0.1):
- # incorrect matching of 18O atoms detected
- # e.g. a and b have 869.4153; a has C39 and b has C41; peaksRatio(a)=1.46, peaksRatio(b)=43.48
- # --> 1.46/43.48~0.0335 (which is in good agreement with On) and thus b has to be removed
- if b not in todel.keys():
- todel[b] = []
- todel[b].append("incorrectly matched hetero atoms (most likely O) with " + str(peakA.mz) + " " + str(peakA.xCount))
- elif isinstance(peakA.xCount, float) and (peakB.xCount - peakA.xCount) in [1, 2, 3] and peakB.loading == peakA.loading:
- # different number of 1 atoms (peakA has less than peakB)
- if a not in todel.keys():
- todel[a] = []
- todel[a].append("same mz but reduced number of carbon atoms with " + str(peakB.mz) + " " + str(peakB.xCount))
-
- if isinstance(peakA.xCount, float) and (peakA.xCount * 2) == peakB.xCount and peakA.loading == peakB.loading:
- # a is intermediate pairing of polymer
- if a not in todel.keys():
- todel[a] = []
- todel[a].append("polymer mismatch with half the number of carbon atoms with " + str(peakB.mz) + " " + str(peakB.xCount))
-
- if isinstance(peakA.xCount, float) and (peakA.xCount * 2) == peakB.xCount and peakA.loading == 1 and peakB.loading == 2:
- # a is intermediate pairing of polymer
- if a not in todel.keys():
- todel[a] = []
- todel[a].append("singly charged mismatch with half the number of carbon atoms with " + str(peakB.mz) + " " + str(peakB.xCount))
-
- if isinstance(peakA.xCount, float) and 0 <= (peakB.xCount - peakA.xCount * 3) <= 2 and peakA.loading == 1 and peakB.loading == 3:
- # a is intermediate pairing of polymer
- if a not in todel.keys():
- todel[a] = []
- todel[a].append("singly charged mismatch with third the number of carbon atoms with " + str(peakB.mz) + " " + str(peakB.xCount))
-
- elif abs(peakB.mz - peakA.mz - 1.00335 / peakA.loading) <= (peakA.mz * 2.5 * self.ppm / 1000000.0) and peakB.xCount == peakA.xCount and peakB.NPeakArea < 2 * peakA.NPeakArea:
- ## increased m/z value by one carbon atom, but the same number of labeling atoms ## happens quite often with 15N-labeled metabolites e.g. 415.21374 and 415.7154 or 414.6978 and 415.19966
- if b not in todel.keys():
- todel[b] = []
- todel[b].append("increased m/z by one labeling atoms while the number of labeled atoms is identical." + str(peakA.mz))
-
- else:
- for i in [1, 2, 3]:
- if peakA.loading == peakB.loading and abs(peakB.mz - peakA.mz - i * self.xOffset / peakA.loading) <= peakA.mz * 2.5 * self.ppm / 1000000.0 and isinstance(peakA.xCount, float) and (peakA.xCount - peakB.xCount) in [1, 2, 3]:
- # b has an mz offset and a reduced number of carbon atoms (by one labelling atom)
- if b not in todel.keys():
- todel[b] = []
- todel[b].append("increased mz (by %d) and reduced number of labeling atoms (by %d) with " % (i, peakA.xCount - peakB.xCount) + str(peakA.mz) + " " + str(peakA.xCount))
- elif peakA.loading == peakB.loading and abs(peakB.mz - peakA.mz - i * 1.00335 / peakA.loading) <= peakA.mz * 2.5 * self.ppm / 1000000.0 and peakA.xCount == peakB.xCount and peakB.NPeakArea <= peakA.NPeakArea * 0.1:
- # b has an mz offset and a reduced number of carbon atoms (by one labelling atom)
- if b not in todel.keys():
- todel[b] = []
- todel[b].append("increased mz (by %d 13C) and same number of labeling atoms with " % (i) + str(peakA.mz) + " " + str(peakA.xCount))
-
- for a in range(0, len(chromPeaks)):
- for b in range(0, len(chromPeaks)):
- peakA = chromPeaks[a]
- peakB = chromPeaks[b]
- if a != b and peakA.ionMode == peakB.ionMode:
- if abs(peakA.NPeakCenter - peakB.NPeakCenter) <= self.peakCenterError:
- # same chrom peak
- if abs(peakA.mz - peakB.mz - (peakA.lmz - peakA.mz)) <= peakA.mz * 2 * self.ppm / 1000000.0 and ((isinstance(peakA.xCount, float) and abs(peakA.xCount * 2 - peakB.xCount) <= 1) or (peakA.xCount == peakB.xCount)):
- if a not in todel.keys():
- todel[a] = []
- todel[a].append("dimer (1)" + str(peakB.mz) + " " + str(peakB.xCount))
- delet = []
- for x in todel.keys():
- delet.append(x)
- delet.sort()
- delet.reverse()
- for dele in delet:
- cp = chromPeaks.pop(dele)
- # Delete from chromPeaks
- db_con.tables["chromPeaks"] = db_con.tables["chromPeaks"].filter(pl.col("id") != cp.id)
- # Update allChromPeaks comment
- db_con.tables["allChromPeaks"] = db_con.tables["allChromPeaks"].with_columns(pl.when(pl.col("id") == cp.id).then(pl.lit(",".join(todel[dele]))).otherwise(pl.col("comment")).alias("comment"))
-
- # Delete XICs not in chromPeaks
- valid_eic_ids = db_con.tables["chromPeaks"]["eicID"].unique().to_list()
- db_con.tables["XICs"] = db_con.tables["XICs"].filter(pl.col("id").is_in(valid_eic_ids))
-
- db_con.commit()
- db_con.close()
-
- # Isotopolog mass differences (monoisotopic, in Da) for natural isotopes
- _ISOTOPOLOG_MASS_DIFFS = {
- "13C": 1.003355,
- "15N": 0.997035,
- "34S": 1.995796,
- "54Fe": -1.995328,
- "37Cl": 1.997050,
- "D": 1.006277,
- "18O": 2.004244,
- }
-
- _M_ISOTOPOLOG_OFFSETS = [
- ("M-13C2", -2 * 1.003355),
- ("M-13C", -1 * 1.003355),
- ("M+13C", +1 * 1.003355),
- ("M+13C2", +2 * 1.003355),
- ("M+15N", +0.997035),
- ("M+34S", +1.995796),
- ("M-54Fe", -1.995328),
- ("M+37Cl", +1.997050),
- ]
-
- _MP_ISOTOPOLOG_OFFSETS = [
- ("Mp-13C2", -2 * 1.003355),
- ("Mp-13C", -1 * 1.003355),
- ("Mp+13C", +1 * 1.003355),
- ("Mp+13C2", +2 * 1.003355),
- ("Mp+15N", +0.997035),
- ("Mp+34S", +1.995796),
- ("Mp-54Fe", -1.995328),
- ("Mp+37Cl", +1.997050),
- ("Mp+D", +1.006277),
- ("Mp+18O", +2.004244),
- ]
-
- def calculateIsotopologRatiosForFeaturePairs(self, chromPeaks, mzxml, reportFunction=None):
- """For each detected feature pair, extract EICs for isotopologs of M and M',
- calculate peak area ratios within the respective peak boundaries, and store
- the results as a dictionary in peak.isotopologRatios.
-
- All M-based ratios are relative to the M peak area; all M'-based ratios are
- relative to the M' peak area.
- """
- db_con = PolarsDB(self.file + getDBSuffix(), format=getDBFormat())
-
- def _peak_area(eic, times, lb, rb):
- rb = min(rb, len(eic) - 1)
- lb = max(lb, 0)
- if lb >= rb:
- return 0.0
- return float(np.trapz(eic[lb : rb + 1], times[lb : rb + 1]))
-
- for i, peak in enumerate(chromPeaks):
- if reportFunction is not None:
- reportFunction(1.0 * i / len(chromPeaks), "%d features remaining" % (len(chromPeaks) - i))
-
- scanEvent = self.positiveScanEvent if peak.ionMode == "+" else self.negativeScanEvent
- loading = peak.loading
-
- lb_N = max(0, peak.NPeakCenter - int(peak.NBorderLeft))
- rb_N = peak.NPeakCenter + int(peak.NBorderRight)
- lb_L = max(0, peak.LPeakCenter - int(peak.LBorderLeft))
- rb_L = peak.LPeakCenter + int(peak.LBorderRight)
-
- eic_M, times_M, _, _ = mzxml.getEIC(peak.mz, self.chromPeakPPM, filterLine=scanEvent)
- eic_Mp, times_Mp, _, _ = mzxml.getEIC(peak.lmz, self.chromPeakPPM, filterLine=scanEvent)
- eic_M = np.asarray(eic_M, dtype=np.float64)
- eic_Mp = np.asarray(eic_Mp, dtype=np.float64)
- times_M = np.asarray(times_M, dtype=np.float64)
- times_Mp = np.asarray(times_Mp, dtype=np.float64)
-
- area_M = _peak_area(eic_M, times_M, lb_N, rb_N)
- area_Mp = _peak_area(eic_Mp, times_Mp, lb_L, rb_L)
-
- isotopolog_ratios = {}
-
- for label, offset in self._M_ISOTOPOLOG_OFFSETS:
- target_mz = peak.mz + offset / loading
- eic_iso, times_iso, _, _ = mzxml.getEIC(target_mz, self.chromPeakPPM, filterLine=scanEvent)
- eic_iso = np.asarray(eic_iso, dtype=np.float64)
- times_iso = np.asarray(times_iso, dtype=np.float64)
- area_iso = _peak_area(eic_iso, times_iso, lb_N, rb_N)
- isotopolog_ratios[label] = area_iso / area_M if area_M > 0 else 0.0
-
- for label, offset in self._MP_ISOTOPOLOG_OFFSETS:
- target_mz = peak.lmz + offset / loading
- eic_iso, times_iso, _, _ = mzxml.getEIC(target_mz, self.chromPeakPPM, filterLine=scanEvent)
- eic_iso = np.asarray(eic_iso, dtype=np.float64)
- times_iso = np.asarray(times_iso, dtype=np.float64)
- area_iso = _peak_area(eic_iso, times_iso, lb_L, rb_L)
- isotopolog_ratios[label] = area_iso / area_Mp if area_Mp > 0 else 0.0
-
- peak.isotopologRatios = isotopolog_ratios
-
- for peak in chromPeaks:
- encoded = base64.b64encode(dumps(peak.isotopologRatios)).decode("utf-8")
- db_con.tables["chromPeaks"] = db_con.tables["chromPeaks"].with_columns(pl.when(pl.col("id") == peak.id).then(pl.lit(encoded)).otherwise(pl.col("isotopologRatios")).alias("isotopologRatios"))
- db_con.tables["allChromPeaks"] = db_con.tables["allChromPeaks"].with_columns(pl.when(pl.col("id") == peak.id).then(pl.lit(encoded)).otherwise(pl.col("isotopologRatios")).alias("isotopologRatios"))
-
- self.printMessage("Isotopolog ratio calculation done.", type="info")
- db_con.commit()
- db_con.close()
-
- # data processing step 5: in full metabolome labeling experiments hetero atoms (e.g. S, Cl) may
- # show characterisitc isotope patterns on the labeled metabolite ion side. There, these peaks may not be
- # dominated by the usually much more abundant carbon isotopes and can thus be easier seen. However, for
- # low abundant metabolite ions these isotope peaks may not be present at all. The search is performed
- # on a MS scan level and does not directly use the chromatographic information (no chromatographic
- # peak picking is performed)
- def annotateFeaturePairs(self, chromPeaks, mzxml, tracer, reportFunction=None):
- db_con = PolarsDB(self.file + getDBSuffix(), format=getDBFormat())
-
- self.postMessageToProgressWrapper("text", "%s: Annotating feature pairs" % tracer.name)
- for i in range(0, len(chromPeaks)):
- if reportFunction is not None:
- reportFunction(
- 1.0 * i / len(chromPeaks),
- "%d features remaining" % (len(chromPeaks) - i),
- )
-
- peak = chromPeaks[i]
-
- ## Annotate hetero atoms
- for pIso in self.heteroAtoms:
- pIsotope = self.heteroAtoms[pIso]
-
- mz = 0
- if pIsotope.mzOffset < 0:
- mz = peak.mz + pIsotope.mzOffset / peak.loading # delta m/z is negative, therefore this decreases the search m/z
- else:
- mz = peak.lmz + pIsotope.mzOffset / peak.loading # delta m/z is positive
-
- refMz = 0
- if pIsotope.mzOffset < 0:
- refMz = peak.mz
- else:
- refMz = peak.lmz
-
- scanEvent = ""
- if peak.ionMode == "+":
- scanEvent = self.positiveScanEvent
- elif peak.ionMode == "-":
- scanEvent = self.negativeScanEvent
-
- for haCount in range(pIsotope.minCount, pIsotope.maxCount + 1):
- if haCount == 0:
- continue
-
- for curScanNum in range(
- int(
- max(
- peak.NPeakCenter - peak.NBorderLeft,
- peak.LPeakCenter - peak.LBorderLeft,
- )
- ),
- int(
- min(
- peak.NPeakCenter + peak.NBorderRight,
- peak.LPeakCenter + peak.LBorderRight,
- )
- )
- + 1,
- ):
- scan = mzxml.getIthMS1Scan(curScanNum, scanEvent)
- if scan is not None:
- mzBounds = scan.findMZ(refMz, self.ppm)
- mzBounds = scan.getMostIntensePeak(mzBounds[0], mzBounds[1])
- if mzBounds != -1:
- peakIntensity = scan.intensity_list[mzBounds]
-
- isoBounds = scan.findMZ(mz, self.ppm)
- isoBounds = scan.getMostIntensePeak(isoBounds[0], isoBounds[1])
- if isoBounds != -1:
- isoIntensity = scan.intensity_list[isoBounds]
-
- relativeIntensity = isoIntensity / peakIntensity
- if abs(relativeIntensity - pIsotope.relativeAbundance * haCount) < self.hAIntensityError:
- if pIso not in peak.heteroIsotopologues:
- peak.heteroIsotopologues[pIso] = {}
- if haCount not in peak.heteroIsotopologues[pIso]:
- peak.heteroIsotopologues[pIso][haCount] = []
- peak.heteroIsotopologues[pIso][haCount].append(
- (
- scan.id,
- relativeIntensity,
- pIsotope.relativeAbundance * haCount,
- )
- )
-
- rmHIso = []
- for hI in peak.heteroIsotopologues:
- for haCount in peak.heteroIsotopologues[hI]:
- if len(peak.heteroIsotopologues[hI][haCount]) < self.hAMinScans:
- rmHIso.append((hI, haCount))
-
- for rmHI, rmHACount in rmHIso:
- del peak.heteroIsotopologues[rmHI][rmHACount]
- rmHIso = []
- for hi in peak.heteroIsotopologues:
- if len(peak.heteroIsotopologues[hi]) == 0:
- rmHIso.append(hi)
- for rmHI in rmHIso:
- del peak.heteroIsotopologues[rmHI]
-
- self.getMostLikelyHeteroIsotope(peak.heteroIsotopologues)
-
- ## Annotate isotopolog ratios
-
- def findRatiosForMZs(mzFrom, mzTo, fromScan, toScan, mzxml, scanEvent, ppm):
- isoRatios = []
- for curScanNum in range(fromScan, toScan):
- scan = mzxml.getIthMS1Scan(curScanNum, scanEvent)
- if scan is not None:
- mzBounds = scan.findMZ(mzTo, ppm)
- mzBounds = scan.getMostIntensePeak(mzBounds[0], mzBounds[1])
- if mzBounds != -1:
- peakIntensity = scan.intensity_list[mzBounds]
-
- isoBounds = scan.findMZ(mzFrom, ppm)
- isoBounds = scan.getMostIntensePeak(isoBounds[0], isoBounds[1])
-
- if isoBounds != -1:
- isoPeakIntensity = scan.intensity_list[isoBounds]
-
- isoRatios.append(
- Bunch(
- ratio=isoPeakIntensity / peakIntensity,
- refInt=peakIntensity,
- )
- )
-
- return isoRatios
-
- peak.isotopeRatios = []
- for i in range(1, self.calcIsoRatioNative + 1):
- isoRatios = findRatiosForMZs(
- peak.mz + 1.00335484 * i / peak.loading,
- peak.mz,
- int(
- max(
- peak.NPeakCenter - peak.NBorderLeft,
- peak.LPeakCenter - peak.LBorderLeft,
- )
- ),
- int(
- min(
- peak.NPeakCenter + peak.NBorderRight,
- peak.LPeakCenter + peak.LBorderRight,
- )
- )
- + 1,
- mzxml,
- scanEvent,
- self.ppm,
- )
- observedRatioMean = weightedMean([t.ratio for t in isoRatios], [t.refInt for t in isoRatios])
- observedRatioSD = weightedSd([t.ratio for t in isoRatios], [t.refInt for t in isoRatios])
- if isinstance(peak.xCount, str):
- theoreticalRatio = -1
- else:
- theoreticalRatio = getNormRatio(self.purityN, peak.xCount, i)
- peak.isotopeRatios.append(
- Bunch(
- type="native",
- subs=i,
- observedRatioMean=observedRatioMean,
- observedRatioSD=observedRatioSD,
- theoreticalRatio=theoreticalRatio,
- )
- )
-
- for i in range(-1, self.calcIsoRatioLabelled - 1, -1):
- isoRatios = findRatiosForMZs(
- peak.lmz + 1.00335484 * i / peak.loading,
- peak.lmz,
- int(
- max(
- peak.NPeakCenter - peak.NBorderLeft,
- peak.LPeakCenter - peak.LBorderLeft,
- )
- ),
- int(
- min(
- peak.NPeakCenter + peak.NBorderRight,
- peak.LPeakCenter + peak.LBorderRight,
- )
- )
- + 1,
- mzxml,
- scanEvent,
- self.ppm,
- )
- observedRatioMean = weightedMean([t.ratio for t in isoRatios], [t.refInt for t in isoRatios])
- observedRatioSD = weightedSd([t.ratio for t in isoRatios], [t.refInt for t in isoRatios])
- if isinstance(peak.xCount, str):
- theoreticalRatio = -1
- else:
- theoreticalRatio = getNormRatio(self.purityL, peak.xCount, abs(i))
- peak.isotopeRatios.append(
- Bunch(
- type="labelled",
- subs=abs(i),
- observedRatioMean=observedRatioMean,
- observedRatioSD=observedRatioSD,
- theoreticalRatio=theoreticalRatio,
- )
- )
- if self.metabolisationExperiment:
- for i in range(1, self.calcIsoRatioMoiety + 1):
- isoRatios = findRatiosForMZs(
- peak.lmz + i * 1.00335484 / peak.loading,
- peak.lmz,
- int(
- max(
- peak.NPeakCenter - peak.NBorderLeft,
- peak.LPeakCenter - peak.LBorderLeft,
- )
- ),
- int(
- min(
- peak.NPeakCenter + peak.NBorderRight,
- peak.LPeakCenter + peak.LBorderRight,
- )
- )
- + 1,
- mzxml,
- scanEvent,
- self.ppm,
- )
- observedRatioMean = weightedMean([t.ratio for t in isoRatios], [t.refInt for t in isoRatios])
- observedRatioSD = weightedSd([t.ratio for t in isoRatios], [t.refInt for t in isoRatios])
- peak.isotopeRatios.append(
- Bunch(
- type="moiety",
- subs=i,
- observedRatioMean=observedRatioMean,
- observedRatioSD=observedRatioSD,
- theoreticalRatio=0,
- )
- )
-
- def findMZDifferenceRelativeToXnForMZs(mzFrom, mzTo, tmz, fromScan, toScan, mzxml, scanEvent, ppm):
- diffs = []
- for curScanNum in range(fromScan, toScan):
- scan = mzxml.getIthMS1Scan(curScanNum, scanEvent)
- if scan is not None:
- mzBounds = scan.findMZ(mzTo, ppm)
- mzBounds = scan.getMostIntensePeak(mzBounds[0], mzBounds[1])
- if mzBounds != -1:
- peakMZ = scan.mz_list[mzBounds]
-
- refBounds = scan.findMZ(mzFrom, ppm)
- refBounds = scan.getMostIntensePeak(refBounds[0], refBounds[1])
-
- if refBounds != -1:
- isoPeakMZ = scan.mz_list[refBounds]
-
- diffs.append((abs(isoPeakMZ - peakMZ) - tmz) * 1000000.0 / mzFrom)
-
- return diffs
-
- diffs = findMZDifferenceRelativeToXnForMZs(
- peak.mz,
- peak.lmz,
- peak.lmz - peak.mz,
- int(
- max(
- peak.NPeakCenter - peak.NBorderLeft,
- peak.LPeakCenter - peak.LBorderLeft,
- )
- ),
- int(
- min(
- peak.NPeakCenter + peak.NBorderRight,
- peak.LPeakCenter + peak.LBorderRight,
- )
- )
- + 1,
- mzxml,
- scanEvent,
- self.ppm,
- )
- peak.mzDiffErrors = Bunch(mean=mean(diffs), sd=sd(diffs), vals=diffs)
-
- # Batch update using Polars for better performance
- updates_dict = {"heteroAtoms": {}, "isotopesRatios": {}, "mzDiffErrors": {}}
-
- for i in range(len(chromPeaks)):
- peak = chromPeaks[i]
- updates_dict["heteroAtoms"][peak.id] = base64.b64encode(dumps(peak.heteroIsotopologues)).decode("utf-8")
- updates_dict["isotopesRatios"][peak.id] = base64.b64encode(dumps(peak.isotopeRatios)).decode("utf-8")
- updates_dict["mzDiffErrors"][peak.id] = base64.b64encode(dumps(peak.mzDiffErrors)).decode("utf-8")
-
- # Update chromPeaks
- for col_name, id_val_map in updates_dict.items():
- for peak_id, value in id_val_map.items():
- db_con.tables["chromPeaks"] = db_con.tables["chromPeaks"].with_columns(pl.when(pl.col("id") == peak_id).then(pl.lit(value)).otherwise(pl.col(col_name)).alias(col_name))
- db_con.tables["allChromPeaks"] = db_con.tables["allChromPeaks"].with_columns(pl.when(pl.col("id") == peak_id).then(pl.lit(value)).otherwise(pl.col(col_name)).alias(col_name))
-
- self.printMessage("%s: Annotating feature pairs done." % tracer.name, type="info")
-
- db_con.commit()
- db_con.close()
-
- # annotate a metabolite group (consisting of ChromPeakPair instances) with the defined
- # hetero atoms based on detected feature pairs
- def annotateFeaturePairsWithHeteroAtoms(self, group, peaksInGroup):
- # iterate all peaks pairwise to find different adducts of the metabolite
- for pa in group:
- peakA = peaksInGroup[pa]
-
- for pb in group:
- peakB = peaksInGroup[pb]
-
- # peakA shall always have the lower mass
- if peakA.mz < peakB.mz and peakA.xCount == peakB.xCount:
- ## check, if it could be a hetero atom
- for pIso in self.heteroAtoms:
- pIsotope = self.heteroAtoms[pIso]
-
- bestFit = None
- bestFitRatio = 100000
- bestFitID = None
-
- if abs(peakB.mz - peakA.mz - pIsotope.mzOffset) <= (max(peakA.mz, peakB.mz) * 2.5 * self.ppm / 1000000.0):
- if pIsotope.mzOffset > 0:
- ratio = peakB.LPeakArea / peakA.LPeakArea
- else:
- ratio = peakB.NPeakArea / peakA.NPeakArea
-
- for nHAtoms in range(pIsotope.minCount, pIsotope.maxCount + 1):
- if abs(ratio - nHAtoms * pIsotope.relativeAbundance) <= self.hAIntensityError:
- if abs(ratio - nHAtoms * pIsotope.relativeAbundance) < bestFitRatio:
- bestFitRatio = abs(ratio - nHAtoms * pIsotope.relativeAbundance)
- bestFit = Bunch(pIso=pIso, nHAtoms=nHAtoms)
- bestFitID = pb
-
- if bestFit is not None:
- peakB = peaksInGroup[bestFitID]
- # peakA.heteroAtomsFeaturePairs.append(Bunch(name="M_%s%d"%(pIso, bestFit), partnerAdd="_%s%d"%(pIso, bestFit), toPeak=pb))
- peakB.heteroAtomsFeaturePairs.append(
- Bunch(
- name="_%s%d" % (bestFit.pIso, bestFit.nHAtoms),
- partnerAdd="M_%s%d" % (bestFit.pIso, bestFit.nHAtoms),
- toPeak=pa,
- )
- )
-
- # annotate a metabolite group (consisting of ChromPeakPair instances) with the defined
- # adducts and generate possible in-source fragments. Remove inconsistencies
- # in the form of impossible adduct combinations (e.g. [M+H]+ and [M+Na]+ for the same ion)
- def annotateChromPeaks(self, group, peaksInGroup):
- for pe in group:
- peak = peaksInGroup[pe]
-
- if not hasattr(peak, "fDesc"):
- setattr(peak, "fDesc", [])
- if not hasattr(peak, "adducts"):
- setattr(peak, "adducts", [])
- if not hasattr(peak, "Ms"):
- setattr(peak, "Ms", [])
-
- if len(group) <= 40 or True:
- self.annotateFeaturePairsWithHeteroAtoms(group, peaksInGroup)
- for pa in group:
- peakA = peaksInGroup[pa]
- peakA.adducts.extend(peakA.heteroAtomsFeaturePairs)
-
- fT = formulaTools()
-
- # prepare adducts list
- addPol = {}
- addPol["+"] = []
- addPol["-"] = []
- adductsDict = {}
- for adduct in self.adducts:
- adductsDict[adduct.name] = adduct
- if adduct.polarity != "":
- addPol[adduct.polarity].append(adduct)
- adducts = addPol
- adducts["+"] = sorted(adducts["+"], key=lambda x: x.mzoffset)
- adducts["-"] = sorted(adducts["-"], key=lambda x: x.mzoffset)
-
- # 1. iterate all peaks pairwise to find different adducts of the metabolite
- for pa in group:
- peakA = peaksInGroup[pa]
- for pb in group:
- peakB = peaksInGroup[pb]
-
- # peakA shall always have the lower mass
- if peakA.mz < peakB.mz and (len(peakB.heteroAtomsFeaturePairs) == 0 or not all([s.name.startswith("_") for s in peakB.heteroAtomsFeaturePairs])):
- # search for different adduct combinations
- # different adducts must have the same number of labelled atoms
- if peakA.xCount == peakB.xCount:
- # check, if it could be some kind of adduct combination
- for adA in adducts[peakA.ionMode]:
- for adB in adducts[peakB.ionMode]:
- if adA.mCount == 1 and adB.mCount == 1 and adA.mzoffset < adB.mzoffset:
- if (
- peakA.ionMode == "-"
- and peakB.ionMode == "+"
- and peakA.loading == peakB.loading
- and abs(abs(adB.mzoffset - adA.mzoffset) - 2 * 1.007276) <= (max(peakA.mz, peakB.mz) * 2.5 * self.ppm / 1000000.0)
- and abs(peakB.mz - peakA.mz - 2 * 1.007276 / peakA.loading) <= (max(peakA.mz, peakB.mz) * 2.5 * self.ppm / 1000000.0)
- ):
- peakA.adducts.append(
- Bunch(
- name="[M-H]-",
- partnerAdd="[M+H]+",
- toPeak=pb,
- )
- )
- peakB.adducts.append(
- Bunch(
- name="[M+H]+",
- partnerAdd="[M-H]-",
- toPeak=pa,
- )
- )
-
- elif peakA.loading == adA.charge and peakB.loading == adB.charge and abs((peakA.mz - adA.mzoffset) / adA.mCount * peakA.loading - (peakB.mz - adB.mzoffset) / adB.mCount * peakB.loading) <= (max(peakA.mz, peakB.mz) * 2.5 * self.ppm / 1000000.0):
- peakA.adducts.append(
- Bunch(
- name=adA.name,
- partnerAdd=adB.name,
- toPeak=pb,
- )
- )
- peakB.adducts.append(
- Bunch(
- name=adB.name,
- partnerAdd=adA.name,
- toPeak=pa,
- )
- )
-
- # search for adducts of the form [2M+XX]+-
- elif peakA.xCount * 2 == peakB.xCount:
- for adA in adducts[peakA.ionMode]:
- for adB in adducts[peakB.ionMode]:
- if adA.mCount == 1 and adB.mCount == 2:
- if peakA.loading == adA.charge and peakB.loading == adB.charge and abs((peakA.mz - adA.mzoffset) / adA.mCount * peakA.loading - (peakB.mz - adB.mzoffset) / adB.mCount * peakB.loading) <= (max(peakA.mz, peakB.mz) * 2.5 * self.ppm / 1000000.0):
- peakA.adducts.append(
- Bunch(
- name=adA.name,
- partnerAdd=adB.name,
- toPeak=pb,
- )
- )
- peakB.adducts.append(
- Bunch(
- name=adB.name,
- partnerAdd=adA.name,
- toPeak=pa,
- )
- )
-
- def isAdductPairPresent(pA, adductAName, adductsA, pB, adductBName, adductsB, checkPartners=True):
- aFound = False
- for add in adductsA:
- if add.name == adductAName and (not checkPartners or (add.partnerAdd == adductBName and add.toPeak == pB)):
- aFound = True
- bFound = False
- for add in adductsB:
- if add.name == adductBName and (not checkPartners or (add.partnerAdd == adductAName and add.toPeak == pA)):
- bFound = True
- return aFound and bFound
-
- def removeAdductFromFeaturePair(pA, adductAName, adductsA):
- aFound = []
- for i, add in enumerate(adductsA):
- if add.name == adductAName:
- aFound.append(i)
-
- aFound = reversed(sorted(list(set(aFound))))
- for i in aFound:
- del adductsA[i]
-
- def removeAdductPair(pA, adductAName, adductsA, pB, adductBName, adductsB):
- aFound = []
- for i, add in enumerate(adductsA):
- if add.name == adductAName and add.partnerAdd == adductBName and add.toPeak == pB:
- aFound.append(i)
- bFound = []
- for i, add in enumerate(adductsB):
- if add.name == adductBName and add.partnerAdd == adductAName and add.toPeak == pA:
- bFound.append(i)
-
- aFound = reversed(sorted(list(set(aFound))))
- for i in aFound:
- del adductsA[i]
- bFound = reversed(sorted(list(set(bFound))))
- for i in bFound:
- del adductsB[i]
-
- # 2. remove incorrect annotations from feature pairs e.g. A: [M+H]+ and [M+Na]+ with B: [M+Na]+ and [M+2Na-H]+
- for p in group:
- peak = peaksInGroup[p]
- peak.adducts = list(set(peak.adducts))
-
- for pa in group:
- peakA = peaksInGroup[pa]
- if len(peakA.adducts) > 0:
- for pb in group:
- peakB = peaksInGroup[pb]
- if len(peakB.adducts) > 0:
- if peakA.mz < peakB.mz:
- for pc in group:
- peakC = peaksInGroup[pc]
- if len(peakC.adducts) > 0:
- if (
- isAdductPairPresent(
- pa,
- "[M+H]+",
- peakA.adducts,
- pb,
- "[M+Na]+",
- peakB.adducts,
- )
- and isAdductPairPresent(
- pa,
- "[M+Na]+",
- peakA.adducts,
- pb,
- "[M+2Na-H]+",
- peakB.adducts,
- )
- and isAdductPairPresent(
- pa,
- "[M+H]+",
- peakA.adducts,
- pc,
- "[M+2Na-H]+",
- peakC.adducts,
- )
- and isAdductPairPresent(
- pb,
- "[M+H]+",
- peakB.adducts,
- pc,
- "[M+Na]+",
- peakC.adducts,
- )
- and isAdductPairPresent(
- pb,
- "[M+Na]+",
- peakB.adducts,
- pc,
- "[M+2Na-H]+",
- peakC.adducts,
- )
- and peakA.mz < peakB.mz < peakC.mz
- ):
- removeAdductPair(
- pa,
- "[M+Na]+",
- peakA.adducts,
- pb,
- "[M+2Na-H]+",
- peakB.adducts,
- )
- removeAdductPair(
- pb,
- "[M+H]+",
- peakB.adducts,
- pc,
- "[M+Na]+",
- peakC.adducts,
- )
-
- if (
- isAdductPairPresent(
- pa,
- "[M+H]+",
- peakA.adducts,
- pb,
- "[M+K]+",
- peakB.adducts,
- )
- and isAdductPairPresent(
- pa,
- "[M+K]+",
- peakA.adducts,
- pb,
- "[M+2K-H]+",
- peakB.adducts,
- )
- and isAdductPairPresent(
- pa,
- "[M+H]+",
- peakA.adducts,
- pc,
- "[M+2K-H]+",
- peakC.adducts,
- )
- and isAdductPairPresent(
- pb,
- "[M+H]+",
- peakB.adducts,
- pc,
- "[M+K]+",
- peakC.adducts,
- )
- and isAdductPairPresent(
- pb,
- "[M+K]+",
- peakB.adducts,
- pc,
- "[M+2K-H]+",
- peakC.adducts,
- )
- and peakA.mz < peakB.mz < peakC.mz
- ):
- removeAdductPair(
- pa,
- "[M+K]+",
- peakA.adducts,
- pb,
- "[M+2K-H]+",
- peakB.adducts,
- )
- removeAdductPair(
- pb,
- "[M+H]+",
- peakB.adducts,
- pc,
- "[M+K]+",
- peakC.adducts,
- )
-
- if (
- isAdductPairPresent(
- pa,
- "[M+2H]++",
- peakA.adducts,
- pb,
- "[M+H+Na]++",
- peakB.adducts,
- )
- and isAdductPairPresent(
- pa,
- "[M+H+Na]++",
- peakA.adducts,
- pb,
- "[M+2Na]++",
- peakB.adducts,
- )
- and peakA.mz < peakB.mz
- ):
- removeAdductPair(
- pa,
- "[M+H+Na]++",
- peakA.adducts,
- pb,
- "[M+2Na]++",
- peakB.adducts,
- )
-
- if (
- isAdductPairPresent(
- pa,
- "[M+2H]++",
- peakA.adducts,
- pb,
- "[M+H+K]++",
- peakB.adducts,
- )
- and isAdductPairPresent(
- pa,
- "[M+H+K]++",
- peakA.adducts,
- pb,
- "[M+2K]++",
- peakB.adducts,
- )
- and peakA.mz < peakB.mz
- ):
- removeAdductPair(
- pa,
- "[M+H+Na]++",
- peakA.adducts,
- pb,
- "[M+2Na]++",
- peakB.adducts,
- )
-
- for pa in group:
- peakA = peaksInGroup[pa]
- if len(peakA.adducts) > 0:
- if isAdductPairPresent(
- pa,
- "[M+H]+",
- peakA.adducts,
- pa,
- "[2M+H]+",
- peakA.adducts,
- checkPartners=False,
- ):
- removeAdductFromFeaturePair(pa, "[M+H]+", peakA.adducts)
- if isAdductPairPresent(
- pa,
- "[M+NH4]+",
- peakA.adducts,
- pa,
- "[2M+NH4]+",
- peakA.adducts,
- checkPartners=False,
- ):
- removeAdductFromFeaturePair(pa, "[M+NH4]+", peakA.adducts)
- if isAdductPairPresent(
- pa,
- "[M+Na]+",
- peakA.adducts,
- pa,
- "[2M+Na]+",
- peakA.adducts,
- checkPartners=False,
- ):
- removeAdductFromFeaturePair(pa, "[M+Na]+", peakA.adducts)
- if isAdductPairPresent(
- pa,
- "[M+K]+",
- peakA.adducts,
- pa,
- "[2M+K]+",
- peakA.adducts,
- checkPartners=False,
- ):
- removeAdductFromFeaturePair(pa, "[M+K]+", peakA.adducts)
-
- inSourceFragments = {}
-
- # 3. iterate all peaks pairwise to find common in-source fragments
- for pa in group:
- peakA = peaksInGroup[pa]
-
- for pb in group:
- peakB = peaksInGroup[pb]
-
- # peakA shall always have the lower mass
- if peakA.mz < peakB.mz and abs(peakA.mz - peakB.mz) <= 101:
- mzDif = peakB.mz - peakA.mz
-
- # generate putative in-source fragments (using the labelled carbon atoms)
- if len(self.elements) > 0:
- elemDictReq = {}
- for elem in self.elements:
- elemDictReq[elem] = [
- self.elements[elem].weight,
- self.elements[elem].numberValenzElectrons,
- ]
- t = SGRGenerator(atoms=elemDictReq)
-
- useAtoms = []
- if self.labellingElement in self.elements.keys():
- useAtoms.append(self.labellingElement)
- for k in self.elements.keys():
- if k != self.labellingElement:
- useAtoms.append(k)
- else:
- useAtoms = list(self.elements.keys())
-
- try:
- atomsRange = []
- if self.labellingElement in useAtoms:
- if self.metabolisationExperiment:
- elem = self.elements[self.labellingElement]
- atomsRange.append(
- [
- abs(peakA.xCount - peakB.xCount),
- abs(peakA.xCount - peakB.xCount + elem.maxCount - elem.minCount),
- ]
- )
- else:
- atomsRange.append(abs(peakA.xCount - peakB.xCount))
-
- for elem in self.elements.keys():
- if elem != self.labellingElement:
- elem = self.elements[elem]
- atomsRange.append([elem.minCount, elem.maxCount])
-
- corrFact = abs(peakB.mz - peakA.mz)
- if corrFact <= 1:
- corrFact = 1.0
-
- pFs = t.findFormulas(
- mzDif,
- ppm=self.ppm * 2.0 * peakA.mz / corrFact,
- useAtoms=useAtoms,
- atomsRange=atomsRange,
- fixed=self.labellingElement,
- useSevenGoldenRules=False,
- )
-
- for pF in pFs:
- if pa not in inSourceFragments.keys():
- inSourceFragments[pa] = {}
- if pb not in inSourceFragments[pa].keys():
- inSourceFragments[pa][pb] = []
-
- c = fT.parseFormula(pF)
- mw = fT.calcMolWeight(c)
- abs(abs(peakB.mz - peakA.mz) - mw)
- sf = fT.flatToString(c, prettyPrintWithHTMLTags=False)
- inSourceFragments[pa][pb].append("%.4f-%s" % (peakB.mz, sf))
-
- except Exception as ex:
- print(f"ERROR: generating in-source fragments failed: {ex}")
-
- if self.simplifyInSourceFragments:
- for pa in group:
- peakA = peaksInGroup[pa]
- peakA.fDesc = []
-
- for pb in group:
- peakB = peaksInGroup[pb]
-
- for pc in group:
- peakC = peaksInGroup[pc]
-
- if peakA.mz < peakC.mz < peakB.mz:
- if pa in inSourceFragments.keys() and pb in inSourceFragments[pa].keys() and pc in inSourceFragments[pa].keys() and pc in inSourceFragments.keys() and pb in inSourceFragments[pc].keys():
- del inSourceFragments[pa][pc]
-
- for pa in group:
- peakA = peaksInGroup[pa]
-
- if pa in inSourceFragments.keys():
- for pb in inSourceFragments[pa].keys():
- for inFrag in inSourceFragments[pa][pb]:
- peakA.fDesc.append(inFrag)
-
- for pe in group:
- peak = peaksInGroup[pe]
- peak.fDesc = list(set(peak.fDesc))
- peak.adducts = list(set([a.name for a in peak.adducts]))
-
- if not hasattr(peak, "Ms"):
- setattr(peak, "Ms", [])
-
- peak.Ms = []
- for assignedAdduct in peak.adducts:
- if assignedAdduct in adductsDict.keys():
- peak.Ms.append((peak.mz - adductsDict[assignedAdduct].mzoffset) / adductsDict[assignedAdduct].mCount / peak.loading)
-
- # data processing step 6: convolute different feature pairs into feature groups using the chromatographic
- # profiles of different metabolite ions
- def groupFeaturePairsUntargetedAndWriteToDB(self, chromPeaks, mzxml, tracer, tracerID, reportFunction=None):
- try:
- db_con = PolarsDB(self.file + getDBSuffix(), format=getDBFormat())
-
- nodes = {}
- correlations = {}
-
- allPeaks = {}
- for peak in chromPeaks:
- nodes[peak.id] = []
- allPeaks[peak.id] = peak
- peak.correlationsToOthers = []
-
- # compare all detected feature pairs at approximately the same retention time
- for piA in range(len(chromPeaks)):
- peakA = chromPeaks[piA]
- if reportFunction is not None:
- reportFunction(
- 0.7 * piA / len(chromPeaks),
- "Matching features (%d remaining)" % (len(chromPeaks) - piA),
- )
-
- if peakA.id not in correlations.keys():
- correlations[peakA.id] = {}
-
- for peakB in chromPeaks:
- if peakB.id not in correlations.keys():
- correlations[peakB.id] = {}
-
- if peakA.mz < peakB.mz:
- if abs(peakA.NPeakCenter - peakB.NPeakCenter) < self.peakCenterError:
- bmin = int(
- max(
- 0,
- mean(
- [
- peakA.NPeakCenter - 1 * peakA.NBorderLeft,
- peakB.NPeakCenter - 1 * peakB.NBorderLeft,
- peakA.LPeakCenter - 1 * peakA.LBorderLeft,
- peakB.LPeakCenter - 1 * peakB.LBorderLeft,
- ]
- ),
- )
- )
- bmax = int(
- min(
- len(peakB.NXICSmoothed) - 1,
- mean(
- [
- peakB.NPeakCenter + 1 * peakB.NBorderRight,
- peakA.NPeakCenter + 1 * peakA.NBorderRight,
- peakB.LPeakCenter + 1 * peakB.LBorderRight,
- peakA.LPeakCenter + 1 * peakA.LBorderRight,
- ]
- ),
- )
- )
-
- pb = corr(
- peakA.NXICSmoothed[bmin:bmax],
- peakB.NXICSmoothed[bmin:bmax],
- )
-
- if str(pb) == "nan":
- pb = -1
-
- correlations[peakA.id][peakB.id] = pb
- correlations[peakB.id][peakA.id] = pb
-
- silRatiosA = peakA.silRatios.silRatios
- silRatiosB = peakB.silRatios.silRatios
-
- meanSilRatioA = weightedMean(silRatiosA, peakA.silRatios.peakNInts)
- meanSilRatioB = weightedMean(silRatiosB, peakB.silRatios.peakNInts)
-
- silRatiosFold = 0
- silRatiosSD = 1
-
- try:
- silRatiosFold = max(meanSilRatioA, meanSilRatioB) / min(meanSilRatioA, meanSilRatioB)
- silRatiosSD = weightedSd(
- [abs(r - meanSilRatioA) / meanSilRatioA for r in silRatiosA if min(r, meanSilRatioA) > 0] + [abs(r - meanSilRatioB) / meanSilRatioB for r in silRatiosB if min(r, meanSilRatioB) > 0],
- peakA.silRatios.peakNInts + peakB.silRatios.peakNInts,
- )
-
- # check for similar chromatographic peak profile and similar native to labeled ratio
- if pb >= self.minCorrelation and silRatiosFold <= 1 + max(0.25, 3 * silRatiosSD):
- nodes[peakA.id].append(peakB.id)
- nodes[peakB.id].append(peakA.id)
-
- peakB.correlationsToOthers.append(peakA.id)
- peakA.correlationsToOthers.append(peakB.id)
-
- except Exception as e:
- logging.error("Error while convoluting two feature pairs, skipping.. (%s)" % str(e))
-
- try:
- db_con.insert_row("featurefeatures", {"fID1": peakA.id, "fID2": peakB.id, "corr": pb, "silRatioValue": silRatiosFold})
- except Exception as e:
- logging.error("Error while convoluting two feature pairs, skipping.. (%s)" % str(e))
- db_con.insert_row("featurefeatures", {"fID1": peakA.id, "fID2": peakB.id, "corr": 0, "silRatioValue": 0})
-
- self.postMessageToProgressWrapper("text", "%s: Convoluting feature groups" % tracer.name)
-
- for peak in chromPeaks:
- delattr(peak, "NXIC")
- delattr(peak, "LXIC")
- delattr(peak, "times")
-
- for k in nodes.keys():
- uniq = []
- for u in nodes[k]:
- if u not in uniq:
- uniq.append(u)
- nodes[k] = uniq
-
- # get subgraphs from the feature pair graph. Each subgraph represents one convoluted
- # feature group
- tGroups = getSubGraphs(nodes)
-
- def splitGroupWithHCA(tGroup, correlations):
- # if 1 or two feature pairs are in a group, automatically use them (no further splitting possible)
- if len(tGroup) <= 2:
- return [tGroup]
-
- # construct 2-dimensional data matrix
- data = []
- for tG1 in tGroup:
- t = []
- for tG2 in tGroup:
- if tG1 in correlations.keys() and tG2 in correlations[tG1].keys():
- t.append(correlations[tG1][tG2])
- elif tG1 == tG2:
- t.append(1.0)
- else:
- t.append(0.0)
- data.append(t)
-
- # calculate HCA tree using the correlations between all feature pairs
- hc = HCA_general.HCA_generic()
- tree = hc.generateTree(objs=data, ids=tGroup)
- # hc.plotTree(tree)
- # print("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
-
- # split the HCA tree in sub-clusters
- def checkSubCluster(tree, hca, corrThreshold, cutOffMinRatio):
- if isinstance(tree, HCA_general.HCALeaf):
- return False
- elif isinstance(tree, HCA_general.HCAComposite):
- corrsLKid = hca.link(tree.getLeftKid())
- corrs = hca.link(tree)
- corrsRKid = hca.link(tree.getRightKid())
-
- aboveThresholdLKid = sum([corr > corrThreshold for corr in corrsLKid])
- aboveThreshold = sum([corr > corrThreshold for corr in corrs])
- aboveThresholdRKid = sum([corr > corrThreshold for corr in corrsRKid])
-
- # print(aboveThresholdLKid, aboveThreshold, aboveThresholdRKid)
-
- if (aboveThresholdLKid * 1.0 / len(corrs)) >= cutOffMinRatio and (aboveThreshold * 1.0 / len(corrs)) >= cutOffMinRatio and (aboveThresholdRKid * 1.0 / len(corrs)) >= cutOffMinRatio:
- return False
- else:
- return True
-
- # subClusts=hc.splitTreeWithCallbackBottomUp(tree,
- # CallBackMethod(_target=checkSubCluster, corrThreshold=self.minCorrelation, cutOffMinRatio=self.minCorrelationConnections).getRunMethod())
- subClusts = hc.splitTreeWithCallback(
- tree,
- CallBackMethod(
- _target=checkSubCluster,
- corrThreshold=self.minCorrelation,
- cutOffMinRatio=self.minCorrelationConnections,
- ).getRunMethod(),
- recursive=False,
- )
-
- # convert the subclusters into arrays of feature pairs belonging to the same metabolite
- return [[leaf.getID() for leaf in subClust.getLeaves()] for subClust in subClusts]
-
- groups = []
- done = 0
- for tGroup in tGroups:
- # print("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
- cGroups = [tGroup]
- while len(cGroups) > 0:
- # print("HCA with", len(cGroups))
- gGroup = cGroups.pop(0)
-
- ## TODO optimize this code, it recalculates the computationally expensive HCA too often for a high number of features
- if False and len(gGroup) > 100:
- groups.append(gGroup)
- continue
-
- sGroups = splitGroupWithHCA(gGroup, correlations)
- # print("first group", len(gGroup), gGroup, "split into", sGroups)
-
- if len(sGroups) == 1:
- groups.append(sGroups[0])
- else:
- cGroups.extend(sGroups)
-
- done = done + 1
- self.postMessageToProgressWrapper(
- "text",
- "%s: Convoluting feature groups (%d/%d done)" % (tracer.name, done, len(tGroups)),
- )
-
- if True:
- done = 0
- for gi in range(len(groups)):
- group = groups[gi]
- if reportFunction is not None:
- reportFunction(
- 0.7 + 0.3 * piA / len(chromPeaks),
- "Searching for relationships (%d remaining)" % (len(groups) - gi),
- )
-
- # first, search for adduct relationships (same number of carbon atoms) in each convoluted
- # feature group
-
- peaksInGroup = {}
- for a in group:
- peaksInGroup[a] = allPeaks[a]
-
- temp = []
- for j in range(len(peaksInGroup[a].correlationsToOthers)):
- if peaksInGroup[a].correlationsToOthers[j] in group:
- temp.append(peaksInGroup[a].correlationsToOthers[j])
-
- peaksInGroup[a].correlationsToOthers = temp
-
- self.annotateChromPeaks(group, peaksInGroup) # store feature pair annotation in the database
-
- done = done + 1
- self.postMessageToProgressWrapper(
- "text",
- "%s: Annotating feature groups (%d/%d done)" % (tracer.name, done, len(groups)),
- )
-
- for peak in chromPeaks:
- adds = countEntries(peak.adducts)
- peak.adducts = list(adds.keys())
-
- # Update chromPeaks
- db_con.tables["chromPeaks"] = db_con.tables["chromPeaks"].with_columns(
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.adducts)).decode("utf-8"))).otherwise(pl.col("adducts")).alias("adducts"),
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.fDesc)).decode("utf-8"))).otherwise(pl.col("fDesc")).alias("fDesc"),
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.correlationsToOthers)).decode("utf-8"))).otherwise(pl.col("correlationsToOthers")).alias("correlationsToOthers"),
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.heteroAtomsFeaturePairs)).decode("utf-8"))).otherwise(pl.col("heteroAtomsFeaturePairs")).alias("heteroAtomsFeaturePairs"),
- )
-
- # Update allChromPeaks
- db_con.tables["allChromPeaks"] = db_con.tables["allChromPeaks"].with_columns(
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.adducts)).decode("utf-8"))).otherwise(pl.col("adducts")).alias("adducts"),
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.fDesc)).decode("utf-8"))).otherwise(pl.col("fDesc")).alias("fDesc"),
- pl.when(pl.col("id") == peak.id).then(pl.lit(base64.b64encode(dumps(peak.heteroAtomsFeaturePairs)).decode("utf-8"))).otherwise(pl.col("heteroAtomsFeaturePairs")).alias("heteroAtomsFeaturePairs"),
- )
-
- # store feature group in the database
- for group in sorted(
- groups,
- key=lambda x: sum([allPeaks[p].NPeakCenterMin / 60.0 for p in x]) / len(x),
- ):
- db_con.insert_row("featureGroups", {"id": self.curFeatureGroupID, "featureName": "fg_%d" % self.curFeatureGroupID, "tracer": tracerID})
- groupMeanElutionIndex = 0
-
- hasPos = False
- hasNeg = False
-
- for p in sorted(group, key=lambda x: allPeaks[x].mz):
- hasPos = hasPos or allPeaks[p].ionMode == "+"
- hasNeg = hasNeg or allPeaks[p].ionMode == "-"
-
- groupMeanElutionIndex += allPeaks[p].NPeakCenter
-
- allPeaks[p].fGroupID = self.curFeatureGroupID
- db_con.insert_row("featureGroupFeatures", {"fID": p, "fDesc": "", "fGroupID": self.curFeatureGroupID})
-
- groupMeanElutionIndex = groupMeanElutionIndex / len(group)
-
- # store one positve and one negative ionisation mode MS scan in the database for
- # later visualisation (one for each convoluted feature group)
- if hasPos:
- scan = mzxml.getIthMS1Scan(int(groupMeanElutionIndex), self.positiveScanEvent)
-
- db_con.insert_row("massspectrum", {"mID": self.curMassSpectrumID, "fgID": self.curFeatureGroupID, "time": scan.retention_time, "mzs": ";".join([str(u) for u in scan.mz_list]), "intensities": ";".join([str(u) for u in scan.intensity_list]), "ionMode": "+"})
- self.curMassSpectrumID += 1
- if hasNeg:
- scan = mzxml.getIthMS1Scan(int(groupMeanElutionIndex), self.negativeScanEvent)
-
- db_con.insert_row("massspectrum", {"mID": self.curMassSpectrumID, "fgID": self.curFeatureGroupID, "time": scan.retention_time, "mzs": ";".join([str(u) for u in scan.mz_list]), "intensities": ";".join([str(u) for u in scan.intensity_list]), "ionMode": "-"})
- self.curMassSpectrumID += 1
-
- self.curFeatureGroupID += 1
-
- db_con.commit()
- self.printMessage(
- "%s: Feature grouping done. " % tracer.name + str(len(groups)) + " feature groups",
- type="info",
- )
-
- db_con.commit()
- db_con.close()
-
- except Exception as ex:
- traceback.print_exc()
-
- self.printMessage("Error in %s: %s" % (self.file, str(ex)), type="error")
- self.postMessageToProgressWrapper("failed", self.pID)
-
- # store one MS scan for each detected feature pair in the database
- def writeMassSpectraToDB(self, chromPeaks, mzxml, reportFunction=None):
- db_con = PolarsDB(self.file + getDBSuffix(), format=getDBFormat())
-
- massSpectraWrittenPos = {}
- massSpectraWrittenNeg = {}
- for pi in range(len(chromPeaks)):
- peak = chromPeaks[pi]
- if reportFunction is not None:
- reportFunction(
- 1.0 * pi / len(chromPeaks),
- "%d feature pairs remaining" % (len(chromPeaks) - pi),
- )
-
- scanEvent = ""
- iMode = ""
- massSpectraWritten = None
- if peak.ionMode == "+":
- iMode = "+"
- scanEvent = self.positiveScanEvent
- massSpectraWritten = massSpectraWrittenPos
- elif peak.ionMode == "-":
- iMode = "-"
- scanEvent = self.negativeScanEvent
- massSpectraWritten = massSpectraWrittenNeg
-
- if peak.NPeakCenter not in massSpectraWritten.keys():
- scan = mzxml.getIthMS1Scan(peak.NPeakCenter, scanEvent)
-
- db_con.insert_row("massspectrum", {"mID": self.curMassSpectrumID, "fgID": -1, "time": scan.retention_time, "mzs": ";".join([str(u) for u in scan.mz_list]), "intensities": ";".join([str(u) for u in scan.intensity_list]), "ionMode": iMode})
-
- massSpectraWritten[peak.NPeakCenter] = self.curMassSpectrumID
- self.curMassSpectrumID = self.curMassSpectrumID + 1
-
- # Update massSpectrumID in chromPeaks
- db_con.tables["chromPeaks"] = db_con.tables["chromPeaks"].with_columns(pl.when(pl.col("id") == peak.id).then(pl.lit(massSpectraWritten[peak.NPeakCenter])).otherwise(pl.col("massSpectrumID")).alias("massSpectrumID"))
-
- db_con.commit()
- db_con.close()
-
- ## write a new featureML file
- def writeResultsToFeatureML(self, forFile):
- db_con = PolarsDB(forFile + getDBSuffix(), format=getDBFormat())
-
- features = []
-
- # Get chromPeaks data directly from Polars DataFrame
- df = db_con.tables.get("chromPeaks", pl.DataFrame())
- for row in df.to_dicts():
- chromPeak = ChromPeakPair()
- chromPeak.id = row["id"]
- chromPeak.mz = row["mz"]
- chromPeak.lmz = row["lmz"]
- chromPeak.xCount = row["xcount"]
- chromPeak.loading = row["Loading"]
- chromPeak.NPeakCenterMin = row["NPeakCenterMin"]
- chromPeak.ionMode = row["ionMode"]
-
- b = Bunch(
- id=chromPeak.id,
- ogroup="-1",
- mz=chromPeak.mz,
- rt=chromPeak.NPeakCenterMin,
- Xn=chromPeak.xCount,
- lmz=chromPeak.lmz,
- charge=chromPeak.loading,
- name=chromPeak.id,
- ionMode=chromPeak.ionMode,
- )
- features.append(b)
-
- exportAsFeatureML.writeFeatureListToFeatureML(features, forFile + ".featureML", ppmPM=self.ppm, rtPM=0.25 * 60)
-
- # store the detected feature pairs in a new mzXML file. Only those MS peaks will be included, which contribute to
- # the chromatographic peaks of a valid feature pair
- def writeResultsToNewMZXMLIntermediateObject(self, mzxml, newMZXMLData, chromPeaks):
- for peak in chromPeaks:
- # writeMZXML: 0001/1: 12C 0010/2: 12C-Iso 0100/4: 13C-Iso 1000/8: 13C
-
- if peak.ionMode == "+":
- scanEv = self.positiveScanEvent
- elif peak.ionMode == "-":
- scanEv = self.negativeScanEvent
-
- if self.writeMZXML & 1:
- scans, times, scanIds = mzxml.getArea(
- peak.NPeakCenter - peak.NBorderLeft,
- peak.NPeakCenter + peak.NBorderRight,
- peak.mz,
- self.chromPeakPPM,
- scanEv,
- )
-
- for w in range(len(scans)):
- curScan = scans[w]
- scanid = scanIds[w]
- if scanid not in newMZXMLData:
- newMZXMLData[scanid] = Bunch(mzs=[], ints=[], rt=mzxml.getScanByID(scanid).retention_time)
- for mz, inte in curScan:
- newMZXMLData[scanid].mzs.append(mz)
- newMZXMLData[scanid].ints.append(inte)
- if self.writeMZXML & 2:
- scans, times, scanIds = mzxml.getArea(
- peak.NPeakCenter - peak.NBorderLeft,
- peak.NPeakCenter + peak.NBorderRight,
- peak.mz + self.xOffset / peak.loading,
- self.chromPeakPPM,
- scanEv,
- )
-
- for w in range(len(scans)):
- curScan = scans[w]
- scanid = scanIds[w]
- if scanid not in newMZXMLData:
- newMZXMLData[scanid] = Bunch(mzs=[], ints=[], rt=mzxml.getScanByID(scanid).retention_time)
- for mz, inte in curScan:
- newMZXMLData[scanid].mzs.append(mz)
- newMZXMLData[scanid].ints.append(inte)
- if self.writeMZXML & 4:
- scans, times, scanIds = mzxml.getArea(
- peak.NPeakCenter - peak.NBorderLeft,
- peak.NPeakCenter + peak.NBorderRight,
- peak.mz + (peak.xCount - 1) * self.xOffset / peak.loading,
- self.chromPeakPPM,
- scanEv,
- )
-
- for w in range(len(scans)):
- curScan = scans[w]
- scanid = scanIds[w]
- if scanid not in newMZXMLData:
- newMZXMLData[scanid] = Bunch(mzs=[], ints=[], rt=mzxml.getScanByID(scanid).retention_time)
- for mz, inte in curScan:
- newMZXMLData[scanid].mzs.append(mz)
- newMZXMLData[scanid].ints.append(inte)
- if self.writeMZXML & 8:
- scans, times, scanIds = mzxml.getArea(
- peak.NPeakCenter - peak.NBorderLeft,
- peak.NPeakCenter + peak.NBorderRight,
- peak.mz + peak.xCount * self.xOffset / peak.loading,
- self.chromPeakPPM,
- scanEv,
- )
-
- for w in range(len(scans)):
- curScan = scans[w]
- scanid = scanIds[w]
- if scanid not in newMZXMLData:
- newMZXMLData[scanid] = Bunch(mzs=[], ints=[], rt=mzxml.getScanByID(scanid).retention_time)
- for mz, inte in curScan:
- newMZXMLData[scanid].mzs.append(mz)
- newMZXMLData[scanid].ints.append(inte)
-
- # helper method if more than one tracer substance is used
- def writeIntermediateMZXMLDataToNewMZXMLFile(self, mzxml, newMZXMLData):
- try:
- self.printMessage("Writing MzXML file..", type="info")
- if ".mzxml" in self.file.lower():
- toFile = self.file[0 : self.file.lower().rfind(".mzxml")] + ".proc.mzXML"
- mzxml.resetMZData(self.file, toFile, newMZXMLData)
- elif ".mzml" in self.file.lower() and False: ## mzml export currently not supported
- toFile = self.file[0 : self.file.lower().rfind(".mzml")] + "proc.mzML"
- self.printMessage("Only mzXML files can be written", type="error")
- else:
- return RuntimeError("Invalid file. Cannot write new data")
-
- except Exception as ex:
- self.printMessage("Cannot write MzXML file.. (%s)" % ex, type="error")
-
- # stores the TICs of the LC-HRMS data in the database
- def writeTICsToDB(self, mzxml, scanEvents):
- db_con = PolarsDB(self.file + getDBSuffix(), format=getDBFormat())
-
- ## save TICs
- ## save mean, sd scan time
- i = 1
- for pol, scanEvent in scanEvents.items():
- if scanEvent != "None":
- TIC, times, scanIds = mzxml.getTIC(filterLine=scanEvent)
-
- # Insert into tics table
- db_con.insert_row("tics", {"id": i, "polarity": pol, "scanevent": scanEvent, "times": ";".join([str(t) for t in times]), "intensities": ";".join(["%.0f" % t for t in TIC])})
- i = i + 1
-
- scanTimes = [times[i + 1] - times[i] for i in range(len(times) - 1)]
-
- # Insert stats
- db_con.insert_row("stats", {"key": "MeanScanTime_%s" % pol, "value": str(mean(scanTimes))})
- db_con.insert_row("stats", {"key": "SDScanTime_%s" % pol, "value": str(sd(scanTimes))})
- db_con.insert_row("stats", {"key": "NumberOfScans_%s" % pol, "value": str(len(times))})
- db_con.insert_row("stats", {"key": "TotalNumberOfSignals_%s" % pol, "value": str(mzxml.getSignalCount(filterLine=scanEvent))})
-
- minInt, maxInt, avgInt = mzxml.getMinMaxAvgSignalIntensities(filterLine=scanEvent)
- db_con.insert_row("stats", {"key": "MinSignalInt_%s" % pol, "value": str(minInt)})
- db_con.insert_row("stats", {"key": "MaxSignalInt_%s" % pol, "value": str(maxInt)})
- db_con.insert_row("stats", {"key": "AvgSignalInt_%s" % pol, "value": str(avgInt)})
-
- db_con.commit()
- db_con.close()
-
- # method, which is called by the multiprocessing module to actually process the LC-HRMS data
- def findIsoPairs(self):
- try:
- start = time.time()
-
- # region Initialize data processing pipeline
- ######################################################################################
-
- self.postMessageToProgressWrapper("start")
- self.postMessageToProgressWrapper("max", 100.0)
- self.postMessageToProgressWrapper("value", 0.0)
- self.postMessageToProgressWrapper("text", "Initialising")
-
- self.BL = Baseline.Baseline()
-
- self.curPeakId = 1
- self.curEICId = 1
- self.curMZId = 1
- self.curMZBinId = 1
- self.curFeatureGroupID = 1
- self.curMassSpectrumID = 1
- # endregion
-
- # region Create results database
- ######################################################################################
- self.postMessageToProgressWrapper("text", "Creating results DB")
-
- if os.path.exists(self.file + getDBSuffix()) and os.path.isfile(self.file + getDBSuffix()):
- os.remove(self.file + getDBSuffix())
- db_con = PolarsDB(self.file + getDBSuffix(), format=getDBFormat())
-
- self.writeConfigurationToDB(db_con)
-
- db_con.close()
- # endregion
-
- # region Parse mzXML file
- ######################################################################################
-
- self.postMessageToProgressWrapper("text", "Parsing chromatogram file")
-
- mzxml = self.parseMzXMLFile()
- newMZXMLData = {}
-
- self.writeTICsToDB(mzxml, {"+": self.positiveScanEvent, "-": self.negativeScanEvent})
-
- # endregion
-
- # Start calculation
- ######################################################################################
-
- self.postMessageToProgressWrapper("text", "Starting data processing")
- tracerProgressWidth = 100.0 / 1
-
- tracerNum = 1
-
- # region Process configured tracer
- ######################################################################################
-
- tracer = self.configuredTracer
-
- curTracerProgress = tracerNum / 1
-
- tracerID = 0
- if self.metabolisationExperiment:
- if tracer is None:
- self.printMessage("Metabolisation experiment requires a configured tracer, but none was available in the identify method.", type="error")
- raise Exception("Metabolisation experiment requires a configured tracer, but none was available in the identify method.")
-
- tracerID = tracer.id
- self.printMessage("Tracer: %s" % tracer.name, type="info")
-
- ##################################################################################################
- # Attention: delta mz for one labelling atom is always saved in the member variable self.xOffset #
- ##################################################################################################
-
- self.xOffset = getIsotopeMass(tracer.isotopeB)[0] - getIsotopeMass(tracer.isotopeA)[0]
-
- else:
- # Full metabolome labelling experiment
- pass
- # endregion
-
- # region 1. Find 12C 13C partners in the mz dimension (0-25%)
- ######################################################################################
-
- self.postMessageToProgressWrapper("value", curTracerProgress + 0 * tracerProgressWidth)
- self.postMessageToProgressWrapper("text", "%s: Extracting signal pairs" % tracer.name)
-
- def reportFunction(curVal, text):
- self.postMessageToProgressWrapper(
- "value",
- curTracerProgress + 0 * tracerProgressWidth + 0.25 * curVal * tracerProgressWidth,
- )
- self.postMessageToProgressWrapper("text", "%s: Extracting signal pairs (%s)" % (tracer.name, text))
-
- mzs, negFound, posFound = self.findSignalPairs(curTracerProgress, mzxml, tracer, reportFunction)
- self.writeSignalPairsToDB(mzs, mzxml, tracerID)
-
- self.printMessage(
- "%s: Extracting signal pairs done. pos: %d neg: %d mzs (including mismatches)" % (tracer.name, posFound, negFound),
- type="info",
- )
- # endregion
-
- # region 2. Cluster found mz values according to mz value and number of x atoms (25-35%)
- ######################################################################################
-
- self.postMessageToProgressWrapper("value", curTracerProgress + 0.25 * tracerProgressWidth)
- self.postMessageToProgressWrapper("text", "%s: Clustering found signal pairs" % tracer.name)
-
- def reportFunction(curVal, text):
- self.postMessageToProgressWrapper(
- "value",
- curTracerProgress + 0.25 * tracerProgressWidth + 0.1 * curVal * tracerProgressWidth,
- )
- self.postMessageToProgressWrapper(
- "text",
- "%s: Clustering found signal pairs (%s)" % (tracer.name, text),
- )
-
- mzbins = self.clusterFeaturePairs(mzs, reportFunction)
- self.writeFeaturePairClustersToDB(mzbins)
- mzbins = self.removeImpossibleFeaturePairClusters(mzbins)
-
- self.printMessage(
- "%s: Clustering found signal pairs done. pos: %d neg: %d mz bins (including mismatches)" % (tracer.name, len(mzbins["+"]), len(mzbins["-"])),
- type="info",
- )
- # endregion
-
- # region 3. Extract chromatographic peaks (35-65%)
- ######################################################################################
-
- self.postMessageToProgressWrapper("value", curTracerProgress + 0.35 * tracerProgressWidth)
- self.postMessageToProgressWrapper("text", "%s: Separating feature pairs" % tracer.name)
-
- def reportFunction(curVal, text):
- self.postMessageToProgressWrapper(
- "value",
- curTracerProgress + 0.35 * tracerProgressWidth + 0.3 * curVal * tracerProgressWidth,
- )
- self.postMessageToProgressWrapper("text", "%s: Separating feature pairs (%s)" % (tracer.name, text))
-
- chromPeaks = self.findChromatographicPeaksAndWriteToDB(mzbins, mzxml, tracerID, reportFunction)
-
- self.printMessage(
- "%s: Separating feature pairs done. pos: %d neg: %d chromatographic peaks (including mismatches)"
- % (
- tracer.name,
- len([c for c in chromPeaks if c.ionMode == "+"]),
- len([c for c in chromPeaks if c.ionMode == "-"]),
- ),
- type="info",
- )
- # endregion
-
- # region 4. Remove isotopolog feature pairs and other false positive findings (65-70%)
- ######################################################################################
-
- self.postMessageToProgressWrapper("value", curTracerProgress + 0.65 * tracerProgressWidth)
- self.postMessageToProgressWrapper("text", "%s: Removing false positive feature pairs" % tracer.name)
-
- def reportFunction(curVal, text):
- self.postMessageToProgressWrapper(
- "value",
- curTracerProgress + 0.65 * tracerProgressWidth + 0.05 * curVal * tracerProgressWidth,
- )
- self.postMessageToProgressWrapper(
- "text",
- "%s: Removing false positive feature pairs (%s)" % (tracer.name, text),
- )
-
- self.removeFalsePositiveFeaturePairsAndUpdateDB(chromPeaks, reportFunction)
-
- self.printMessage(
- "%s: Removing false positive feature pairs done. %d chromatographic peaks" % (tracer.name, len(chromPeaks)),
- type="info",
- )
- # endregion
-
- # region 5. Search for hetero atoms (70-75%)
- ######################################################################################
-
- self.postMessageToProgressWrapper("value", curTracerProgress + 0.7 * tracerProgressWidth)
- self.postMessageToProgressWrapper("text", "%s: Searching for hetero atoms" % tracer.name)
-
- def reportFunction(curVal, text):
- self.postMessageToProgressWrapper(
- "value",
- curTracerProgress + 0.7 * tracerProgressWidth + 0.05 * curVal * tracerProgressWidth,
- )
- self.postMessageToProgressWrapper("text", "%s: Annotating feature pairs (%s)" % (tracer.name, text))
-
- self.annotateFeaturePairs(chromPeaks, mzxml, tracer, reportFunction)
- # endregion
-
- # region 6. Group feature pairs untargeted using chromatographic peak shape into feature groups (75-80%)
- ######################################################################################
-
- self.postMessageToProgressWrapper("value", curTracerProgress + 0.75 * tracerProgressWidth)
- self.postMessageToProgressWrapper("text", "%s: Grouping feature pairs" % tracer.name)
-
- def reportFunction(curVal, text):
- self.postMessageToProgressWrapper(
- "value",
- curTracerProgress + 0.75 * tracerProgressWidth + 0.05 * curVal * tracerProgressWidth,
- )
- self.postMessageToProgressWrapper("text", "%s: Grouping feature pairs (%s)" % (tracer.name, text))
-
- self.groupFeaturePairsUntargetedAndWriteToDB(chromPeaks, mzxml, tracer, tracerID, reportFunction)
- # endregion
-
- # region 7. Extract mass spectra for feature pairs and feature groups (80-95%)
- ######################################################################################
-
- self.postMessageToProgressWrapper("value", curTracerProgress + 0.8 * tracerProgressWidth)
- self.postMessageToProgressWrapper("text", "%s: Extracting mass spectra to DB" % tracer.name)
-
- def reportFunction(curVal, text):
- self.postMessageToProgressWrapper(
- "value",
- curTracerProgress + 0.8 * tracerProgressWidth + 0.15 * curVal * tracerProgressWidth,
- )
- self.postMessageToProgressWrapper(
- "text",
- "%s: Extracting mass spectra to DB (%s)" % (tracer.name, text),
- )
-
- self.writeMassSpectraToDB(chromPeaks, mzxml, reportFunction)
-
- # Log time used for processing of individual files
- elapsed = (time.time() - start) / 60.0
- hours = ""
- if elapsed >= 60.0:
- if elapsed < 120.0:
- hours = "1 hour "
- else:
- hours = "%d hours " % (elapsed // 60)
- mins = "%.2f min(s)" % (elapsed % 60.0)
-
- self.printMessage(
- "%s: Calculations finished (%s%s).." % (tracer.name, hours, mins),
- type="info",
- )
- # endregion
-
- # region 7b. Calculate isotopolog ratios for each feature pair
- ######################################################################################
-
- self.postMessageToProgressWrapper("text", "%s: Calculating isotopolog ratios" % tracer.name)
-
- def reportFunction(curVal, text):
- self.postMessageToProgressWrapper("text", "%s: Calculating isotopolog ratios (%s)" % (tracer.name, text))
-
- self.calculateIsotopologRatiosForFeaturePairs(chromPeaks, mzxml, reportFunction)
-
- self.printMessage(
- "%s: Isotopolog ratio calculation done." % tracer.name,
- type="info",
- )
- # endregion
-
- # region 8. Write results to files (95-100%, without progress indicator)
- ######################################################################################
-
- # W.1 Save results to new MzXML file (intermediate step) (95-100%)
-
- if self.writeMZXML:
- self.postMessageToProgressWrapper("value", curTracerProgress + 0.95 * tracerProgressWidth)
- self.postMessageToProgressWrapper("text", "%s: Writing results to mzXML.." % tracer.name)
-
- self.writeResultsToNewMZXMLIntermediateObject(mzxml, newMZXMLData, chromPeaks)
- # endregion
-
- self.postMessageToProgressWrapper("text", "Writing results to mzXML..")
-
- # region W.2 Write results to TSV File
- ##########################################################################################
- if self.writeFeatureML:
- self.postMessageToProgressWrapper("text", "Writing results to featureML..")
-
- self.writeResultsToFeatureML(self.file)
- # endregion
-
- # region W.4 Save all results to new MzXML file
- ##########################################################################################
- if self.writeMZXML:
- self.postMessageToProgressWrapper("text", "Writing results to new mzXML file")
-
- self.writeIntermediateMZXMLDataToNewMZXMLFile(mzxml, newMZXMLData)
- # endregion
-
- mzxml.freeMe()
- self.printMessage("%s done.." % self.file, type="info")
- self.postMessageToProgressWrapper("end")
-
- except Exception as ex:
- self.printMessage("Error in %s: %s" % (self.file, str(ex)), type="error")
- self.postMessageToProgressWrapper("failed")
- traceback.print_exc()
diff --git a/src/style.css b/src/style.css
index 18eb646..b119075 100644
--- a/src/style.css
+++ b/src/style.css
@@ -458,6 +458,7 @@ QProgressBar {
text-align: center;
color: #202124;
font-size: 11px;
+ min-height: 22px;
}
QProgressBar::chunk {
diff --git a/tests/test_metabolite_grouping.py b/tests/test_metabolite_grouping.py
new file mode 100644
index 0000000..33e9359
--- /dev/null
+++ b/tests/test_metabolite_grouping.py
@@ -0,0 +1,89 @@
+from src.metaboliteGrouping import split_group_by_relative_abundance
+
+
+def _normalize_groups(groups):
+ return sorted([sorted(group) for group in groups])
+
+
+def test_split_group_by_relative_abundance_splits_dissimilar_profiles():
+ groups = split_group_by_relative_abundance(
+ [1, 2, 3, 4],
+ {
+ 1: [100, 200, 300],
+ 2: [50, 100, 150],
+ 3: [300, 200, 100],
+ 4: [150, 100, 50],
+ },
+ min_peak_correlation=0.8,
+ min_connection_rate=0.6,
+ )
+
+ assert _normalize_groups(groups) == [[1, 2], [3, 4]]
+
+
+def test_split_group_by_relative_abundance_keeps_group_for_missing_vectors():
+ groups = split_group_by_relative_abundance(
+ [10, 11, 12],
+ {
+ 10: [100, 200, 300],
+ 11: [120, 220, 320],
+ },
+ min_peak_correlation=0.8,
+ min_connection_rate=0.6,
+ )
+
+ assert groups == [[10, 11, 12]]
+
+
+def test_split_group_by_relative_abundance_respects_similarity_threshold():
+ abundance_vectors = {
+ 1: [100, 200, 300, 400],
+ 2: [90, 210, 280, 420],
+ 3: [400, 300, 200, 100],
+ }
+
+ relaxed_groups = split_group_by_relative_abundance(
+ [1, 2, 3],
+ abundance_vectors,
+ min_peak_correlation=-1.0,
+ min_connection_rate=0.6,
+ )
+ strict_groups = split_group_by_relative_abundance(
+ [1, 2, 3],
+ abundance_vectors,
+ min_peak_correlation=0.8,
+ min_connection_rate=0.6,
+ )
+
+ assert _normalize_groups(relaxed_groups) == [[1, 2, 3]]
+ assert _normalize_groups(strict_groups) == [[1, 2], [3]]
+
+
+def test_split_group_by_relative_abundance_ignores_mismatched_zero_presence():
+ groups = split_group_by_relative_abundance(
+ [1, 2, 3],
+ {
+ 1: [10, 0, 11, 0],
+ 2: [9, 0, 12, 0],
+ 3: [0, 20, 0, 18],
+ },
+ min_peak_correlation=0.7,
+ min_connection_rate=0.6,
+ )
+
+ assert _normalize_groups(groups) == [[1, 2], [3]]
+
+
+def test_split_group_by_relative_abundance_keeps_constant_proportional_profiles_together():
+ groups = split_group_by_relative_abundance(
+ [1, 2, 3],
+ {
+ 1: [10, 10, 0, 0],
+ 2: [20, 20, 0, 0],
+ 3: [0, 0, 30, 30],
+ },
+ min_peak_correlation=0.8,
+ min_connection_rate=0.6,
+ )
+
+ assert _normalize_groups(groups) == [[1, 2], [3]]
diff --git a/uv.lock b/uv.lock
index 7234aae..e40db96 100644
--- a/uv.lock
+++ b/uv.lock
@@ -40,6 +40,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" },
]
+[[package]]
+name = "certifi"
+version = "2026.4.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
+]
+
[[package]]
name = "charset-normalizer"
version = "3.4.3"
@@ -175,6 +184,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/25/dd83b280a4a405f7e1558951492714935e0c1601724d35a0db970afd0ad0/dbus_fast-4.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:069df5d46390299d7fccfc0e704af504a8b75794c2c3bc0246b6db0db1f245b0", size = 849052, upload-time = "2026-04-02T04:50:16.563Z" },
]
+[[package]]
+name = "deprecated"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wrapt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
+]
+
[[package]]
name = "desktop-notifier"
version = "6.2.0"
@@ -250,6 +271,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/a4/247d3e54eb5ed59e94e09866cfc4f9567e274fbf310ba390711851f63b3b/fonttools-4.60.0-py3-none-any.whl", hash = "sha256:496d26e4d14dcccdd6ada2e937e4d174d3138e3d73f5c9b6ec6eb2fd1dab4f66", size = 1142186, upload-time = "2025-09-17T11:33:59.287Z" },
]
+[[package]]
+name = "idna"
+version = "3.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
+]
+
[[package]]
name = "iniconfig"
version = "2.1.0"
@@ -301,30 +331,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" },
]
+[[package]]
+name = "llvmlite"
+version = "0.44.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306, upload-time = "2025-01-20T11:14:09.035Z" },
+ { url = "https://files.pythonhosted.org/packages/01/cf/1dd5a60ba6aee7122ab9243fd614abcf22f36b0437cbbe1ccf1e3391461c/llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e", size = 26201090, upload-time = "2025-01-20T11:14:15.401Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf", size = 42361904, upload-time = "2025-01-20T11:14:22.949Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/e1/12c5f20cb9168fb3464a34310411d5ad86e4163c8ff2d14a2b57e5cc6bac/llvmlite-0.44.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc", size = 41184245, upload-time = "2025-01-20T11:14:31.731Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193, upload-time = "2025-01-20T11:14:38.578Z" },
+]
+
[[package]]
name = "lxml"
-version = "6.0.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8f/bd/f9d01fd4132d81c6f43ab01983caea69ec9614b913c290a26738431a015d/lxml-6.0.1.tar.gz", hash = "sha256:2b3a882ebf27dd026df3801a87cf49ff791336e0f94b0fad195db77e01240690", size = 4070214, upload-time = "2025-08-22T10:37:53.525Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/43/c4/cd757eeec4548e6652eff50b944079d18ce5f8182d2b2cf514e125e8fbcb/lxml-6.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:485eda5d81bb7358db96a83546949c5fe7474bec6c68ef3fa1fb61a584b00eea", size = 8405139, upload-time = "2025-08-22T10:33:34.09Z" },
- { url = "https://files.pythonhosted.org/packages/ff/99/0290bb86a7403893f5e9658490c705fcea103b9191f2039752b071b4ef07/lxml-6.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d12160adea318ce3d118f0b4fbdff7d1225c75fb7749429541b4d217b85c3f76", size = 4585954, upload-time = "2025-08-22T10:33:36.294Z" },
- { url = "https://files.pythonhosted.org/packages/88/a7/4bb54dd1e626342a0f7df6ec6ca44fdd5d0e100ace53acc00e9a689ead04/lxml-6.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48c8d335d8ab72f9265e7ba598ae5105a8272437403f4032107dbcb96d3f0b29", size = 4944052, upload-time = "2025-08-22T10:33:38.19Z" },
- { url = "https://files.pythonhosted.org/packages/71/8d/20f51cd07a7cbef6214675a8a5c62b2559a36d9303fe511645108887c458/lxml-6.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:405e7cf9dbdbb52722c231e0f1257214202dfa192327fab3de45fd62e0554082", size = 5098885, upload-time = "2025-08-22T10:33:40.035Z" },
- { url = "https://files.pythonhosted.org/packages/5a/63/efceeee7245d45f97d548e48132258a36244d3c13c6e3ddbd04db95ff496/lxml-6.0.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:299a790d403335a6a057ade46f92612ebab87b223e4e8c5308059f2dc36f45ed", size = 5017542, upload-time = "2025-08-22T10:33:41.896Z" },
- { url = "https://files.pythonhosted.org/packages/57/5d/92cb3d3499f5caba17f7933e6be3b6c7de767b715081863337ced42eb5f2/lxml-6.0.1-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:48da704672f6f9c461e9a73250440c647638cc6ff9567ead4c3b1f189a604ee8", size = 5347303, upload-time = "2025-08-22T10:33:43.868Z" },
- { url = "https://files.pythonhosted.org/packages/69/f8/606fa16a05d7ef5e916c6481c634f40870db605caffed9d08b1a4fb6b989/lxml-6.0.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:21e364e1bb731489e3f4d51db416f991a5d5da5d88184728d80ecfb0904b1d68", size = 5641055, upload-time = "2025-08-22T10:33:45.784Z" },
- { url = "https://files.pythonhosted.org/packages/b3/01/15d5fc74ebb49eac4e5df031fbc50713dcc081f4e0068ed963a510b7d457/lxml-6.0.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bce45a2c32032afddbd84ed8ab092130649acb935536ef7a9559636ce7ffd4a", size = 5242719, upload-time = "2025-08-22T10:33:48.089Z" },
- { url = "https://files.pythonhosted.org/packages/42/a5/1b85e2aaaf8deaa67e04c33bddb41f8e73d07a077bf9db677cec7128bfb4/lxml-6.0.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:fa164387ff20ab0e575fa909b11b92ff1481e6876835014e70280769920c4433", size = 4717310, upload-time = "2025-08-22T10:33:49.852Z" },
- { url = "https://files.pythonhosted.org/packages/42/23/f3bb1292f55a725814317172eeb296615db3becac8f1a059b53c51fc1da8/lxml-6.0.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7587ac5e000e1594e62278422c5783b34a82b22f27688b1074d71376424b73e8", size = 5254024, upload-time = "2025-08-22T10:33:52.22Z" },
- { url = "https://files.pythonhosted.org/packages/b4/be/4d768f581ccd0386d424bac615d9002d805df7cc8482ae07d529f60a3c1e/lxml-6.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:57478424ac4c9170eabf540237125e8d30fad1940648924c058e7bc9fb9cf6dd", size = 5055335, upload-time = "2025-08-22T10:33:54.041Z" },
- { url = "https://files.pythonhosted.org/packages/40/07/ed61d1a3e77d1a9f856c4fab15ee5c09a2853fb7af13b866bb469a3a6d42/lxml-6.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:09c74afc7786c10dd6afaa0be2e4805866beadc18f1d843cf517a7851151b499", size = 4784864, upload-time = "2025-08-22T10:33:56.382Z" },
- { url = "https://files.pythonhosted.org/packages/01/37/77e7971212e5c38a55431744f79dff27fd751771775165caea096d055ca4/lxml-6.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7fd70681aeed83b196482d42a9b0dc5b13bab55668d09ad75ed26dff3be5a2f5", size = 5657173, upload-time = "2025-08-22T10:33:58.698Z" },
- { url = "https://files.pythonhosted.org/packages/32/a3/e98806d483941cd9061cc838b1169626acef7b2807261fbe5e382fcef881/lxml-6.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:10a72e456319b030b3dd900df6b1f19d89adf06ebb688821636dc406788cf6ac", size = 5245896, upload-time = "2025-08-22T10:34:00.586Z" },
- { url = "https://files.pythonhosted.org/packages/07/de/9bb5a05e42e8623bf06b4638931ea8c8f5eb5a020fe31703abdbd2e83547/lxml-6.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0fa45fb5f55111ce75b56c703843b36baaf65908f8b8d2fbbc0e249dbc127ed", size = 5267417, upload-time = "2025-08-22T10:34:02.719Z" },
- { url = "https://files.pythonhosted.org/packages/f2/43/c1cb2a7c67226266c463ef8a53b82d42607228beb763b5fbf4867e88a21f/lxml-6.0.1-cp313-cp313-win32.whl", hash = "sha256:01dab65641201e00c69338c9c2b8a0f2f484b6b3a22d10779bb417599fae32b5", size = 3610051, upload-time = "2025-08-22T10:34:04.553Z" },
- { url = "https://files.pythonhosted.org/packages/34/96/6a6c3b8aa480639c1a0b9b6faf2a63fb73ab79ffcd2a91cf28745faa22de/lxml-6.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:bdf8f7c8502552d7bff9e4c98971910a0a59f60f88b5048f608d0a1a75e94d1c", size = 4009325, upload-time = "2025-08-22T10:34:06.24Z" },
- { url = "https://files.pythonhosted.org/packages/8c/66/622e8515121e1fd773e3738dae71b8df14b12006d9fb554ce90886689fd0/lxml-6.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a6aeca75959426b9fd8d4782c28723ba224fe07cfa9f26a141004210528dcbe2", size = 3670443, upload-time = "2025-08-22T10:34:07.974Z" },
+version = "6.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/58/25e00bb40b185c974cfe156c110474d9a8a8390d5f7c92a4e328189bb60e/lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d", size = 4617892, upload-time = "2026-04-18T04:32:01.78Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" },
+ { url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" },
+ { url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/10/e9842d2ec322ea65f0a7270aa0315a53abed06058b88ef1b027f620e7a5f/lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9", size = 5341578, upload-time = "2026-04-18T04:33:44.596Z" },
+ { url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" },
+ { url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" },
+ { url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/73/16596f7e4e38fa33084b9ccbccc22a15f82a290a055126f2c1541236d2ff/lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62", size = 3596938, upload-time = "2026-04-18T04:31:56.206Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16", size = 3995728, upload-time = "2026-04-18T04:31:58.763Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/e8/c358a38ac3e541d16a1b527e4e9cb78c0419b0506a070ace11777e5e8404/lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d", size = 3658372, upload-time = "2026-04-18T04:32:03.629Z" },
]
[[package]]
@@ -339,9 +382,37 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c", size = 38094, upload-time = "2023-09-25T09:10:14.188Z" },
]
+[[package]]
+name = "matchms"
+version = "0.33.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "deprecated" },
+ { name = "lxml" },
+ { name = "matplotlib" },
+ { name = "networkx" },
+ { name = "numba" },
+ { name = "numpy" },
+ { name = "pandas" },
+ { name = "pickydict" },
+ { name = "pubchempy" },
+ { name = "pynndescent" },
+ { name = "pyteomics" },
+ { name = "pyyaml" },
+ { name = "rdkit" },
+ { name = "requests" },
+ { name = "scipy" },
+ { name = "sparsestack" },
+ { name = "tqdm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8c/0b/29e9b7c2eb19391af23be895ce85dad5922edc68840fca1143e018b8fe3a/matchms-0.33.0.tar.gz", hash = "sha256:a4cf7ffab8190ac909b598fa83565e2509f584d53bde3fab1aba993e9d194b0a", size = 394784, upload-time = "2026-05-12T13:19:41.844Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/26/f975122c7cc4ba9960f67be3e4fea5d23e04a691df95ea95991087ddd0bf/matchms-0.33.0-py3-none-any.whl", hash = "sha256:39774cd39e8e643cceed99c03003ed57deaafe70156f70130ceec2f40c97273f", size = 213756, upload-time = "2026-05-12T13:19:43.653Z" },
+]
+
[[package]]
name = "matplotlib"
-version = "3.10.6"
+version = "3.10.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "contourpy" },
@@ -354,22 +425,22 @@ dependencies = [
{ name = "pyparsing" },
{ name = "python-dateutil" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a0/59/c3e6453a9676ffba145309a73c462bb407f4400de7de3f2b41af70720a3c/matplotlib-3.10.6.tar.gz", hash = "sha256:ec01b645840dd1996df21ee37f208cd8ba57644779fa20464010638013d3203c", size = 34804264, upload-time = "2025-08-30T00:14:25.137Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/db/18380e788bb837e724358287b08e223b32bc8dccb3b0c12fa8ca20bc7f3b/matplotlib-3.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:819e409653c1106c8deaf62e6de6b8611449c2cd9939acb0d7d4e57a3d95cc7a", size = 8273231, upload-time = "2025-08-30T00:13:13.881Z" },
- { url = "https://files.pythonhosted.org/packages/d3/0f/38dd49445b297e0d4f12a322c30779df0d43cb5873c7847df8a82e82ec67/matplotlib-3.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59c8ac8382fefb9cb71308dde16a7c487432f5255d8f1fd32473523abecfecdf", size = 8128730, upload-time = "2025-08-30T00:13:15.556Z" },
- { url = "https://files.pythonhosted.org/packages/e5/b8/9eea6630198cb303d131d95d285a024b3b8645b1763a2916fddb44ca8760/matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84e82d9e0fd70c70bc55739defbd8055c54300750cbacf4740c9673a24d6933a", size = 8698539, upload-time = "2025-08-30T00:13:17.297Z" },
- { url = "https://files.pythonhosted.org/packages/71/34/44c7b1f075e1ea398f88aeabcc2907c01b9cc99e2afd560c1d49845a1227/matplotlib-3.10.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25f7a3eb42d6c1c56e89eacd495661fc815ffc08d9da750bca766771c0fd9110", size = 9529702, upload-time = "2025-08-30T00:13:19.248Z" },
- { url = "https://files.pythonhosted.org/packages/b5/7f/e5c2dc9950c7facaf8b461858d1b92c09dd0cf174fe14e21953b3dda06f7/matplotlib-3.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9c862d91ec0b7842920a4cfdaaec29662195301914ea54c33e01f1a28d014b2", size = 9593742, upload-time = "2025-08-30T00:13:21.181Z" },
- { url = "https://files.pythonhosted.org/packages/ff/1d/70c28528794f6410ee2856cd729fa1f1756498b8d3126443b0a94e1a8695/matplotlib-3.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:1b53bd6337eba483e2e7d29c5ab10eee644bc3a2491ec67cc55f7b44583ffb18", size = 8122753, upload-time = "2025-08-30T00:13:23.44Z" },
- { url = "https://files.pythonhosted.org/packages/e8/74/0e1670501fc7d02d981564caf7c4df42974464625935424ca9654040077c/matplotlib-3.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:cbd5eb50b7058b2892ce45c2f4e92557f395c9991f5c886d1bb74a1582e70fd6", size = 7992973, upload-time = "2025-08-30T00:13:26.632Z" },
- { url = "https://files.pythonhosted.org/packages/b1/4e/60780e631d73b6b02bd7239f89c451a72970e5e7ec34f621eda55cd9a445/matplotlib-3.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:acc86dd6e0e695c095001a7fccff158c49e45e0758fdf5dcdbb0103318b59c9f", size = 8316869, upload-time = "2025-08-30T00:13:28.262Z" },
- { url = "https://files.pythonhosted.org/packages/f8/15/baa662374a579413210fc2115d40c503b7360a08e9cc254aa0d97d34b0c1/matplotlib-3.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e228cd2ffb8f88b7d0b29e37f68ca9aaf83e33821f24a5ccc4f082dd8396bc27", size = 8178240, upload-time = "2025-08-30T00:13:30.007Z" },
- { url = "https://files.pythonhosted.org/packages/c6/3f/3c38e78d2aafdb8829fcd0857d25aaf9e7dd2dfcf7ec742765b585774931/matplotlib-3.10.6-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:658bc91894adeab669cf4bb4a186d049948262987e80f0857216387d7435d833", size = 8711719, upload-time = "2025-08-30T00:13:31.72Z" },
- { url = "https://files.pythonhosted.org/packages/96/4b/2ec2bbf8cefaa53207cc56118d1fa8a0f9b80642713ea9390235d331ede4/matplotlib-3.10.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8913b7474f6dd83ac444c9459c91f7f0f2859e839f41d642691b104e0af056aa", size = 9541422, upload-time = "2025-08-30T00:13:33.611Z" },
- { url = "https://files.pythonhosted.org/packages/83/7d/40255e89b3ef11c7871020563b2dd85f6cb1b4eff17c0f62b6eb14c8fa80/matplotlib-3.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:091cea22e059b89f6d7d1a18e2c33a7376c26eee60e401d92a4d6726c4e12706", size = 9594068, upload-time = "2025-08-30T00:13:35.833Z" },
- { url = "https://files.pythonhosted.org/packages/f0/a9/0213748d69dc842537a113493e1c27daf9f96bd7cc316f933dc8ec4de985/matplotlib-3.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:491e25e02a23d7207629d942c666924a6b61e007a48177fdd231a0097b7f507e", size = 8200100, upload-time = "2025-08-30T00:13:37.668Z" },
- { url = "https://files.pythonhosted.org/packages/be/15/79f9988066ce40b8a6f1759a934ea0cde8dc4adc2262255ee1bc98de6ad0/matplotlib-3.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3d80d60d4e54cda462e2cd9a086d85cd9f20943ead92f575ce86885a43a565d5", size = 8042142, upload-time = "2025-08-30T00:13:39.426Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" },
+ { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" },
+ { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" },
+ { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" },
+ { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" },
]
[[package]]
@@ -410,34 +481,58 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
+[[package]]
+name = "networkx"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" },
+]
+
+[[package]]
+name = "numba"
+version = "0.61.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "llvmlite" },
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785, upload-time = "2025-04-09T02:57:59.96Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/71/91b277d712e46bd5059f8a5866862ed1116091a7cb03bd2704ba8ebe015f/numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140", size = 2773289, upload-time = "2025-04-09T02:58:01.435Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab", size = 3893918, upload-time = "2025-04-09T02:58:02.933Z" },
+ { url = "https://files.pythonhosted.org/packages/17/58/064f4dcb7d7e9412f16ecf80ed753f92297e39f399c905389688cf950b81/numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e", size = 3584056, upload-time = "2025-04-09T02:58:04.538Z" },
+ { url = "https://files.pythonhosted.org/packages/af/a4/6d3a0f2d3989e62a18749e1e9913d5fa4910bbb3e3311a035baea6caf26d/numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7", size = 2831846, upload-time = "2025-04-09T02:58:06.125Z" },
+]
+
[[package]]
name = "numpy"
-version = "2.3.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588, upload-time = "2025-09-09T15:56:59.087Z" },
- { url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802, upload-time = "2025-09-09T15:57:01.73Z" },
- { url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537, upload-time = "2025-09-09T15:57:03.765Z" },
- { url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", size = 6640743, upload-time = "2025-09-09T15:57:07.921Z" },
- { url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", size = 14278881, upload-time = "2025-09-09T15:57:11.349Z" },
- { url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", size = 16636301, upload-time = "2025-09-09T15:57:14.245Z" },
- { url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", size = 16053645, upload-time = "2025-09-09T15:57:16.534Z" },
- { url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", size = 18578179, upload-time = "2025-09-09T15:57:18.883Z" },
- { url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8", size = 6312250, upload-time = "2025-09-09T15:57:21.296Z" },
- { url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf", size = 12783269, upload-time = "2025-09-09T15:57:23.034Z" },
- { url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5", size = 10195314, upload-time = "2025-09-09T15:57:25.045Z" },
- { url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", size = 21048025, upload-time = "2025-09-09T15:57:27.257Z" },
- { url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", size = 14301053, upload-time = "2025-09-09T15:57:30.077Z" },
- { url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", size = 5229444, upload-time = "2025-09-09T15:57:32.733Z" },
- { url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", size = 6738039, upload-time = "2025-09-09T15:57:34.328Z" },
- { url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", size = 14352314, upload-time = "2025-09-09T15:57:36.255Z" },
- { url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", size = 16701722, upload-time = "2025-09-09T15:57:38.622Z" },
- { url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", size = 16132755, upload-time = "2025-09-09T15:57:41.16Z" },
- { url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", size = 18651560, upload-time = "2025-09-09T15:57:43.459Z" },
- { url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd", size = 6442776, upload-time = "2025-09-09T15:57:45.793Z" },
- { url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d", size = 12927281, upload-time = "2025-09-09T15:57:47.492Z" },
- { url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1", size = 10265275, upload-time = "2025-09-09T15:57:49.647Z" },
+version = "2.2.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
+ { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
+ { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
+ { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
+ { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
+ { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
+ { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
+ { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
+ { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
+ { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
]
[[package]]
@@ -506,6 +601,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791, upload-time = "2023-02-07T12:28:36.678Z" },
]
+[[package]]
+name = "pickydict"
+version = "0.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6c/20/ec9aa324363cc10a9f2cf37bda9a469f1f725dcce4e25b8c37adbdcff303/pickydict-0.5.0.tar.gz", hash = "sha256:fc66944dc5782c1078ffe41cf4dd8fdf6702f9d63c5373748403c12ea2da8cc6", size = 5514, upload-time = "2024-09-25T16:07:46.8Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/ad/40ba18d3a88ccce689687a5b6b246f7c651823c48d2e3051d9cd53afafe5/pickydict-0.5.0-py3-none-any.whl", hash = "sha256:a6614d2aacbe07d3d4b0b6e7c9e9834eae7b8c0127bad4b1127b6fdcebd5cd65", size = 6228, upload-time = "2024-09-25T16:07:45.617Z" },
+]
+
[[package]]
name = "pillow"
version = "11.3.0"
@@ -601,6 +705,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/26/65/1070a6e3c036f39142c2820c4b52e9243246fcfc3f96239ac84472ba361e/psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07", size = 244971, upload-time = "2025-09-17T20:15:12.262Z" },
]
+[[package]]
+name = "pubchempy"
+version = "1.0.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/a1/1b63d717a315b5fdc281e312bd02e9069e22ed8aa63a56d79f8dae95a1f2/pubchempy-1.0.5.tar.gz", hash = "sha256:08f0b2a82a5caa5d61e14935d655da554602d7b5686fe661ab584c882ffff623", size = 26814, upload-time = "2025-09-08T20:53:01.971Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/e3/2c887645f21b94d992a16775bcf81cdf6dcc36cf1606782cb2a3c86e9a35/pubchempy-1.0.5-py3-none-any.whl", hash = "sha256:e936cfed31fa194042ad463be3c803dde5b12ef2f795caf336e3114127c34fa0", size = 21355, upload-time = "2025-09-08T20:53:00.831Z" },
+]
+
[[package]]
name = "pycodestyle"
version = "2.14.0"
@@ -678,6 +791,7 @@ dependencies = [
{ name = "desktop-notifier" },
{ name = "fastexcel" },
{ name = "lxml" },
+ { name = "matchms" },
{ name = "matplotlib" },
{ name = "numpy" },
{ name = "openpyxl" },
@@ -723,6 +837,7 @@ requires-dist = [
{ name = "fastexcel", specifier = ">=0.19.0" },
{ name = "flake8", marker = "extra == 'dev'", specifier = ">=3.8" },
{ name = "lxml", specifier = ">=4.6.0" },
+ { name = "matchms", specifier = ">=0.33.0" },
{ name = "matplotlib", specifier = ">=3.3.0" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=0.900" },
{ name = "numpy", specifier = ">=1.20.0" },
@@ -768,6 +883,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/58/6b/a3547bc34db0e8b37f55524df915271a67b15164f838d4d1c3b2374697eb/pymzml-2.5.11-py3-none-any.whl", hash = "sha256:f0f507f94977fefd2b319a613c5b55679db858374aadae31769603994d0ad64f", size = 17466151, upload-time = "2025-01-31T14:03:05.612Z" },
]
+[[package]]
+name = "pynndescent"
+version = "0.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "joblib" },
+ { name = "llvmlite" },
+ { name = "numba" },
+ { name = "scikit-learn" },
+ { name = "scipy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4a/fb/7f58c397fb31666756457ee2ac4c0289ef2daad57f4ae4be8dec12f80b03/pynndescent-0.6.0.tar.gz", hash = "sha256:7ffde0fb5b400741e055a9f7d377e3702e02250616834231f6c209e39aac24f5", size = 2992987, upload-time = "2026-01-08T21:29:58.943Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b2/e6/94145d714402fd5ade00b5661f2d0ab981219e07f7db9bfa16786cdb9c04/pynndescent-0.6.0-py3-none-any.whl", hash = "sha256:dc8c74844e4c7f5cbd1e0cd6909da86fdc789e6ff4997336e344779c3d5538ef", size = 73511, upload-time = "2026-01-08T21:29:57.306Z" },
+]
+
[[package]]
name = "pyparsing"
version = "3.2.4"
@@ -834,6 +965,16 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/91/70/db78afc8b60b2e53f99145bde2f644cca43924a4dd869ffe664e0792730a/pyside6_essentials-6.9.2-cp39-abi3-win_arm64.whl", hash = "sha256:ecd7b5cd9e271f397fb89a6357f4ec301d8163e50869c6c557f9ccc6bed42789", size = 49561720, upload-time = "2025-08-26T07:49:43.708Z" },
]
+[[package]]
+name = "pyteomics"
+version = "4.7.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1a/cd/b7eb951a99ad3ab425a2dbc6e38af076116d13ebcbe144019a2bd37dc093/pyteomics-4.7.5.tar.gz", hash = "sha256:382aeaa8b921bdd2a7e5b4aa9fe46c6184bb43701205a845b4b861ee3e88f46a", size = 236493, upload-time = "2024-10-18T13:26:29.903Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/59/2ea7637f20a0e2d082fc0cae021d20934aa49ac8154db817a1a3fa9e27b5/pyteomics-4.7.5-py2.py3-none-any.whl", hash = "sha256:9b8008ad8d8bbbc6856c4e804bc88e018df44809cd9a86900862b311e760862d", size = 238983, upload-time = "2024-10-18T13:26:25.923Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/62/b5d706255739553398d3a308d92e2476d5a363ad3ca0598c51bc75cc5054/pyteomics-4.7.5-py3-none-any.whl", hash = "sha256:5155e1d2581845926e49b0abd0be8cfd6ea45ffd3511958b805347037c5934c8", size = 238978, upload-time = "2024-10-18T13:26:27.764Z" },
+]
+
[[package]]
name = "pytest"
version = "8.4.2"
@@ -929,6 +1070,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
]
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+]
+
+[[package]]
+name = "rdkit"
+version = "2026.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "pillow" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/c1/b3e8469291a9725eda29e911987f39be8a625b78f78d64bdc731ddda852f/rdkit-2026.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c30bf6846b2f55a24b0e2beba7f0480dd948a73bfc13ad95cdb29f839586a497", size = 29984328, upload-time = "2026-05-16T11:47:10.123Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/46/47f8ab91a05a6b060852a8484b555256a8f32b740c67560b00560ef56eb5/rdkit-2026.3.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fb14f634f630cac27e6484bf01a789a2ae8e8dbea96c87e365ff956b31c384a2", size = 35575509, upload-time = "2026-05-16T11:47:15.281Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/ed/568c508712bb4f1b94b9b85f3a514aedcd37da73fb08d6ef22a3b0981698/rdkit-2026.3.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9908eff69e89da927006b9513adaf7985b46bf0b213cf204a2529d367e932dda", size = 37089613, upload-time = "2026-05-16T11:47:20.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/39/8e095f9406d7416b9c1284817c62049dccc801008ee98a8aaa7a68c609bf/rdkit-2026.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:b4a3e15fd1e9986284288362ef415478d5a895c9409c72a3fa9320e728b76ca3", size = 24612214, upload-time = "2026-05-16T11:47:23.628Z" },
+]
+
[[package]]
name = "regex"
version = "2025.11.3"
@@ -978,6 +1152,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/52/c8/aaf4e08679e7b1dc896ad30de0d0527f0fd55582c2e6deee4f2cc899bf9f/reportlab-4.4.3-py3-none-any.whl", hash = "sha256:df905dc5ec5ddaae91fc9cb3371af863311271d555236410954961c5ee6ee1b5", size = 1953896, upload-time = "2025-07-23T11:18:20.572Z" },
]
+[[package]]
+name = "requests"
+version = "2.34.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
+]
+
[[package]]
name = "rubicon-objc"
version = "0.5.4"
@@ -1013,33 +1202,31 @@ wheels = [
[[package]]
name = "scipy"
-version = "1.16.2"
+version = "1.15.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/4c/3b/546a6f0bfe791bbb7f8d591613454d15097e53f906308ec6f7c1ce588e8e/scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b", size = 30580599, upload-time = "2025-09-11T17:48:08.271Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/27/c5b52f1ee81727a9fc457f5ac1e9bf3d6eab311805ea615c83c27ba06400/scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70", size = 36604856, upload-time = "2025-09-11T17:41:47.695Z" },
- { url = "https://files.pythonhosted.org/packages/32/a9/15c20d08e950b540184caa8ced675ba1128accb0e09c653780ba023a4110/scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9", size = 28864626, upload-time = "2025-09-11T17:41:52.642Z" },
- { url = "https://files.pythonhosted.org/packages/4c/fc/ea36098df653cca26062a627c1a94b0de659e97127c8491e18713ca0e3b9/scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5", size = 20855689, upload-time = "2025-09-11T17:41:57.886Z" },
- { url = "https://files.pythonhosted.org/packages/dc/6f/d0b53be55727f3e6d7c72687ec18ea6d0047cf95f1f77488b99a2bafaee1/scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925", size = 23512151, upload-time = "2025-09-11T17:42:02.303Z" },
- { url = "https://files.pythonhosted.org/packages/11/85/bf7dab56e5c4b1d3d8eef92ca8ede788418ad38a7dc3ff50262f00808760/scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9", size = 33329824, upload-time = "2025-09-11T17:42:07.549Z" },
- { url = "https://files.pythonhosted.org/packages/da/6a/1a927b14ddc7714111ea51f4e568203b2bb6ed59bdd036d62127c1a360c8/scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7", size = 35681881, upload-time = "2025-09-11T17:42:13.255Z" },
- { url = "https://files.pythonhosted.org/packages/c1/5f/331148ea5780b4fcc7007a4a6a6ee0a0c1507a796365cc642d4d226e1c3a/scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb", size = 36006219, upload-time = "2025-09-11T17:42:18.765Z" },
- { url = "https://files.pythonhosted.org/packages/46/3a/e991aa9d2aec723b4a8dcfbfc8365edec5d5e5f9f133888067f1cbb7dfc1/scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e", size = 38682147, upload-time = "2025-09-11T17:42:25.177Z" },
- { url = "https://files.pythonhosted.org/packages/a1/57/0f38e396ad19e41b4c5db66130167eef8ee620a49bc7d0512e3bb67e0cab/scipy-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c", size = 38520766, upload-time = "2025-09-11T17:43:25.342Z" },
- { url = "https://files.pythonhosted.org/packages/1b/a5/85d3e867b6822d331e26c862a91375bb7746a0b458db5effa093d34cdb89/scipy-1.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104", size = 25451169, upload-time = "2025-09-11T17:43:30.198Z" },
- { url = "https://files.pythonhosted.org/packages/09/d9/60679189bcebda55992d1a45498de6d080dcaf21ce0c8f24f888117e0c2d/scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1", size = 37012682, upload-time = "2025-09-11T17:42:30.677Z" },
- { url = "https://files.pythonhosted.org/packages/83/be/a99d13ee4d3b7887a96f8c71361b9659ba4ef34da0338f14891e102a127f/scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a", size = 29389926, upload-time = "2025-09-11T17:42:35.845Z" },
- { url = "https://files.pythonhosted.org/packages/bf/0a/130164a4881cec6ca8c00faf3b57926f28ed429cd6001a673f83c7c2a579/scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f", size = 21381152, upload-time = "2025-09-11T17:42:40.07Z" },
- { url = "https://files.pythonhosted.org/packages/47/a6/503ffb0310ae77fba874e10cddfc4a1280bdcca1d13c3751b8c3c2996cf8/scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4", size = 23914410, upload-time = "2025-09-11T17:42:44.313Z" },
- { url = "https://files.pythonhosted.org/packages/fa/c7/1147774bcea50d00c02600aadaa919facbd8537997a62496270133536ed6/scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21", size = 33481880, upload-time = "2025-09-11T17:42:49.325Z" },
- { url = "https://files.pythonhosted.org/packages/6a/74/99d5415e4c3e46b2586f30cdbecb95e101c7192628a484a40dd0d163811a/scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7", size = 35791425, upload-time = "2025-09-11T17:42:54.711Z" },
- { url = "https://files.pythonhosted.org/packages/1b/ee/a6559de7c1cc710e938c0355d9d4fbcd732dac4d0d131959d1f3b63eb29c/scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8", size = 36178622, upload-time = "2025-09-11T17:43:00.375Z" },
- { url = "https://files.pythonhosted.org/packages/4e/7b/f127a5795d5ba8ece4e0dce7d4a9fb7cb9e4f4757137757d7a69ab7d4f1a/scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472", size = 38783985, upload-time = "2025-09-11T17:43:06.661Z" },
- { url = "https://files.pythonhosted.org/packages/3e/9f/bc81c1d1e033951eb5912cd3750cc005943afa3e65a725d2443a3b3c4347/scipy-1.16.2-cp313-cp313t-win_amd64.whl", hash = "sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351", size = 38631367, upload-time = "2025-09-11T17:43:14.44Z" },
- { url = "https://files.pythonhosted.org/packages/d6/5e/2cc7555fd81d01814271412a1d59a289d25f8b63208a0a16c21069d55d3e/scipy-1.16.2-cp313-cp313t-win_arm64.whl", hash = "sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d", size = 25787992, upload-time = "2025-09-11T17:43:19.745Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" },
+ { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" },
+ { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" },
+ { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" },
+ { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" },
+ { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" },
+ { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" },
+ { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" },
+ { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" },
]
[[package]]
@@ -1072,6 +1259,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
+[[package]]
+name = "sparsestack"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numba" },
+ { name = "numpy" },
+ { name = "scipy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/59/9a/d2493b2760052cac9661304e2010616a377eba9101f5f1b9c3441af41dfc/sparsestack-0.7.0.tar.gz", hash = "sha256:920d81f130f9390bcf491ddb60e5924d1223eaa63d818b8476b4eb57e461ed5b", size = 9348, upload-time = "2025-05-26T15:31:21.145Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bd/06/1b8dc3021ec7139db7cfd99ecd3853d953f44cdab239ad79006111569335/sparsestack-0.7.0-py3-none-any.whl", hash = "sha256:6e63a9d549397bf8c0ae7df407fe91f40d0076191bdb5e4ff043d26e2fbfc0e3", size = 9776, upload-time = "2025-05-26T15:31:19.836Z" },
+]
+
[[package]]
name = "termcolor"
version = "3.1.0"
@@ -1090,6 +1291,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
]
+[[package]]
+name = "tqdm"
+version = "4.67.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
+]
+
[[package]]
name = "typing-extensions"
version = "4.15.0"
@@ -1108,6 +1321,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
+[[package]]
+name = "urllib3"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
+]
+
[[package]]
name = "winrt-runtime"
version = "3.2.1"
@@ -1192,6 +1414,37 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/10/89012eeb14b932eb3f0ac541d8788d794e09f8fe1934fdae1760d0cbd252/winrt_windows_ui_notifications-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:aec78f6a632e2ae57a55b7aee60f8df848ccffd1c4d14c3c9fbfaa6bb564e1a2", size = 155184, upload-time = "2025-06-06T14:11:53.863Z" },
]
+[[package]]
+name = "wrapt"
+version = "2.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" },
+ { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" },
+ { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" },
+ { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" },
+ { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" },
+ { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" },
+ { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" },
+ { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" },
+ { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" },
+ { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" },
+ { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" },
+ { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" },
+]
+
[[package]]
name = "xlrd"
version = "2.0.2"