From 5349d78bfb0a957ff65e069e726a25be75cfac8e Mon Sep 17 00:00:00 2001 From: Sugar Ray <150720362+RayMSMS@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:00:24 -0500 Subject: [PATCH 01/12] Fix the bug on localization probability (#2632) * Fix the bug by capping the p to 1 * add a tester to reach the line * finish my unit test --- .../GlycoSearch/GlycoSearchEngine.cs | 6 +- MetaMorpheus/Test/GlycoSearchEngineTest.cs | 80 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 MetaMorpheus/Test/GlycoSearchEngineTest.cs diff --git a/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs b/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs index 4847b4e09a..8681df4139 100644 --- a/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs +++ b/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs @@ -363,7 +363,11 @@ private GlycoSpectralMatch CreateGsm(Ms2ScanWithSpecificMass theScan, int scanIn var psmGlyco = new GlycoSpectralMatch(peptideWithMod, 0, PeptideScore, scanIndex, theScan, CommonParameters, matchedIons); //TO DO: This p is from childScan p, it works for HCD-pd-EThcD, which may not work for other type. - psmGlyco.ScanInfo_p = p; + psmGlyco.ScanInfo_p = p > 1? 1 : p; + // p is the probability of randomly matching a single theoretical fragment ion. + // With a wide mass tolerance (> 0.4 Da), the computed p may exceed 1, + // which is not physically meaningful and can cause numerical issues. + // In such cases, p is capped at 1. psmGlyco.Thero_n = n; diff --git a/MetaMorpheus/Test/GlycoSearchEngineTest.cs b/MetaMorpheus/Test/GlycoSearchEngineTest.cs new file mode 100644 index 0000000000..cdcc9a73b2 --- /dev/null +++ b/MetaMorpheus/Test/GlycoSearchEngineTest.cs @@ -0,0 +1,80 @@ +using EngineLayer; +using EngineLayer.GlycoSearch; +using MassSpectrometry; +using MzLibUtil; +using NUnit.Framework; +using Omics.Modifications; +using Org.BouncyCastle.Asn1.Cmp; +using Proteomics; +using Proteomics.ProteolyticDigestion; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using TaskLayer; +using ThermoFisher.CommonCore.Data.Business; +using static Org.BouncyCastle.Asn1.Cmp.Challenge; + +namespace Test +{ + [TestFixture] + public class GlycoSearchEngineTest + { + [Test] + public void CreateGsm_WithWideProductTolerance_ScanInfo_p_IsCappedToOne() + { + // Use a very wide product mass tolerance to force p > 1 so ScanInfo_p is capped at 1. + var commonParameters = new CommonParameters(productMassTolerance: new AbsoluteTolerance(1_000_000), dissociationType: DissociationType.HCD, trimMsMsPeaks: false); + + // ensure glycan DB paths used by GlycoSearchEngine ctor are registered (filenames must match ctor arguments) + string oglycanPath = "OGlycan.gdb"; + string nglycanPath = "NGlycan_ForNoSearch.gdb"; + if (!GlobalVariables.OGlycanDatabasePaths.Contains(oglycanPath)) GlobalVariables.OGlycanDatabasePaths.Add(oglycanPath); + if (!GlobalVariables.NGlycanDatabasePaths.Contains(nglycanPath)) GlobalVariables.NGlycanDatabasePaths.Add(nglycanPath); + + // Load the test MGF file and get MS2 scans + string spectraFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\2019_09_16_StcEmix_35trig_EThcD25_rep1_9906.mgf"); + var myFileManager = new MyFileManager(true); + var msDataFile = myFileManager.LoadFile(spectraFile,commonParameters); + var ms2ScanWithSpecificMass = MetaMorpheusTask.GetMs2Scans(msDataFile, spectraFile, commonParameters).ToArray(); + Assert.That(ms2ScanWithSpecificMass.Length, Is.GreaterThan(0), "No MS2 scans found in test MGF."); + + // Create a peptide instance to satisfy CreateGsm inputs. + var protein = new Protein("AATVGSLAGQPLQER", "accession"); + var peptides = protein.Digest(new DigestionParams(minPeptideLength: 1), new List(), new List()).ToList(); + var peptide = peptides.First(); + var peptideIndex = new List { peptide }; + var globalGsms = new List[ms2ScanWithSpecificMass.Length]; + + // Create a GlycoSearchEngine instance using the wide product mass tolerance. + var engine = new GlycoSearchEngine(globalGsms, ms2ScanWithSpecificMass, peptideIndex, null, null, 0, commonParameters, null, oglycanPath, nglycanPath, glycoSearchType: GlycoSearchType.OGlycanSearch, 30, 3, false, null); + + // Create minimal inputs for CreateGsm. + var route = new Route(); + route.AddPos(2, 2, true); + var oxoniumIonIntensities = new double[Glycan.AllOxoniumIons.Length]; + var localizationGraphs = new List(); + + // Access the private CreateGsm method via reflection. + var createGsmMethod = typeof(GlycoSearchEngine).GetMethod("CreateGsm", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.That(createGsmMethod, Is.Not.Null, "Unable to find private CreateGsm method via reflection."); + + // Compute expected p and verify it exceeds 1 so capping behavior is exercised. + var scan = ms2ScanWithSpecificMass.First(); + double spectrumSize = scan.TheScan.MassSpectrum.Size; + double rangeWidth = scan.TheScan.MassSpectrum.Range.Width; + double tolWidth = commonParameters.ProductMassTolerance.GetRange(1000).Width; + double pEstimate = rangeWidth > 0 ? spectrumSize * tolWidth / rangeWidth : double.PositiveInfinity; + var result = (GlycoSpectralMatch)createGsmMethod.Invoke(engine, new object[] { ms2ScanWithSpecificMass.First(), 0, 0, peptide, route, oxoniumIonIntensities, localizationGraphs }); + + // Compute expected p and verify it exceeds 1 so capping behavior is exercised. + Assert.That(pEstimate, Is.GreaterThan(1d), "Test must drive p > 1 to validate the cap."); + Assert.That(result.ScanInfo_p, Is.EqualTo(1), "P value should be capped at 1 when product mass tolerance is very wide."); + } + } +} From 055d2d3a79d7da4dd63e8c2f5d0fa760dc2a169a Mon Sep 17 00:00:00 2001 From: Nic Bollis Date: Fri, 27 Mar 2026 14:59:02 -0500 Subject: [PATCH 02/12] mzLib 1.0.576 (#2633) --- .gitignore | 1 + MetaMorpheus/CMD/CMD.csproj | 2 +- MetaMorpheus/EngineLayer/EngineLayer.csproj | 2 +- MetaMorpheus/GUI/GUI.csproj | 2 +- MetaMorpheus/GuiFunctions/GuiFunctions.csproj | 2 +- MetaMorpheus/MetaMorpheusSetup/Product.wxs | 3 +++ MetaMorpheus/TaskLayer/TaskLayer.csproj | 2 +- MetaMorpheus/Test/Test.csproj | 2 +- MetaMorpheus/Test/Transcriptomics/RnaDatabaseLoadingTests.cs | 1 + 9 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 9f446d4ced..85ca205c2c 100644 --- a/.gitignore +++ b/.gitignore @@ -235,3 +235,4 @@ _Pvt_Extensions # Macintosh files **/.DS_Store /AGENTS.md +/.serena diff --git a/MetaMorpheus/CMD/CMD.csproj b/MetaMorpheus/CMD/CMD.csproj index 13fa31edc9..1b4c53a686 100644 --- a/MetaMorpheus/CMD/CMD.csproj +++ b/MetaMorpheus/CMD/CMD.csproj @@ -24,7 +24,7 @@ - + diff --git a/MetaMorpheus/EngineLayer/EngineLayer.csproj b/MetaMorpheus/EngineLayer/EngineLayer.csproj index d2f7beb085..0cc8ac4775 100644 --- a/MetaMorpheus/EngineLayer/EngineLayer.csproj +++ b/MetaMorpheus/EngineLayer/EngineLayer.csproj @@ -29,7 +29,7 @@ - + diff --git a/MetaMorpheus/GUI/GUI.csproj b/MetaMorpheus/GUI/GUI.csproj index a264ec25cb..9449ab0152 100644 --- a/MetaMorpheus/GUI/GUI.csproj +++ b/MetaMorpheus/GUI/GUI.csproj @@ -63,7 +63,7 @@ - + diff --git a/MetaMorpheus/GuiFunctions/GuiFunctions.csproj b/MetaMorpheus/GuiFunctions/GuiFunctions.csproj index 8b95147e5d..df20fc330b 100644 --- a/MetaMorpheus/GuiFunctions/GuiFunctions.csproj +++ b/MetaMorpheus/GuiFunctions/GuiFunctions.csproj @@ -16,7 +16,7 @@ - + diff --git a/MetaMorpheus/MetaMorpheusSetup/Product.wxs b/MetaMorpheus/MetaMorpheusSetup/Product.wxs index e5a3946624..90ca23627f 100644 --- a/MetaMorpheus/MetaMorpheusSetup/Product.wxs +++ b/MetaMorpheus/MetaMorpheusSetup/Product.wxs @@ -202,6 +202,9 @@ + + + diff --git a/MetaMorpheus/TaskLayer/TaskLayer.csproj b/MetaMorpheus/TaskLayer/TaskLayer.csproj index 013ae6bcfc..27034a6bb1 100644 --- a/MetaMorpheus/TaskLayer/TaskLayer.csproj +++ b/MetaMorpheus/TaskLayer/TaskLayer.csproj @@ -21,7 +21,7 @@ - + diff --git a/MetaMorpheus/Test/Test.csproj b/MetaMorpheus/Test/Test.csproj index 3cdbb7bab6..7ca29075c6 100644 --- a/MetaMorpheus/Test/Test.csproj +++ b/MetaMorpheus/Test/Test.csproj @@ -24,7 +24,7 @@ - + diff --git a/MetaMorpheus/Test/Transcriptomics/RnaDatabaseLoadingTests.cs b/MetaMorpheus/Test/Transcriptomics/RnaDatabaseLoadingTests.cs index 67e6c8eabc..01888888c7 100644 --- a/MetaMorpheus/Test/Transcriptomics/RnaDatabaseLoadingTests.cs +++ b/MetaMorpheus/Test/Transcriptomics/RnaDatabaseLoadingTests.cs @@ -196,6 +196,7 @@ public TestBioPolymer(string baseSequence = "GUACUGCCUCUAGUGAAGCA", string baseS public string DatabaseFilePath { get; } public bool IsDecoy { get; } public bool IsContaminant { get; } + public bool IsEntrapment { get; } = false; public string Organism { get; } public string Accession { get; set; } public List> GeneNames { get; } From 10b60613ab1ba2a8f2b9a0d86bbeff828e87c908 Mon Sep 17 00:00:00 2001 From: Nic Bollis Date: Tue, 31 Mar 2026 15:44:30 -0500 Subject: [PATCH 03/12] Fragmentation Parameters and M-Ion Losses (#2627) * Added fragment params to common params * Created UI control and view model for fragmentation parameters * Updated Mzlib * Update to fix toml read/write * Update Mod parsing * Fixed how ambiguity handles refragmentation * mzlib update * fixed mion loss annotations * MetaDraw: DeconExploration Range Update * MetaDraw: DeconExploration Range Update * Custom M Ions: UI * tempMzLib * Obo update * Updated obo and PtmListLoader * Added M-Ion stuff in to metadraw * mzlib * Update test * updated mzlib * test changes * Split M Ions * MetaDraw: PlotModel Stat splitting by grouping property. * Agent: GItignore * Secondary axis * grouping better * fixed redundant addition * fix bad test * test * MetaDrawSettingsTests * Reduce grouping options * remove unnecessary change * plot model stat tests * decon testing * decon plot testing * Expand test coverage * feat(FragmentationParams): add chemical formula column to M-Ion loss table - Add ChemicalFormula property to MIonLossViewModel to expose formula string - Add Chemical Formula column to the M-Ion losses DataGrid - Rearrange FragmentationParamsControl layout: - Left column: Max Fragment Mass only - Right column: All checkboxes (Complementary, N/C Terminal, Internal, M-Ions) - Bottom section: GroupBox with evenly spaced buttons, DataGrid, and notes * fix(FragmentationReanalysisViewModel): correct constructor parameter logic Swap the parameter initialization blocks so that: - When isProtein=true, use DigestionParams and SearchParameters (protein) - When isProtein=false, use RnaDigestionParams and RnaSearchParameters (RNA) Previously these were inverted, causing wrong fragmentation parameters to be loaded based on the analyte type. * fix(FragmentationReanalysis): include NeutralLoss in ion equality comparer Add NeutralLoss to both Equals() and GetHashCode() methods so that ions with different neutral losses (e.g., b5 with -80 Da loss vs b5 without loss) are treated as distinct. This preserves neutral loss fragment ions from the original PSM during refragmentation. * fix(FragmentationParamsControl): correct N/C-terminal checkbox bindings Swap the XAML bindings so that: - N-Terminal Ions checkbox binds to LeftSideFragmentIons (not RightSide) - C-Terminal Ions checkbox binds to RightSideFragmentIons (not LeftSide) This fixes the crossed bindings issue where selecting 'N-Terminal Ions' was silently enabling C-terminal ion search and vice versa. * fix(FragmentationParamsViewModel): preserve terminus flags in ReloadMIonLosses - Capture current LeftSideFragmentIons/RightSideFragmentIons state and reconstruct the proper FragmentationTerminus to pass to DigestionParams - Remove redundant selection restoration loop - LoadAvailableMIonLosses already restores selections from FragmentationParameters.MIonLosses - This prevents silently resetting N/C-terminal ion flags to defaults when user reloads custom M-Ion losses * fix(FragmentationReanalysisViewModel): add thread safety for static dictionary mutation Add a static lock object and wrap the DissociationTypeCollection dictionary writes and subsequent Fragment() calls to ensure thread safety when multiple refragmentation operations run concurrently. This prevents race conditions where concurrent calls could corrupt the shared static dictionary state. * fix(FragmentationParamsViewModel): branch on IsRnaMode for ReloadMIonLosses CommonParameters constructor checks 'if (digestionParams is RnaDigestionParams)' to select RNA vs protein defaults. Passing a protein DigestionParams in RNA mode causes it to override the passed fragmentationParams with new FragmentationParams(). - Branch on IsRnaMode when constructing digestion params - Add test ReloadMIonLosses_InRnaMode_PreservesRnaFragmentationParams to verify that ToFragmentationParams() returns RnaFragmentationParams after reload * fix(locking products): custom products are now locked properly * fix(mode change popup) --- .gitignore | 1 + .../ClassicSearch/ClassicSearchEngine.cs | 4 +- MetaMorpheus/EngineLayer/CommonParameters.cs | 15 +- MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs | 2 +- .../EngineLayer/Indexing/IndexingEngine.cs | 2 +- .../Localization/LocalizationEngine.cs | 2 +- .../ModernSearch/ModernSearchEngine.cs | 2 +- MetaMorpheus/EngineLayer/Mods/RnaMods.txt | 143 ++- .../NonSpecificEnzymeSearchEngine.cs | 4 +- .../SpectralLibrarySearchFunction.cs | 2 +- MetaMorpheus/GUI/App.xaml | 19 +- MetaMorpheus/GUI/GUI_h05aw1sq_wpftmp.csproj | 448 ++++++++++ MetaMorpheus/GUI/MainWindow.xaml | 14 - MetaMorpheus/GUI/MetaDraw/MetaDraw.xaml | 104 ++- MetaMorpheus/GUI/MetaDraw/MetaDraw.xaml.cs | 45 +- .../MetaDraw/SettingsButtonControl.xaml.cs | 6 +- .../GUI/TaskWindows/SearchTaskWindow.xaml | 164 +--- .../GUI/TaskWindows/SearchTaskWindow.xaml.cs | 119 ++- .../Converters/BoolToFontWeightConverter.cs | 22 + .../Converters/CollapseOnRnaModeConverter.cs | 15 + .../GUI/Views/CustomMIonLossWindow.xaml | 144 +++ .../GUI/Views/CustomMIonLossWindow.xaml.cs | 126 +++ .../GUI/Views/FragmentReanalysisControl.xaml | 158 +++- .../GUI/Views/FragmentationParamsControl.xaml | 259 ++++++ .../Views/FragmentationParamsControl.xaml.cs | 39 + .../GuiFunctions/GuiGlobalParamsViewModel.cs | 5 +- .../DeconExplorationTabViewModel.cs | 4 +- .../DeconExploration/DeconvolutionPlot.cs | 5 +- .../FragmentationReanalysisViewModel.cs | 92 +- .../MetaDraw/MetaDrawDataLoader.cs | 36 +- .../GuiFunctions/MetaDraw/MetaDrawSettings.cs | 27 +- .../MetaDraw/MetaDrawSettingsSnapshot.cs | 5 + .../GuiFunctions/MetaDraw/PlotModelStat.cs | 759 ++++++++++++++-- .../MetaDraw/PlotModelStatParameters.cs | 68 ++ .../PlotModelStatParametersViewModel.cs | 124 +++ .../SpectrumMatch/SpectrumMatchPlot.cs | 29 +- .../GuiFunctions/Models/CustomMIonLoss.cs | 102 +++ .../Util/CustomMIonLossManager.cs | 180 ++++ .../FragmentationParamsViewModel.cs | 330 +++++++ .../ViewModels/MetaDrawSettingsViewModel.cs | 32 + MetaMorpheus/TaskLayer/MetaMorpheusTask.cs | 25 +- .../Test/GuiTests/CustomMIonLossTests.cs | 544 ++++++++++++ .../Test/GuiTests/FragmentParamsVM.cs | 505 +++++++++++ .../DeconExplorationTabViewModelTests.cs | 81 +- .../Test/MetaDraw/FragmentReanalysis.cs | 8 +- .../FragmentReanalysisRaceConditionTest.cs | 97 ++ .../Test/MetaDraw/MetaDrawDataLoaderTests.cs | 95 ++ .../MetaDraw/MetaDrawSettingsAndViewsTest.cs | 48 + MetaMorpheus/Test/MetaDraw/MetaDrawTest.cs | 398 ++++++++- .../MetaDraw/PlotModelStatParametersTests.cs | 827 ++++++++++++++++++ .../Test/Transcriptomics/GptmdTests.cs | 2 +- 51 files changed, 5835 insertions(+), 452 deletions(-) create mode 100644 MetaMorpheus/GUI/GUI_h05aw1sq_wpftmp.csproj create mode 100644 MetaMorpheus/GUI/Util/Converters/BoolToFontWeightConverter.cs create mode 100644 MetaMorpheus/GUI/Views/CustomMIonLossWindow.xaml create mode 100644 MetaMorpheus/GUI/Views/CustomMIonLossWindow.xaml.cs create mode 100644 MetaMorpheus/GUI/Views/FragmentationParamsControl.xaml create mode 100644 MetaMorpheus/GUI/Views/FragmentationParamsControl.xaml.cs create mode 100644 MetaMorpheus/GuiFunctions/MetaDraw/PlotModelStatParameters.cs create mode 100644 MetaMorpheus/GuiFunctions/MetaDraw/PlotModelStatParametersViewModel.cs create mode 100644 MetaMorpheus/GuiFunctions/Models/CustomMIonLoss.cs create mode 100644 MetaMorpheus/GuiFunctions/Util/CustomMIonLossManager.cs create mode 100644 MetaMorpheus/GuiFunctions/ViewModels/FragmentationParamsViewModel.cs create mode 100644 MetaMorpheus/Test/GuiTests/CustomMIonLossTests.cs create mode 100644 MetaMorpheus/Test/GuiTests/FragmentParamsVM.cs create mode 100644 MetaMorpheus/Test/MetaDraw/FragmentReanalysisRaceConditionTest.cs create mode 100644 MetaMorpheus/Test/MetaDraw/PlotModelStatParametersTests.cs diff --git a/.gitignore b/.gitignore index 85ca205c2c..0a3cf251d0 100644 --- a/.gitignore +++ b/.gitignore @@ -234,5 +234,6 @@ _Pvt_Extensions # Macintosh files **/.DS_Store +/.opencode/ /AGENTS.md /.serena diff --git a/MetaMorpheus/EngineLayer/ClassicSearch/ClassicSearchEngine.cs b/MetaMorpheus/EngineLayer/ClassicSearch/ClassicSearchEngine.cs index a275e1e678..4c1c0af479 100644 --- a/MetaMorpheus/EngineLayer/ClassicSearch/ClassicSearchEngine.cs +++ b/MetaMorpheus/EngineLayer/ClassicSearch/ClassicSearchEngine.cs @@ -158,7 +158,7 @@ protected override MetaMorpheusEngineResults RunSpecific() // check if we've already generated theoretical fragments for this peptide+dissociation type if (peptideTheorProducts.Count == 0) { - specificBioPolymer.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts); + specificBioPolymer.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts, CommonParameters.FragmentationParameters); } // match theoretical target ions to spectrum @@ -205,7 +205,7 @@ private void DecoyScoreForSpectralLibrarySearch(ScanWithIndexAndNotchInfo scan, if (decoyTheoreticalFragments.Count == 0) { - reversedOnTheFlyDecoy.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, decoyTheoreticalFragments); + reversedOnTheFlyDecoy.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, decoyTheoreticalFragments, CommonParameters.FragmentationParameters); } Ms2ScanWithSpecificMass theScan = ArrayOfSortedMS2Scans[scan.ScanIndex]; var decoyMatchedIons = MatchFragmentIons(theScan, decoyTheoreticalFragments, CommonParameters, diff --git a/MetaMorpheus/EngineLayer/CommonParameters.cs b/MetaMorpheus/EngineLayer/CommonParameters.cs index f745ce5f48..dbc22a309f 100644 --- a/MetaMorpheus/EngineLayer/CommonParameters.cs +++ b/MetaMorpheus/EngineLayer/CommonParameters.cs @@ -7,9 +7,9 @@ using System.Reflection; using Nett; using Omics.Digestion; -using Omics.Fragmentation.Peptide; using Transcriptomics.Digestion; using EngineLayer.DIA; +using Transcriptomics; namespace EngineLayer { @@ -60,7 +60,8 @@ public CommonParameters( DeconvolutionParameters precursorDeconParams = null, DeconvolutionParameters productDeconParams = null, bool useMostAbundantPrecursorIntensity = true, - DIAparameters diaParameters = null) + DIAparameters diaParameters = null, + IFragmentationParams fragmentationParams = null) { TaskDescriptor = taskDescriptor; @@ -119,11 +120,13 @@ public CommonParameters( ListOfModsFixed = listOfModsFixed ?? new List<(string, string)>(); PrecursorDeconvolutionParameters.AverageResidueModel = new OxyriboAveragine(); ProductDeconvolutionParameters.AverageResidueModel = new OxyriboAveragine(); + FragmentationParameters = fragmentationParams ?? RnaFragmentationParams.Default; } else { ListOfModsVariable = listOfModsVariable ?? new List<(string, string)> { ("Common Variable", "Oxidation on M") }; ListOfModsFixed = listOfModsFixed ?? new List<(string, string)> { ("Common Fixed", "Carbamidomethyl on C"), ("Common Fixed", "Carbamidomethyl on U") }; + FragmentationParameters = fragmentationParams ?? new FragmentationParams(); } CustomIons = digestionParams.ProductsFromDissociationType()[DissociationType.Custom]; @@ -158,7 +161,7 @@ public int DeconvolutionMaxAssumedChargeState public int TotalPartitions { get; set; } public Tolerance ProductMassTolerance { get; set; } // public setter required for calibration task public Tolerance PrecursorMassTolerance { get; set; } // public setter required for calibration task - public bool AddCompIons { get; private set; } + public bool AddCompIons { get; set; } /// /// Only peptides/PSMs with Q-Value and Q-Value Notch below this threshold are used for quantification and /// spectral library generation. If SearchParameters.WriteHighQValuePsms is set to false, only @@ -199,6 +202,7 @@ public int DeconvolutionMaxAssumedChargeState public bool UseMostAbundantPrecursorIntensity { get; set; } public DIAparameters? DIAparameters { get; set; } //only for DIA analysis involving pseudo ms2 scan generation + public IFragmentationParams FragmentationParameters { get; set; } public CommonParameters Clone() { @@ -267,7 +271,10 @@ public CommonParameters CloneWithNewTerminus(FragmentationTerminus? terminus = n MinVariantDepth, AddTruncations, PrecursorDeconvolutionParameters, - ProductDeconvolutionParameters); + ProductDeconvolutionParameters, + UseMostAbundantPrecursorIntensity, + DIAparameters, + FragmentationParameters); } public void SetCustomProductTypes() diff --git a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs index 99c1c4a423..ded6bade32 100644 --- a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs +++ b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs @@ -136,7 +136,7 @@ protected override MetaMorpheusEngineResults RunSpecific() var newPep = pepWithSetMods.Localize(pepSeqIndex, mod.MonoisotopicMass.Value); peptideTheorProducts.Clear(); - newPep.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts); + newPep.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts, CommonParameters.FragmentationParameters); ms2ScanWithSpecificMass ??= new Ms2ScanWithSpecificMass(scan, precursorMass, precursorCharge, fileName, CommonParameters); var matchedIons = MatchFragmentIons(ms2ScanWithSpecificMass, peptideTheorProducts, CommonParameters, matchAllCharges: false); diff --git a/MetaMorpheus/EngineLayer/Indexing/IndexingEngine.cs b/MetaMorpheus/EngineLayer/Indexing/IndexingEngine.cs index ec51eda000..daf416cb0b 100644 --- a/MetaMorpheus/EngineLayer/Indexing/IndexingEngine.cs +++ b/MetaMorpheus/EngineLayer/Indexing/IndexingEngine.cs @@ -160,7 +160,7 @@ protected override MetaMorpheusEngineResults RunSpecific() for (int peptideId = 0; peptideId < peptides.Count; peptideId++) { - peptides[peptideId].Fragment(CommonParameters.DissociationType, CommonParameters.DigestionParams.FragmentationTerminus, fragments); + peptides[peptideId].Fragment(CommonParameters.DissociationType, CommonParameters.DigestionParams.FragmentationTerminus, fragments, CommonParameters.FragmentationParameters); foreach (var theoreticalFragment in fragments) { diff --git a/MetaMorpheus/EngineLayer/Localization/LocalizationEngine.cs b/MetaMorpheus/EngineLayer/Localization/LocalizationEngine.cs index da2f989a44..9d25ba2eea 100644 --- a/MetaMorpheus/EngineLayer/Localization/LocalizationEngine.cs +++ b/MetaMorpheus/EngineLayer/Localization/LocalizationEngine.cs @@ -65,7 +65,7 @@ protected override MetaMorpheusEngineResults RunSpecific() var peptideWithLocalizedMassDiff = peptide.Localize(r, massDifference); // this is the list of theoretical products for this peptide with mass-difference on this residue - peptideWithLocalizedMassDiff.Fragment(CommonParameters.DissociationType, CommonParameters.DigestionParams.FragmentationTerminus, productsWithLocalizedMassDiff); + peptideWithLocalizedMassDiff.Fragment(CommonParameters.DissociationType, CommonParameters.DigestionParams.FragmentationTerminus, productsWithLocalizedMassDiff, CommonParameters.FragmentationParameters); var matchedIons = MatchFragmentIons(scanWithSpecificMass, productsWithLocalizedMassDiff, CommonParameters); diff --git a/MetaMorpheus/EngineLayer/ModernSearch/ModernSearchEngine.cs b/MetaMorpheus/EngineLayer/ModernSearch/ModernSearchEngine.cs index 3d1660c149..5292a56c30 100644 --- a/MetaMorpheus/EngineLayer/ModernSearch/ModernSearchEngine.cs +++ b/MetaMorpheus/EngineLayer/ModernSearch/ModernSearchEngine.cs @@ -340,7 +340,7 @@ protected SpectralMatch FineScorePeptide(int id, Ms2ScanWithSpecificMass scan, i { PeptideWithSetModifications peptide = PeptideIndex[id]; - peptide.Fragment(CommonParameters.DissociationType, FragmentationTerminus.Both, peptideTheorProducts); + peptide.Fragment(CommonParameters.DissociationType, FragmentationTerminus.Both, peptideTheorProducts, CommonParameters.FragmentationParameters); List matchedIons = MatchFragmentIons(scan, peptideTheorProducts, CommonParameters); diff --git a/MetaMorpheus/EngineLayer/Mods/RnaMods.txt b/MetaMorpheus/EngineLayer/Mods/RnaMods.txt index 03c1289752..3fe4e6c080 100644 --- a/MetaMorpheus/EngineLayer/Mods/RnaMods.txt +++ b/MetaMorpheus/EngineLayer/Mods/RnaMods.txt @@ -1,4 +1,56 @@ -################################## METALS + +--------------------------------------------------------------------------- + UniProt Knowledgebase: + Swiss-Prot Protein Knowledgebase + TrEMBL Protein Database + SIB Swiss Institute of Bioinformatics; Geneva, Switzerland + European Bioinformatics Institute (EBI); Hinxton, United Kingdom + Protein Information Resource (PIR); Washington DC, USA +--------------------------------------------------------------------------- + +Description: Controlled vocabulary of posttranslational modifications (PTM) +Name: ptmlist.txt +Release: 2018_01 of 31-Jan-2018 + +--------------------------------------------------------------------------- + + This document lists the posttranslational modifications used in the + UniProt knowledgebase (Swiss-Prot and TrEMBL). + + The definition of the posttranslational modifications usage as well as + other information is provided in the following format: + + --------- --------------------------- ------------------------------ + Line code Content Occurrence in an entry + --------- --------------------------- ------------------------------ + ID Identifier (FT description) Once; starts a PTM entry + AC Accession (PTM-xxxx) Once + FT Feature key Once + TG Target Once; two targets separated by + a dash in case of intrachain + crosslinks + PA Position of the modification Optional; once + on the amino acid + PP Position of the modification Optional; once + in the polypeptide + CF Correction formula Optional; once + MM Monoisotopic mass difference Optional; once + MA Average mass difference Optional; once + LC Cellular location Optional; once; alternatives + can be proposed + TR Taxonomic range Optional; once or more + KW Keyword Optional; once or more + DR Cross-reference to PTM Optional; once or more + databases + BM Backbone modification Optional; once; alternatives can be + proposed; if present, the + affected fragment types are + listed after the modification + // Terminator Once; ends an entry + + +___________________________________________________________________________ +################################## METALS ID Sodium TG A or C or G or U PP Anywhere. @@ -14,40 +66,77 @@ CF H-1 K1 DR Unimod; 530. // ################################## Biological -ID Methylation -TG A or C or G or U or T or Y +ID 2'-O-Methyladenosine +TG A +PP Anywhere. +MT Biological +CF C1 H2 +BL Suppressed +DR Unimod; 34. +// +ID N6-methyladenosine +TG A +PP Anywhere. +MT Biological +CF C1 H2 +BL Modified +DR Unimod; 34. +// +ID 2'-O-Methyluridine +TG U +PP Anywhere. +MT Biological +CF C1 H2 +BL Suppressed +DR Unimod; 34. +// +ID 5-Methylcytidine +TG C PP Anywhere. MT Biological CF C1 H2 +BL Modified:C1H2 DR Unimod; 34. // +ID N6,2'-O-dimethyladenosine +TG A +PP Anywhere. +MT Biological +CF C2 H4 +BL Suppressed:C1H2 +DR Unimod; 36. +// +################################## Simple Biological +ID Methylation +TG A or C or G or U or T or Y +PP Anywhere. +MT Simple Biological +CF C1 H2 +// ID Dimethylation TG A or C or G or U or T or Y PP Anywhere. -MT Biological +MT Simple Biological CF C2 H4 -DR Unimod; 34. // ID MethoxyEthoxylation TG A or C or G or U PP Anywhere. -MT Biological +MT Simple Biological CF C3 H6 O1 -DR Unimod; 34. // +################################## Base Conversion ID Deoxylnosine TG T PP Anywhere. -MT Biological +MT Base Conversion CF N-1H-1 -DR Unimod; 34. // ID Deoxylnosine TG G PP Anywhere. -MT Biological +MT Base Conversion CF N-1H-1O-1 -DR Unimod; 34. // ################################## Common Artificial ID DeoxyFluoronation @@ -55,7 +144,6 @@ TG A or C or G or U PP Anywhere. MT Common Artificial CF O-1 H-1 F1 -DR Unimod; 34. // ################################## Terminal Shifts ID Cyclic Phosphate @@ -63,34 +151,55 @@ TG X PP Oligo 3'-terminal. MT Digestion Termini CF H-2 O-1 -DR Unimod; 280. // ID Terminal Phosphorylation TG X PP Oligo 5'-terminal. MT Digestion Termini CF H1 O3 P1 -DR Unimod; 280. // ID Terminal Dephosphorylation TG X PP Oligo 5'-terminal. MT Digestion Termini CF P-1 O-3 H-1 -DR Unimod; 280. // ID Pfizer 5'-Cap TG X PP 5'-terminal. MT Standard CF C13H22N5O14P3 -DR Unimod; 280. // ################################## Backbone Shift ID Phosphorothioate TG X +FT Backbone PP Anywhere. MT Common Variable CF SO-1 -DR Unimod; 280. +BM w,x,c,d +// +ID Boranophosphate +TG X +PP Anywhere. +FT Backbone +MT Therapeutic +CF B1 H3 O-1 +BM w,x,c,d +// +ID N3'-Phosphoramidate +TG X +PP Anywhere. +FT Backbone +MT Therapeutic +CF N1 H1 O-1 # Replace 3'-O with NH +BM w,b,c,d +// +ID Methylphosphonate +TG X +PP Anywhere. +FT Backbone +MT Therapeutic +CF C1 H2 O-1 +BM w,x,c,d // \ No newline at end of file diff --git a/MetaMorpheus/EngineLayer/NonSpecificEnzymeSearch/NonSpecificEnzymeSearchEngine.cs b/MetaMorpheus/EngineLayer/NonSpecificEnzymeSearch/NonSpecificEnzymeSearchEngine.cs index 85b530b504..ed363e2ab8 100644 --- a/MetaMorpheus/EngineLayer/NonSpecificEnzymeSearch/NonSpecificEnzymeSearchEngine.cs +++ b/MetaMorpheus/EngineLayer/NonSpecificEnzymeSearch/NonSpecificEnzymeSearchEngine.cs @@ -124,13 +124,13 @@ protected override MetaMorpheusEngineResults RunSpecific() foreach (int id in idsOfPeptidesPossiblyObserved.Where(id => scoringTable[id] == maxInitialScore)) { PeptideWithSetModifications peptide = PeptideIndex[id]; - peptide.Fragment(CommonParameters.DissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts); + peptide.Fragment(CommonParameters.DissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts, CommonParameters.FragmentationParameters); Tuple notchAndUpdatedPeptide = Accepts(peptideTheorProducts, scan.PrecursorMass, peptide, CommonParameters.DigestionParams.FragmentationTerminus, MassDiffAcceptor, semiSpecificSearch); int notch = notchAndUpdatedPeptide.Item1; if (notch >= 0) { peptide = notchAndUpdatedPeptide.Item2; - peptide.Fragment(CommonParameters.DissociationType, FragmentationTerminus.Both, peptideTheorProducts); + peptide.Fragment(CommonParameters.DissociationType, FragmentationTerminus.Both, peptideTheorProducts, CommonParameters.FragmentationParameters); List matchedIons = MatchFragmentIons(scan, peptideTheorProducts, ModifiedParametersNoComp); double thisScore = CalculatePeptideScore(scan.TheScan, matchedIons); diff --git a/MetaMorpheus/EngineLayer/SpectralLibrarySearch/SpectralLibrarySearchFunction.cs b/MetaMorpheus/EngineLayer/SpectralLibrarySearch/SpectralLibrarySearchFunction.cs index c32832c180..a8630ae29b 100644 --- a/MetaMorpheus/EngineLayer/SpectralLibrarySearch/SpectralLibrarySearchFunction.cs +++ b/MetaMorpheus/EngineLayer/SpectralLibrarySearch/SpectralLibrarySearchFunction.cs @@ -60,7 +60,7 @@ public static void CalculateSpectralAngles(SpectralLibrary spectralLibrary, Spec else if (bestMatch.IsDecoy && spectralLibrary.TryGetSpectrum(bestMatch.SpecificBioPolymer.Description, scan.PrecursorCharge, out var targetlibrarySpectrum)) { var decoyPeptideTheorProducts = new List(); - bestMatch.SpecificBioPolymer.Fragment(commonParameters.DissociationType, commonParameters.DigestionParams.FragmentationTerminus, decoyPeptideTheorProducts); + bestMatch.SpecificBioPolymer.Fragment(commonParameters.DissociationType, commonParameters.DigestionParams.FragmentationTerminus, decoyPeptideTheorProducts, commonParameters.FragmentationParameters); var decoylibrarySpectrum = LibrarySpectrum.GetDecoyLibrarySpectrumFromTargetByReverse(targetlibrarySpectrum, decoyPeptideTheorProducts); SpectralSimilarity s = new SpectralSimilarity(scan.TheScan.MassSpectrum, decoylibrarySpectrum.Select(x => x.Mz).ToArray(), decoylibrarySpectrum.Select(x => x.Intensity).ToArray(), SpectralSimilarity.SpectrumNormalizationScheme.SquareRootSpectrumSum, diff --git a/MetaMorpheus/GUI/App.xaml b/MetaMorpheus/GUI/App.xaml index 3b560cfcf2..31a78fda89 100644 --- a/MetaMorpheus/GUI/App.xaml +++ b/MetaMorpheus/GUI/App.xaml @@ -2,6 +2,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:MetaMorpheusGUI" + xmlns:guiFunctions="clr-namespace:GuiFunctions;assembly=GuiFunctions" StartupUri="MainWindow.xaml"> @@ -105,7 +106,21 @@ - + + + + @@ -113,5 +128,7 @@ + + \ No newline at end of file diff --git a/MetaMorpheus/GUI/GUI_h05aw1sq_wpftmp.csproj b/MetaMorpheus/GUI/GUI_h05aw1sq_wpftmp.csproj new file mode 100644 index 0000000000..4f03a4b587 --- /dev/null +++ b/MetaMorpheus/GUI/GUI_h05aw1sq_wpftmp.csproj @@ -0,0 +1,448 @@ + + + MetaMorpheusGUI + obj\Debug\ + obj\ + C:\Users\Nic\source\repos\MetaMorpheus\MetaMorpheus\GUI\obj\ + <_TargetAssemblyProjectName>GUI + + + + WinExe + net8.0-windows + true + false + false + false + false + false + false + MetaMorpheusGUI + MetaMorpheusGUI + Debug;Release + full + true + Icons\MMnice.ico + true + + + true + false + false + false + + + x64 + + + x64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MetaMorpheus/GUI/MainWindow.xaml b/MetaMorpheus/GUI/MainWindow.xaml index f0848612ad..ebab7d7fab 100644 --- a/MetaMorpheus/GUI/MainWindow.xaml +++ b/MetaMorpheus/GUI/MainWindow.xaml @@ -154,20 +154,6 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + * Required fields. Select at least one applicable mode. + + + + + - \ No newline at end of file + diff --git a/MetaMorpheus/GUI/TaskWindows/CalibrateTaskWindow.xaml.cs b/MetaMorpheus/GUI/TaskWindows/CalibrateTaskWindow.xaml.cs index 2906a3b917..5a06d67ced 100644 --- a/MetaMorpheus/GUI/TaskWindows/CalibrateTaskWindow.xaml.cs +++ b/MetaMorpheus/GUI/TaskWindows/CalibrateTaskWindow.xaml.cs @@ -1,4 +1,4 @@ -using EngineLayer; +using EngineLayer; using GuiFunctions; using MassSpectrometry; using MzLibUtil; @@ -99,8 +99,29 @@ private void UpdateFieldsFromTask(CalibrationTask task) PrecursorMassToleranceComboBox.SelectedIndex = task.CommonParameters.PrecursorMassTolerance is AbsoluteTolerance ? 0 : 1; CustomFragmentationWindow = new CustomFragmentationWindow(task.CommonParameters.CustomIons); writeIndexMzmlCheckbox.IsChecked = task.CalibrationParameters.WriteIndexedMzml; - - //writeIntermediateFilesCheckBox.IsChecked = task.CalibrationParameters.WriteIntermediateFiles; + NumberOfDatabaseSearchesTextBox.Text = task.CommonParameters.TotalPartitions.ToString(CultureInfo.InvariantCulture); + + //// Set Search Type radio buttons + switch (task.CalibrationParameters.SearchType) + { + case SearchType.Classic: + ClassicSearchRadioButton.IsChecked = true; + ModernSearchRadioButton.IsChecked = false; + break; + case SearchType.Modern: + ClassicSearchRadioButton.IsChecked = false; + ModernSearchRadioButton.IsChecked = true; + break; + default: + MessageBox.Show( + $"SearchType '{task.CalibrationParameters.SearchType}' is not supported by the Calibration Task window.", + "Unsupported Search Type", + MessageBoxButton.OK, + MessageBoxImage.Warning); + break; + } + + writeIntermediateFilesCheckBox.IsChecked = task.CalibrationParameters.WriteIntermediateFiles; MinScoreAllowed.Text = task.CommonParameters.ScoreCutoff.ToString(CultureInfo.InvariantCulture); @@ -159,7 +180,7 @@ private void UpdateFieldsFromTask(CalibrationTask task) private void PopulateChoices() { - bool isRnaMode = GuiGlobalParamsViewModel.Instance.IsRnaMode; + bool isRnaMode = GuiGlobalParamsViewModel.Instance.IsRnaMode; List modsToUse = isRnaMode ? GlobalVariables.AllRnaModsKnown.ToList() : GlobalVariables.AllModsKnown.ToList(); foreach (string dissassociationType in GlobalVariables.AllSupportedDissociationTypes.Keys) @@ -236,7 +257,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) if (!TaskValidator.CheckTaskSettingsValidity(PrecursorMassToleranceTextBox.Text, ProductMassToleranceTextBox.Text, MissedCleavagesTextBox.Text, MaxModificationIsoformsTextBox.Text, MinPeptideLengthTextBox.Text, MaxPeptideLengthTextBox.Text, MaxThreadsTextBox.Text, MinScoreAllowed.Text, - fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, null, null, fieldNotUsed, MaxModsPerPeptideTextBox.Text, fieldNotUsed, + fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, null, null, fieldNotUsed, MaxModsPerPeptideTextBox.Text, fieldNotUsed, null, null, null)) { return; @@ -332,11 +353,12 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) assumeOrphanPeaksAreZ1Fragments: protease.Name != "top-down" && !isRnaMode, minVariantDepth: minVariantDepth, maxHeterozygousVariants: maxHeterozygousVariants, + totalPartitions: int.Parse(NumberOfDatabaseSearchesTextBox.Text, CultureInfo.InvariantCulture), trimMsMsPeaks: false, doPrecursorDeconvolution: doPrecursorDeconvolution, precursorDeconParams: precursorDeconvolutionParameters, productDeconParams: productDeconvolutionParameters, - useProvidedPrecursorInfo: useProvidedPrecursorInfo); + useProvidedPrecursorInfo: useProvidedPrecursorInfo); TheTask.CommonParameters = commonParamsToSave; } else //bottom-up @@ -354,6 +376,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) assumeOrphanPeaksAreZ1Fragments: protease.Name != "top-down", minVariantDepth: minVariantDepth, maxHeterozygousVariants: maxHeterozygousVariants, + totalPartitions: int.Parse(NumberOfDatabaseSearchesTextBox.Text, CultureInfo.InvariantCulture), useProvidedPrecursorInfo: useProvidedPrecursorInfo, doPrecursorDeconvolution: doPrecursorDeconvolution, precursorDeconParams: precursorDeconvolutionParameters, @@ -362,6 +385,24 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) } TheTask.CalibrationParameters.WriteIndexedMzml = writeIndexMzmlCheckbox.IsChecked.Value; + TheTask.CalibrationParameters.WriteIntermediateFiles = writeIntermediateFilesCheckBox.IsChecked.Value; + if (ModernSearchRadioButton.IsChecked == true) + { + TheTask.CalibrationParameters.SearchType = SearchType.Modern; + } + else if (ClassicSearchRadioButton.IsChecked == true) + { + TheTask.CalibrationParameters.SearchType = SearchType.Classic; + } + else + { + MessageBox.Show( + "No search type is selected. Please select Classic or Modern search.", + "No Search Type Selected", + MessageBoxButton.OK, + MessageBoxImage.Warning); + return; + } DialogResult = true; } diff --git a/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationParameters.cs b/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationParameters.cs index 7029b07da6..87aa591424 100644 --- a/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationParameters.cs +++ b/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationParameters.cs @@ -10,10 +10,12 @@ public CalibrationParameters() NumFragmentsNeededForEveryIdentification = 10; QValueCutoffForCalibratingPSMs = 0.01; WriteIndexedMzml = true; + SearchType = SearchType.Classic; } public bool WriteIntermediateFiles { get; set; } public bool WriteIndexedMzml { get; set; } + public SearchType SearchType { get; set; } public int MinMS1IsotopicPeaksNeededForConfirmedIdentification { get; set; } public int MinMS2IsotopicPeaksNeededForConfirmedIdentification { get; set; } @@ -21,4 +23,4 @@ public CalibrationParameters() public double QValueCutoffForCalibratingPSMs { get; set; } } -} \ No newline at end of file +} diff --git a/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationTask.cs b/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationTask.cs index aad3a64d6d..c8cd3de261 100644 --- a/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationTask.cs +++ b/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationTask.cs @@ -2,12 +2,15 @@ using EngineLayer.Calibration; using EngineLayer.ClassicSearch; using EngineLayer.FdrAnalysis; +using EngineLayer.Indexing; +using EngineLayer.ModernSearch; using EngineLayer.Util; using MassSpectrometry; using MzLibUtil; using Nett; using Omics; using Omics.Modifications; +using Proteomics; using Proteomics.ProteolyticDigestion; using Readers; using System; @@ -30,6 +33,7 @@ public CalibrationTask() : base(MyTask.Calibrate) precursorMassTolerance: new PpmTolerance(InitialPrecursorTolerance) ); + CalibrationParameters = new CalibrationParameters(); } @@ -45,12 +49,18 @@ public CalibrationTask() : base(MyTask.Calibrate) public const string CalibSuffix = "-calib"; + private List _unsuccessfullyCalibratedFilePaths; private string _taskId; private List _proteinList; private List _variableModifications; private List _fixedModifications; private MyFileManager _myFileManager; + private List _dbFilenameList; + + // Modern Search indexing fields + private List _peptideIndex; + private List[] _fragmentIndex; protected override MyTaskResults RunSpecific(string outputFolder, List dbFilenameList, List currentRawFileList, string taskId, FileSpecificParameters[] fileSettingsList) { @@ -67,6 +77,10 @@ protected override MyTaskResults RunSpecific(string outputFolder, List { _taskId, "Individual Spectra Files", originalUncalibratedFilePath }); // carry over file-specific parameters from the uncalibrated file to the calibrated one and update combined params - FileSpecificParameters fileSpecificParams = fileSettingsList[spectraFileIndex] == null - ? new() + FileSpecificParameters fileSpecificParams = fileSettingsList[spectraFileIndex] == null + ? new() : fileSettingsList[spectraFileIndex].Clone(); CommonParameters combinedParams = SetAllFileSpecificCommonParams(CommonParameters, fileSpecificParams); - + // load the file Status("Loading spectra file...", new List { _taskId, "Individual Spectra Files" }); MsDataFile myMsDataFile = _myFileManager.LoadFile(originalUncalibratedFilePath, combinedParams).LoadAllStaticData(); @@ -101,7 +115,7 @@ protected override MyTaskResults RunSpecific(string outputFolder, List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }); + Log("Searching with searchType: " + CalibrationParameters.SearchType, new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }); Log("Searching with precursorMassTolerance: " + combinedParameters.PrecursorMassTolerance, new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }); Log("Searching with productMassTolerance: " + combinedParameters.ProductMassTolerance, new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }); - _ = new ClassicSearchEngine(allPsmsArray, listOfSortedms2Scans, _variableModifications, _fixedModifications, null, null, null, _proteinList, searchMode, combinedParameters, - FileSpecificParameters, null, new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }, false).Run(); + if (CalibrationParameters.SearchType == SearchType.Classic) + { + _ = new ClassicSearchEngine(allPsmsArray, listOfSortedms2Scans, _variableModifications, _fixedModifications, null, null, null, _proteinList, searchMode, combinedParameters, + FileSpecificParameters, null, new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }, false).Run(); + } + else // Modern Search + { + // Regenerate indexes whenever tolerances change to keep fragment index in sync with current parameters + GenerateIndexes(combinedParameters, fileNameWithoutExtension); + + _ = new ModernSearchEngine(allPsmsArray, listOfSortedms2Scans, _peptideIndex, _fragmentIndex, 0, combinedParameters, + FileSpecificParameters, searchMode, SearchParameters.DefaultMaxFragmentSize, new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }).Run(); + } List allPsms = allPsmsArray.Where(b => b != null).OrderByDescending(b => b.Score) .ThenBy(b => b.BioPolymerWithSetModsMonoisotopicMass.HasValue ? Math.Abs(b.ScanPrecursorMass - b.BioPolymerWithSetModsMonoisotopicMass.Value) : double.MaxValue) @@ -232,6 +258,7 @@ private DataPointAquisitionResults GetDataAcquisitionResults(MsDataFile myMsData return currentResult; } + /// /// Writes prose settings and initializes the following private fields used by the calibration engine: /// _taskId, _variableModifications, _fixedModifications, _proteinList, _myFileManager, _unsuccessfullyCalibratedFilePaths @@ -249,14 +276,89 @@ private void Initialize(string taskId, List dbFilenameList) var dbLoader = new DatabaseLoadingEngine(CommonParameters, this.FileSpecificParameters, [taskId], dbFilenameList, taskId, DecoyType.Reverse, true, localizeableModificationTypes); var loadingResults = dbLoader.Run() as DatabaseLoadingEngineResults; _proteinList = loadingResults!.BioPolymers; - + _myFileManager = new MyFileManager(true); _unsuccessfullyCalibratedFilePaths = new List(); + _dbFilenameList = dbFilenameList; + + // Reset indexes for Modern Search (will be generated on first use) + _peptideIndex = null; + _fragmentIndex = null; // write prose settings WriteProse(_fixedModifications, _variableModifications, _proteinList); } + /// + /// Filters a mixed biopolymer list down to entries only, returning + /// the filtered list and the number of non-protein entries that were excluded. + /// Does not throw or warn — callers are responsible for acting on the returned counts. + /// + /// The full list of biopolymers loaded from the database. + /// + /// A tuple of the filtered list and the count of excluded non-protein entries. + /// + public static (List Proteins, int ExcludedCount) FilterBiopolymersForModernSearch( + List bioPolymers) + { + var proteins = bioPolymers.OfType().ToList(); + int excludedCount = bioPolymers.Count - proteins.Count; + return (proteins, excludedCount); + } + + /// + /// Generates peptide and fragment indexes for Modern Search. + /// Delegates biopolymer filtering to . + /// Throws if no Protein entries remain after filtering. + /// Warns if non-protein biopolymers were excluded. + /// + /// The combined common parameters for the current search. + /// The spectra file name (without extension) for status reporting. + /// Thrown when the database contains no Protein entries compatible with Modern Search. + private void GenerateIndexes(CommonParameters combinedParameters, string fileNameWithoutExtension) + { + Status("Generating indexes for Modern Search...", new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }); + + var (proteinList, excludedCount) = FilterBiopolymersForModernSearch(_proteinList); + + if (proteinList.Count == 0) + { + throw new MetaMorpheusException( + "Modern Search calibration requires protein sequences, but the loaded database contains no Protein entries " + + $"({excludedCount} non-protein biopolymer(s) were filtered out). " + + "Use Classic Search calibration for non-protein analytes, or provide a protein sequence database."); + } + + if (excludedCount > 0) + { + Warn($"Modern Search calibration: {excludedCount} non-protein biopolymer(s) were excluded from the search index. " + + "Only protein sequences are supported by Modern Search."); + } + + var indexingEngine = new IndexingEngine( + proteinList, + _variableModifications, + _fixedModifications, + null, // silacLabels + null, // startLabel + null, // endLabel + 0, // currentPartition (using single partition for calibration) + DecoyType.Reverse, + combinedParameters, + FileSpecificParameters, + SearchParameters.DefaultMaxFragmentSize, + false, // generatePrecursorIndex + _dbFilenameList.Select(p => new FileInfo(p.FilePath)).ToList(), + TargetContaminantAmbiguity.RemoveContaminant, + new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }); + + var indexingResults = (IndexingResults)indexingEngine.Run(); + _peptideIndex = indexingResults.PeptideIndex; + _fragmentIndex = indexingResults.FragmentIndex; + + Status("Indexing complete.", new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }); + } + public void WriteProse(List fixedModifications, List variableModifications, List bioPolymerList) { // write prose settings @@ -319,7 +421,7 @@ private void WriteUncalibratedFile(string originalUncalibratedFilePath, string u // if we didn't calibrate, write the uncalibrated file to the output folder as an mzML File.Copy(originalUncalibratedFilePath, uncalibratedNewFullFilePath, true); // and add it to the list of all unsuccessfully calibrated files - unsuccessfullyCalibratedFilePaths.Add(uncalibratedNewFullFilePath); + unsuccessfullyCalibratedFilePaths.Add(uncalibratedNewFullFilePath); // provide a message indicating why we couldn't calibrate CalibrationWarnMessage(acquisitionResults); @@ -339,7 +441,7 @@ private bool CalibrationHasValue(DataPointAquisitionResults acquisitionResultsFi int peptidesCountFirst = acquisitionResultsFirst.Psms.Select(p => p.FullSequence).Distinct().Count(); int peptidesCountSecond = acquisitionResultsSecond.Psms.Select(p => p.FullSequence).Distinct().Count(); bool improvedCounts = psmsCountSecond > psmsCountFirst && peptidesCountSecond > peptidesCountFirst; - if(improvedCounts) + if (improvedCounts) { return true; } @@ -350,11 +452,11 @@ private bool CalibrationHasValue(DataPointAquisitionResults acquisitionResultsFi return (numPsmsIncreased && numPeptidesIncreased && psmPrecursorMedianPpmErrorDecreased && psmProductMedianPpmErrorDecreased); } - + private bool SufficientAcquisitionResults(DataPointAquisitionResults acquisitionResults) { - return acquisitionResults.Psms.Count >= NumRequiredPsms - && acquisitionResults.Ms1List.Count >= NumRequiredMs1Datapoints + return acquisitionResults.Psms.Count >= NumRequiredPsms + && acquisitionResults.Ms1List.Count >= NumRequiredMs1Datapoints && acquisitionResults.Ms2List.Count >= NumRequiredMs2Datapoints; } @@ -428,4 +530,4 @@ private static void WriteNewExperimentalDesignFile(string pathToOldExperDesign, _ = ExperimentalDesign.WriteExperimentalDesignToFile(newExperDesign); } } -} \ No newline at end of file +} diff --git a/MetaMorpheus/TaskLayer/SearchTask/SearchParameters.cs b/MetaMorpheus/TaskLayer/SearchTask/SearchParameters.cs index 76d42eefcf..4b7d659447 100644 --- a/MetaMorpheus/TaskLayer/SearchTask/SearchParameters.cs +++ b/MetaMorpheus/TaskLayer/SearchTask/SearchParameters.cs @@ -7,6 +7,12 @@ namespace TaskLayer { public class SearchParameters { + /// + /// Default maximum fragment size in Daltons used for indexing. This value is shared across + /// all task types that require fragment indexing (Search, Calibration, CrossLink, Glyco). + /// + public const double DefaultMaxFragmentSize = 30000.0; + public SearchParameters() { // default search task parameters @@ -26,7 +32,7 @@ public SearchParameters() WritePrunedDatabase = false; KeepAllUniprotMods = true; MassDiffAcceptorType = MassDiffAcceptorType.OneMM; - MaxFragmentSize = 30000.0; + MaxFragmentSize = DefaultMaxFragmentSize; MinAllowedInternalFragmentLength = 0; WriteMzId = true; WritePepXml = false; diff --git a/MetaMorpheus/Test/CalibrationTests.cs b/MetaMorpheus/Test/CalibrationTests.cs index 61a35c152f..b4e0f06c31 100644 --- a/MetaMorpheus/Test/CalibrationTests.cs +++ b/MetaMorpheus/Test/CalibrationTests.cs @@ -1,17 +1,21 @@ using EngineLayer; +using EngineLayer.DatabaseLoading; using FlashLFQ; using MassSpectrometry; +using MzLibUtil; using NUnit.Framework; +using Omics; +using Omics.Modifications; +using Proteomics; +using System; +using System.Reflection; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text.RegularExpressions; using TaskLayer; -using System; -using EngineLayer.DatabaseLoading; -using System.Diagnostics; -using MzLibUtil; -using Proteomics; +using Transcriptomics; namespace Test @@ -49,7 +53,7 @@ public static void ExperimentalDesignCalibrationTest(string nonCalibratedFile) string expectedCalibratedFileName = Path.GetFileNameWithoutExtension(nonCalibratedFilePath) + "-calib.mzML"; var expectedCalibratedFilePath = Path.Combine(outputFolder, expectedCalibratedFileName); var newExperDesign = ExperimentalDesign.ReadExperimentalDesign(newExpDesignPath, new List { expectedCalibratedFilePath }, out var errors); - + Assert.That(!errors.Any()); Assert.That(newExperDesign.Count == 1); @@ -75,15 +79,17 @@ public static void ExperimentalDesignCalibrationTest(string nonCalibratedFile) } [Test] - public static void TestToleranceExpansion() + [TestCase(SearchType.Classic)] + [TestCase(SearchType.Modern)] + public static void TestToleranceExpansion(SearchType searchType) { // capture warnings - var originalOut = Console.Out; - var originalErr = Console.Error; - var sw = new StringWriter(); - var listener = new TextWriterTraceListener(sw) { TraceOutputOptions = TraceOptions.None }; - Trace.Listeners.Add(listener); - Console.SetOut(sw); + var originalOut = Console.Out; + var originalErr = Console.Error; + var sw = new StringWriter(); + var listener = new TextWriterTraceListener(sw) { TraceOutputOptions = TraceOptions.None }; + Trace.Listeners.Add(listener); + Console.SetOut(sw); Console.SetError(sw); try @@ -103,6 +109,7 @@ public static void TestToleranceExpansion() // run calibration CalibrationTask calibrationTask = new(); + calibrationTask.CalibrationParameters.SearchType = searchType; calibrationTask.CommonParameters.PrecursorMassTolerance = new PpmTolerance(2); calibrationTask.CommonParameters.ProductMassTolerance = new PpmTolerance(2); calibrationTask.RunTask(outputFolder, new List { new DbForTask(myDatabase, false) }, new List { nonCalibratedFilePath }, "test"); @@ -149,7 +156,9 @@ public static void TestToleranceExpansion() } [Test] - public static void CalibrationTestNoPsms() + [TestCase(SearchType.Classic)] + [TestCase(SearchType.Modern)] + public static void CalibrationTestNoPsms(SearchType searchType) { // set up directories string unitTestFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, @"ExperimentalDesignCalibrationTest"); @@ -170,6 +179,7 @@ public static void CalibrationTestNoPsms() // run calibration CalibrationTask calibrationTask = new(); + calibrationTask.CalibrationParameters.SearchType = searchType; calibrationTask.RunTask(outputFolder, new List { new DbForTask(myDatabase, false) }, new List { nonCalibratedFilePath }, "test"); // test new experimental design written by calibration @@ -232,9 +242,12 @@ private static void CalibrationWarnHandler(object sender, StringEventArgs e, ref } [Test] - public static void CalibrationTestLowRes() + [TestCase(SearchType.Classic)] + [TestCase(SearchType.Modern)] + public static void CalibrationTestLowRes(SearchType searchType) { CalibrationTask calibrationTask = new CalibrationTask(); + calibrationTask.CalibrationParameters.SearchType = searchType; string outputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestCalibrationLow"); string myFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\TaGe_SA_A549_3_snip.mzML"); @@ -309,8 +322,8 @@ public static void ExperimentalDesignCalibrationAndSearch(string nonCalibratedFi // run the tasks EverythingRunnerEngine a = new EverythingRunnerEngine( - new List<(string, MetaMorpheusTask)> { ("", calibrationTask), ("", searchTask) }, - new List { nonCalibratedFilePath }, + new List<(string, MetaMorpheusTask)> { ("", calibrationTask), ("", searchTask) }, + new List { nonCalibratedFilePath }, new List { new DbForTask(myDatabase, false) }, outputFolder); @@ -368,7 +381,7 @@ public static void ExperimentalDesignCalibrationAndSearchWithOneCalibratibleAndO } [Test] - [TestCase("ExpDesFileNotFound","small.mzML", "Experimental design file not found!")] + [TestCase("ExpDesFileNotFound", "small.mzML", "Experimental design file not found!")] [TestCase("WrongNumberOfCells", "small.mzML", "Error: The experimental design was not formatted correctly. Expected 5 cells, but found 4 on line 2")] [TestCase("BioRepNotInteger", "small.mzML", "Error: The experimental design was not formatted correctly. The biorep on line 2 is not an integer")] [TestCase("FractionNotInteger", "small.mzML", "Error: The experimental design was not formatted correctly. The fraction on line 2 is not an integer")] @@ -392,7 +405,7 @@ public static void TestWriteNewExperimentalDesignFileDuringCalibration() string outputFolder = Path.Combine(unitTestFolder, @"TaskOutput"); Directory.CreateDirectory(unitTestFolder); Directory.CreateDirectory(outputFolder); - + // set up original spectra file (input to calibration) string nonCalibratedFilePath = Path.Combine(unitTestFolder, "filename1.mzML"); File.Copy(Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\SmallCalibratible_Yeast.mzML"), nonCalibratedFilePath, true); @@ -400,7 +413,7 @@ public static void TestWriteNewExperimentalDesignFileDuringCalibration() // set up original BAD experimental design (input to calibration) string experimentalDesignPath = Path.Combine(unitTestFolder, "ExperimentalDesign.tsv"); File.Copy(badExperimentalDesignPath, experimentalDesignPath, true); - + // protein db string myDatabase = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\smalldb.fasta"); @@ -410,16 +423,302 @@ public static void TestWriteNewExperimentalDesignFileDuringCalibration() bool wasCalled = false; MetaMorpheusTask.WarnHandler += (o, e) => wasCalled = true; - + calibrationTask.RunTask(outputFolder, new List { new DbForTask(myDatabase, false) }, new List { nonCalibratedFilePath }, "test"); - + //The original experimental design file is bad so we expect Warn event in "WriteNewExperimentalDesignFile" Assert.That(wasCalled); - + + + // clean up + Directory.Delete(unitTestFolder, true); + } + + /// + /// Tests that calibration using Modern Search produces the expected output files. + /// This is critical because Modern Search uses an indexed database approach (IndexingEngine + ModernSearchEngine) + /// rather than the ClassicSearchEngine. We need to verify that the calibration workflow works correctly + /// when using this alternative search strategy. + /// + [Test] + public static void CalibrationWithModernSearchTest() + { + // set up directories + string unitTestFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, @"CalibrationModernSearchTest"); + string outputFolder = Path.Combine(unitTestFolder, @"TaskOutput"); + Directory.CreateDirectory(unitTestFolder); + Directory.CreateDirectory(outputFolder); + + // set up original spectra file (input to calibration) + string nonCalibratedFilePath = Path.Combine(unitTestFolder, "testfile.mzML"); + File.Copy(Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\SmallCalibratible_Yeast.mzML"), nonCalibratedFilePath, true); + + // protein db + string myDatabase = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\smalldb.fasta"); + + // run calibration with Modern Search + CalibrationTask calibrationTask = new(); + calibrationTask.CalibrationParameters.SearchType = SearchType.Modern; + calibrationTask.RunTask(outputFolder, new List { new DbForTask(myDatabase, false) }, new List { nonCalibratedFilePath }, "test"); + + // test file-specific toml written by calibration w/ suggested ppm tolerances + string expectedTomlName = Path.GetFileNameWithoutExtension(nonCalibratedFilePath) + "-calib.toml"; + string expectedCalibratedFileName = Path.GetFileNameWithoutExtension(nonCalibratedFilePath) + "-calib.mzML"; + var expectedCalibratedFilePath = Path.Combine(outputFolder, expectedCalibratedFileName); + + Assert.That(File.Exists(Path.Combine(outputFolder, expectedTomlName)), "Calibration toml file should exist"); + Assert.That(File.Exists(expectedCalibratedFilePath), "Calibrated mzML file should exist"); + + var lines = File.ReadAllLines(Path.Combine(outputFolder, expectedTomlName)); + var tolerance = Regex.Match(lines[0], @"\d+\.\d*").Value; + var tolerance1 = Regex.Match(lines[1], @"\d+\.\d*").Value; + + Assert.That(double.TryParse(tolerance, out double tol), "Precursor tolerance should be parseable"); + Assert.That(double.TryParse(tolerance1, out double tol1), "Product tolerance should be parseable"); + Assert.That(lines[0].Contains("PrecursorMassTolerance"), "First line should contain PrecursorMassTolerance"); + Assert.That(lines[1].Contains("ProductMassTolerance"), "Second line should contain ProductMassTolerance"); + + // clean up + Directory.Delete(unitTestFolder, true); + } + + /// + /// Compares calibration results between Classic Search and Modern Search to ensure they produce + /// comparable results. This is critical to verify that the Modern Search implementation doesn't + /// introduce significant differences in calibration quality. Both search types should find + /// similar numbers of PSMs and produce similar mass tolerance recommendations. + /// + [Test] + public static void CalibrationClassicVsModernSearchComparison() + { + // set up directories + string unitTestFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, @"CalibrationClassicVsModernTest"); + string classicOutputFolder = Path.Combine(unitTestFolder, @"ClassicOutput"); + string modernOutputFolder = Path.Combine(unitTestFolder, @"ModernOutput"); + Directory.CreateDirectory(unitTestFolder); + Directory.CreateDirectory(classicOutputFolder); + Directory.CreateDirectory(modernOutputFolder); + + // set up original spectra files (need separate copies to avoid file locking issues) + string classicFilePath = Path.Combine(unitTestFolder, "classic_test.mzML"); + string modernFilePath = Path.Combine(unitTestFolder, "modern_test.mzML"); + File.Copy(Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\SmallCalibratible_Yeast.mzML"), classicFilePath, true); + File.Copy(Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\SmallCalibratible_Yeast.mzML"), modernFilePath, true); + + // protein db + string myDatabase = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\smalldb.fasta"); + + // run calibration with Classic Search + CalibrationTask classicCalibrationTask = new(); + classicCalibrationTask.CalibrationParameters.SearchType = SearchType.Classic; + classicCalibrationTask.RunTask(classicOutputFolder, new List { new DbForTask(myDatabase, false) }, new List { classicFilePath }, "classic"); + + // run calibration with Modern Search + CalibrationTask modernCalibrationTask = new(); + modernCalibrationTask.CalibrationParameters.SearchType = SearchType.Modern; + modernCalibrationTask.RunTask(modernOutputFolder, new List { new DbForTask(myDatabase, false) }, new List { modernFilePath }, "modern"); + + // verify both produced output files + string classicTomlPath = Path.Combine(classicOutputFolder, "classic_test-calib.toml"); + string modernTomlPath = Path.Combine(modernOutputFolder, "modern_test-calib.toml"); + + Assert.That(File.Exists(classicTomlPath), "Classic calibration should produce toml file"); + Assert.That(File.Exists(modernTomlPath), "Modern calibration should produce toml file"); + + // read tolerances from both + var classicLines = File.ReadAllLines(classicTomlPath); + var modernLines = File.ReadAllLines(modernTomlPath); + + Assert.That(double.TryParse(Regex.Match(classicLines[0], @"\d+\.\d*").Value, out double classicPrecursorTol), + "Classic precursor tolerance should be parseable"); + Assert.That(double.TryParse(Regex.Match(classicLines[1], @"\d+\.\d*").Value, out double classicProductTol), + "Classic product tolerance should be parseable"); + Assert.That(double.TryParse(Regex.Match(modernLines[0], @"\d+\.\d*").Value, out double modernPrecursorTol), + "Modern precursor tolerance should be parseable"); + Assert.That(double.TryParse(Regex.Match(modernLines[1], @"\d+\.\d*").Value, out double modernProductTol), + "Modern product tolerance should be parseable"); + + // tolerances should be very close — both engines search the same data + // and feed into the same calibration math + Assert.That(modernPrecursorTol, Is.InRange(classicPrecursorTol * 0.9, classicPrecursorTol * 1.1), + $"Modern precursor tolerance ({modernPrecursorTol}) should be within 10% of Classic ({classicPrecursorTol})"); + Assert.That(modernProductTol, Is.InRange(classicProductTol * 0.9, classicProductTol * 1.1), + $"Modern product tolerance ({modernProductTol}) should be within 10% of Classic ({classicProductTol})"); // clean up Directory.Delete(unitTestFolder, true); } + /// + /// Verifies that returns an empty + /// protein list and the correct excluded count when the input contains only non-Protein biopolymers. + /// GenerateIndexes uses this result to throw an informative MetaMorpheusException directing + /// users to Classic Search for non-protein analytes. + /// + [Test] + public static void FilterBiopolymersForModernSearch_AllNonProtein_ReturnsEmptyWithCorrectCount() + { + var rnaList = new List + { + new RNA("GUACUG", "rna_1"), + new RNA("AACUGCUAG", "rna_2") + }; + + var (proteins, excludedCount) = CalibrationTask.FilterBiopolymersForModernSearch(rnaList); + + Assert.That(proteins, Is.Empty, + "No proteins should be returned when the input contains only non-protein biopolymers"); + Assert.That(excludedCount, Is.EqualTo(2), + "Excluded count should equal the total number of non-protein entries"); + } + + /// + /// Verifies that correctly separates + /// a mixed biopolymer list, keeping only Protein entries and reporting the right excluded count. + /// GenerateIndexes uses this result to emit a warning when non-protein entries are dropped. + /// + [Test] + public static void FilterBiopolymersForModernSearch_MixedList_RetainsProteinsAndCountsExcluded() + { + var protein = new Protein("MKWVTFISLLLLFSSAYSR", "P00001"); + var rna = new RNA("GUACUG", "rna_1"); + var mixedList = new List { protein, rna }; + + var (proteins, excludedCount) = CalibrationTask.FilterBiopolymersForModernSearch(mixedList); + + Assert.That(proteins.Count, Is.EqualTo(1), + "Only the Protein entry should be retained"); + Assert.That(proteins[0].Accession, Is.EqualTo("P00001"), + "The retained protein should be the one from the input list"); + Assert.That(excludedCount, Is.EqualTo(1), + "Excluded count should equal the number of non-protein entries"); + } + + /// + /// Verifies that passes through + /// an all-protein list with zero excluded entries. + /// + [Test] + public static void FilterBiopolymersForModernSearch_AllProteins_ReturnsAllWithZeroExcluded() + { + var proteins = new List + { + new Protein("MKWVTFISLLLLFSSAYSR", "P00001"), + new Protein("ACDEFGHIKLMNPQRSTVWY", "P00002") + }; + + var (result, excludedCount) = CalibrationTask.FilterBiopolymersForModernSearch(proteins); + + Assert.That(result.Count, Is.EqualTo(2), + "All proteins should be retained when input contains only Protein entries"); + Assert.That(excludedCount, Is.EqualTo(0), + "No entries should be excluded when input contains only Protein entries"); + } + + /// + /// Verifies that throws a + /// when the loaded database contains only non-Protein biopolymers, + /// and that the message directs the user to Classic Search or a protein database. + /// The exception is thrown before any indexing work begins, so no spectra files are needed. + /// + [Test] + public static void GenerateIndexes_AllNonProteinDatabase_ThrowsMetaMorpheusException() + { + var calibrationTask = new CalibrationTask(); + var type = typeof(CalibrationTask); + + type.GetField("_proteinList", BindingFlags.NonPublic | BindingFlags.Instance) + .SetValue(calibrationTask, new List + { + new RNA("GUACUG", "rna_1"), + new RNA("AACUGCUAG", "rna_2") + }); + type.GetField("_taskId", BindingFlags.NonPublic | BindingFlags.Instance) + .SetValue(calibrationTask, "testTask"); + + var generateIndexes = type.GetMethod("GenerateIndexes", + BindingFlags.NonPublic | BindingFlags.Instance, + null, + new[] { typeof(CommonParameters), typeof(string) }, + null); + + MetaMorpheusException caughtException = null; + try + { + generateIndexes.Invoke(calibrationTask, new object[] { new CommonParameters(), "testFile" }); + } + catch (TargetInvocationException ex) + { + caughtException = ex.InnerException as MetaMorpheusException; + } + + Assert.That(caughtException, Is.Not.Null, + "A MetaMorpheusException should be thrown when the database contains no Protein entries"); + Assert.That(caughtException.Message, Does.Contain("Modern Search calibration requires protein sequences"), + "Exception message should describe the requirement for protein sequences"); + Assert.That(caughtException.Message, Does.Contain("2 non-protein biopolymer(s) were filtered out"), + "Exception message should report the number of excluded non-protein biopolymers"); + } + + /// + /// Verifies that emits a + /// warning via when the loaded database contains a mix + /// of Protein and non-Protein biopolymers, informing the user that only protein sequences are indexed. + /// The warning is emitted before the runs, so any failure in the + /// subsequent indexing step does not affect whether the warning was captured. + /// + [Test] + public static void GenerateIndexes_MixedBiopolymers_EmitsWarnForExcludedNonProteinEntries() + { + var calibrationTask = new CalibrationTask(); + var type = typeof(CalibrationTask); + + type.GetField("_proteinList", BindingFlags.NonPublic | BindingFlags.Instance) + .SetValue(calibrationTask, new List + { + new Protein("MKWVTFISLLLLFSSAYSR", "P00001"), + new RNA("GUACUG", "rna_1") + }); + type.GetField("_taskId", BindingFlags.NonPublic | BindingFlags.Instance) + .SetValue(calibrationTask, "testTask"); + type.GetField("_variableModifications", BindingFlags.NonPublic | BindingFlags.Instance) + .SetValue(calibrationTask, new List()); + type.GetField("_fixedModifications", BindingFlags.NonPublic | BindingFlags.Instance) + .SetValue(calibrationTask, new List()); + + string dbPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\smalldb.fasta"); + type.GetField("_dbFilenameList", BindingFlags.NonPublic | BindingFlags.Instance) + .SetValue(calibrationTask, new List { new DbForTask(dbPath, false) }); + + var warnings = new List(); + EventHandler warnHandler = (o, e) => warnings.Add(e.S); + MetaMorpheusTask.WarnHandler += warnHandler; + + var generateIndexes = type.GetMethod("GenerateIndexes", + BindingFlags.NonPublic | BindingFlags.Instance, + null, + new[] { typeof(CommonParameters), typeof(string) }, + null); + + try + { + generateIndexes.Invoke(calibrationTask, new object[] { new CommonParameters(), "testFile" }); + } + catch (TargetInvocationException) + { + // The warning is emitted before the IndexingEngine runs; any subsequent + // failure does not affect whether the warning was captured. + } + finally + { + MetaMorpheusTask.WarnHandler -= warnHandler; + } + + Assert.That(warnings.Any(w => w.Contains("1 non-protein biopolymer(s) were excluded from the search index")), + "A warning should be emitted when non-protein entries are excluded from the search index"); + Assert.That(warnings.Any(w => w.Contains("Only protein sequences are supported by Modern Search")), + "Warning should indicate that Modern Search only supports protein sequences"); + } + } -} \ No newline at end of file +} From eb41ee2b8f99fc0ea2efa52d18476e8c5574c6ac Mon Sep 17 00:00:00 2001 From: trishorts Date: Mon, 6 Apr 2026 16:05:45 -0500 Subject: [PATCH 09/12] Enable non-specific search to use library spectra (#2618) * enable spectrum library calculation in non-specific search * this test don't work * ok i think this test work * a better unit test * major complaints dealt with * Change PSM count assertion to check for greater than 30 --------- Co-authored-by: pcruzparri <43578034+pcruzparri@users.noreply.github.com> --- .../TaskLayer/SearchTask/SearchTask.cs | 35 +++++++-- .../Test/SpectralLibraryReaderTest.cs | 75 ++++++++++++++++--- MetaMorpheus/Test/Test.csproj | 3 + .../TestData/bosTaurusSpectralLibrary.msp | 12 +++ 4 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 MetaMorpheus/Test/TestData/bosTaurusSpectralLibrary.msp diff --git a/MetaMorpheus/TaskLayer/SearchTask/SearchTask.cs b/MetaMorpheus/TaskLayer/SearchTask/SearchTask.cs index a724e5399b..a8e34662d7 100644 --- a/MetaMorpheus/TaskLayer/SearchTask/SearchTask.cs +++ b/MetaMorpheus/TaskLayer/SearchTask/SearchTask.cs @@ -88,7 +88,7 @@ public static MassDiffAcceptor GetMassDiffAcceptor(Tolerance precursorMassTolera } } - protected override MyTaskResults RunSpecific(string OutputFolder, List dbFilenameList, List currentRawFileList, string taskId, + protected override MyTaskResults RunSpecific(string OutputFolder, List dbFilenameList, List currentRawFileList, string taskId, FileSpecificParameters[] fileSettingsList) { MyTaskResults = new(this); @@ -199,7 +199,7 @@ protected override MyTaskResults RunSpecific(string OutputFolder, List { taskId } ); + Status("Searching files...", new List { taskId }); Status("Searching files...", new List { taskId, "Individual Spectra Files" }); Dictionary numMs2SpectraPerFile = new Dictionary(); // key is filename, value is an int array of length 2, where the first element is the number of MS2 spectra in the file, and the second element is the number of different deconvoluted precursors assigned to those scans @@ -236,7 +236,7 @@ protected override MyTaskResults RunSpecific(string OutputFolder, List s.MsnOrder ==3)) + if (SearchParameters.DoMultiplexQuantification && myMsDataFile.Scans.Any(s => s.MsnOrder == 3)) { // In most experiments with MS3 scans for reporter ion detection, MS2ChildScanDissociationType is LowCID. // However, we do not set it here to allow for flexibility in dissociation type selection. @@ -276,7 +276,7 @@ protected override MyTaskResults RunSpecific(string OutputFolder, List p.DigestionParams)], + ListOfDigestionParams = [.. fileSpecificCommonParams.Select(p => p.DigestionParams)], CurrentRawFileList = currentRawFileList, MyFileManager = myFileManager, NumNotches = numNotches, diff --git a/MetaMorpheus/Test/SpectralLibraryReaderTest.cs b/MetaMorpheus/Test/SpectralLibraryReaderTest.cs index bfba8d50d4..77aed1a85e 100644 --- a/MetaMorpheus/Test/SpectralLibraryReaderTest.cs +++ b/MetaMorpheus/Test/SpectralLibraryReaderTest.cs @@ -1,18 +1,21 @@ -using NUnit.Framework; -using System.IO; -using System; -using System.Linq; -using EngineLayer; -using TaskLayer; -using System.Collections.Generic; -using Omics.Fragmentation; -using Proteomics; -using MassSpectrometry; +using EngineLayer; using EngineLayer.ClassicSearch; using EngineLayer.DatabaseLoading; +using MassSpectrometry; +using MzLibUtil; +using Nett; +using NUnit.Framework; +using Omics.Fragmentation; using Omics.Modifications; +using Proteomics; +using Proteomics.ProteolyticDigestion; +using Readers; using Readers.SpectralLibrary; -using MzLibUtil; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using TaskLayer; namespace Test { @@ -217,6 +220,56 @@ public static void AnotherSpectralLibrarySearchTestDecoyNoLibrarySpectrum() } + /// + /// Tests that spectral angles are calculated correctly when using a spectral library with non-specific enzyme search. + /// This is critical because non-specific search stores PSMs in a different data structure (fileSpecificPsmsSeparatedByFdrCategory) + /// rather than the standard fileSpecificPsms array used by Classic and Modern search. + /// Spectral angles must be calculated before FDR analysis since they are used in PEP (posterior error probability) calculation. + /// This test verifies: + /// 1. The output TSV file is properly formatted (no warnings when reading) + /// 2. The expected number of PSMs are found + /// 3. The "Normalized Spectral Angle" column exists in the output + /// 4. At least one PSM has a valid spectral angle calculated (value >= 0, not -1 which indicates no calculation) + /// + [Test] + public static void SpectralLibrarySearchWithNonSpecificSearchTest() + { + var myTomlPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\NonSpecificSearchToml.toml"); + var searchTaskLoaded = Toml.ReadFile(myTomlPath, MetaMorpheusTask.tomlConfig); + string outputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, @"SpectralLibraryNonSpecificSearchTest"); + Directory.CreateDirectory(outputFolder); + string myFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\TaGe_SA_A549_3_snip.mzML"); + string myDatabase = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\bosTaurusEnamPruned.xml"); + string mySpectralLibrary = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\bosTaurusSpectralLibrary.msp"); + + var engineToml = new EverythingRunnerEngine(new List<(string, MetaMorpheusTask)> { ("SearchTOML", searchTaskLoaded) }, new List { myFile }, new List { new DbForTask(myDatabase, false), new DbForTask(mySpectralLibrary, false) }, outputFolder); + engineToml.Run(); + + string psmFile = Path.Combine(outputFolder, @"SearchTOML\AllPSMs.psmtsv"); + + // Verify the output file can be read without warnings + List parsedPsms = SpectrumMatchTsvReader.ReadPsmTsv(psmFile, out var warnings); + Assert.That(warnings, Is.Empty, "TSV file should be readable without warnings"); + + // Verify PSMs were found + Assert.That(parsedPsms.Count, Is.GreaterThan(30), "Expected 38 total PSMs"); + + // Verify the Normalized Spectral Angle column exists in the output file + var headerLine = File.ReadLines(psmFile).First(); + var headers = headerLine.Split('\t'); + int spectralAngleIndex = Array.IndexOf(headers, "Normalized Spectral Angle"); + Assert.That(spectralAngleIndex, Is.GreaterThanOrEqualTo(0), + "Normalized Spectral Angle column should exist in the output TSV"); + + // Verify at least one PSM has a spectral angle calculated (not -1 which indicates no spectral angle) + // This confirms the fix for non-specific search spectral angle calculation is working + bool hasValidSpectralAngle = parsedPsms.Any(psm => psm.SpectralAngle.HasValue && psm.SpectralAngle.Value >= 0); + Assert.That(hasValidSpectralAngle, Is.True, + "At least one PSM should have a valid spectral angle calculated for non-specific search with spectral library"); + + Directory.Delete(outputFolder, true); + } + } } diff --git a/MetaMorpheus/Test/Test.csproj b/MetaMorpheus/Test/Test.csproj index 7ca29075c6..1928a80bb1 100644 --- a/MetaMorpheus/Test/Test.csproj +++ b/MetaMorpheus/Test/Test.csproj @@ -241,6 +241,9 @@ Always + + Always + Always diff --git a/MetaMorpheus/Test/TestData/bosTaurusSpectralLibrary.msp b/MetaMorpheus/Test/TestData/bosTaurusSpectralLibrary.msp new file mode 100644 index 0000000000..cbcf14e3c0 --- /dev/null +++ b/MetaMorpheus/Test/TestData/bosTaurusSpectralLibrary.msp @@ -0,0 +1,12 @@ +Name: EGGAQDSP[Common Biological:Hydroxylation on P]VPE[Metal:Magnesium on E]AHLAD/3 +MW: 544.2285805285128 +Comment: Parent=544.2285805285128 RT=45.71715833333333 +Num peaks: 8 +130.05018615722656 0.04932016185284771 "b1^1/0ppm" +187.07142639160156 0.1602421358893173 "b2^1/0ppm" +244.09349060058594 0.15911879547864063 "b3^1/0ppm" +315.13018798828125 0.17239024905339215 "b4^1/0ppm" +443.1852722167969 0.023484907591677602 "b5^1/0ppm" +774.3210077626543 1 "y7^1/0ppm" +758.295166015625 0.07613163014346051 "b8^1/0ppm" +873.3892079799635 0.4522696878710261 "y8^1/0ppm" From a9441831fa7bbeaab42e3f0f229e21b745a4ed8e Mon Sep 17 00:00:00 2001 From: RayMSMS <150720362+RayMSMS@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:07:00 -0500 Subject: [PATCH 10/12] Resolve the commit --- MetaMorpheus/EngineLayer/CommonParameters.cs | 4 +- .../GlycoSearch/GlycoSearchEngine.cs | 5 +- .../TaskWindows/CalibrateTaskWindow.xaml.cs | 2 +- .../GUI/TaskWindows/GPTMDTaskWindow.xaml.cs | 2 +- .../TaskWindows/GlycoSearchTaskWindow.xaml | 4 +- .../TaskWindows/GlycoSearchTaskWindow.xaml.cs | 23 +- .../GUI/TaskWindows/SearchTaskWindow.xaml.cs | 1 - .../TaskWindows/XLSearchTaskWindow.xaml.cs | 2 +- MetaMorpheus/GUI/Util/TaskValidator.cs | 61 ++++- MetaMorpheus/TaskLayer/MetaMorpheusTask.cs | 2 +- MetaMorpheus/Test/GlycoSearchEngineTest.cs | 211 ++++++++++++++++-- ...askconfigOGlycoTest_ToleranceTracking.toml | 65 ++++++ MetaMorpheus/Test/Test.csproj | 3 + 13 files changed, 342 insertions(+), 43 deletions(-) create mode 100644 MetaMorpheus/Test/GlycoTestData/GlycoSearchTaskconfigOGlycoTest_ToleranceTracking.toml diff --git a/MetaMorpheus/EngineLayer/CommonParameters.cs b/MetaMorpheus/EngineLayer/CommonParameters.cs index ceba632f4f..c6876c1d14 100644 --- a/MetaMorpheus/EngineLayer/CommonParameters.cs +++ b/MetaMorpheus/EngineLayer/CommonParameters.cs @@ -86,7 +86,7 @@ public CommonParameters( MaxThreadsToUsePerFile = maxThreadsToUsePerFile == -1 ? Environment.ProcessorCount > 1 ? Environment.ProcessorCount - 1 : 1 : maxThreadsToUsePerFile; ProductMassTolerance = productMassTolerance ?? new PpmTolerance(20); PrecursorMassTolerance = precursorMassTolerance ?? new PpmTolerance(5); - ProductMassTolerance_LowRes = productMassTolerance_LowRes ?? ProductMassTolerance; + ProductMassTolerance_LowRes = productMassTolerance_LowRes ?? new AbsoluteTolerance(0.35); DeconvolutionMassTolerance = deconvolutionMassTolerance ?? new PpmTolerance(4); DigestionParams = digestionParams ?? new DigestionParams(); DissociationType = dissociationType; @@ -162,7 +162,7 @@ public int DeconvolutionMaxAssumedChargeState [TomlIgnore] public Tolerance DeconvolutionMassTolerance { get; private set; } public int TotalPartitions { get; set; } public Tolerance ProductMassTolerance { get; set; } // public setter required for calibration task - public Tolerance ProductMassTolerance_LowRes { get; set; } // Wider mass tolerance for lower resolution analyzer (e.g. ion trap). + public Tolerance ProductMassTolerance_LowRes { get; set; }// Wider mass tolerance for lower resolution analyzer (e.g. ion trap). For now, this is a independent parameter, will not be modified by the calibration task. public Tolerance PrecursorMassTolerance { get; set; } // public setter required for calibration task public bool AddCompIons { get; set; } /// diff --git a/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs b/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs index f58eb06178..f1c31a60cc 100644 --- a/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs +++ b/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs @@ -332,7 +332,7 @@ private GlycoSpectralMatch CreateGsm(Ms2ScanWithSpecificMass theScan, int scanIn foreach (var childScan in theScan.ChildScans) { var childFragments = GlycoPeptides.OGlyGetTheoreticalFragments(CommonParameters.MS2ChildScanDissociationType, CommonParameters.CustomIons, peptide, peptideWithMod); - bool isIonTrapData = childScan.TheScan.MzAnalyzer == MZAnalyzerType.IonTrap2D; + bool isIonTrapData = childScan.TheScan.MzAnalyzer == MZAnalyzerType.IonTrap2D || childScan.TheScan.MzAnalyzer == MZAnalyzerType.IonTrap3D; var matchedChildIons = MatchFragmentIons(childScan, childFragments, CommonParameters, isLowRes : isIonTrapData); n += childFragments.Where(v => v.ProductType == ProductType.c || v.ProductType == ProductType.zDot).Count(); @@ -447,7 +447,8 @@ private void FindOGlycan(Ms2ScanWithSpecificMass theScan, int scanIndex, int sco { localizationScan = theScan.ChildScans.First(); // For the localization scan, if it is from ion trap, we will use a wider tolerance for the localization. - toleranceForLocalizationScan = localizationScan.TheScan.MzAnalyzer == MZAnalyzerType.IonTrap2D? CommonParameters.ProductMassTolerance_LowRes : CommonParameters.ProductMassTolerance; + toleranceForLocalizationScan = localizationScan.TheScan.MzAnalyzer == MZAnalyzerType.IonTrap2D || + localizationScan.TheScan.MzAnalyzer == MZAnalyzerType.IonTrap3D ? CommonParameters.ProductMassTolerance_LowRes : CommonParameters.ProductMassTolerance; theScanBestPeptide.Fragment(DissociationType.ETD, FragmentationTerminus.Both, products); } diff --git a/MetaMorpheus/GUI/TaskWindows/CalibrateTaskWindow.xaml.cs b/MetaMorpheus/GUI/TaskWindows/CalibrateTaskWindow.xaml.cs index 691e1d8c6a..2906a3b917 100644 --- a/MetaMorpheus/GUI/TaskWindows/CalibrateTaskWindow.xaml.cs +++ b/MetaMorpheus/GUI/TaskWindows/CalibrateTaskWindow.xaml.cs @@ -234,7 +234,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) string fieldNotUsed = "1"; bool isRnaMode = GuiGlobalParamsViewModel.Instance.IsRnaMode; - if (!TaskValidator.CheckTaskSettingsValidity(PrecursorMassToleranceTextBox.Text, ProductMassToleranceTextBox.Text, null,MissedCleavagesTextBox.Text, + if (!TaskValidator.CheckTaskSettingsValidity(PrecursorMassToleranceTextBox.Text, ProductMassToleranceTextBox.Text, MissedCleavagesTextBox.Text, MaxModificationIsoformsTextBox.Text, MinPeptideLengthTextBox.Text, MaxPeptideLengthTextBox.Text, MaxThreadsTextBox.Text, MinScoreAllowed.Text, fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, null, null, fieldNotUsed, MaxModsPerPeptideTextBox.Text, fieldNotUsed, null, null, null)) diff --git a/MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml.cs b/MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml.cs index f01e50d34e..9ff4d3ee91 100644 --- a/MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml.cs +++ b/MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml.cs @@ -438,7 +438,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) { string fieldNotUsed = "1"; - if (!TaskValidator.CheckTaskSettingsValidity(PrecursorMassToleranceTextBox.Text, ProductMassToleranceTextBox.Text, null, MissedCleavagesTextBox.Text, + if (!TaskValidator.CheckTaskSettingsValidity(PrecursorMassToleranceTextBox.Text, ProductMassToleranceTextBox.Text, MissedCleavagesTextBox.Text, MaxModificationIsoformsTextBox.Text, MinPeptideLengthTextBox.Text, MaxPeptideLengthTextBox.Text, MaxThreadsTextBox.Text, MinScoreAllowed.Text, fieldNotUsed, fieldNotUsed, fieldNotUsed, DeconHostViewModel.PrecursorDeconvolutionParameters.MaxAssumedChargeState.ToString(), NumberOfPeaksToKeepPerWindowTextBox.Text, MinimumAllowedIntensityRatioToBasePeakTexBox.Text, null, null, fieldNotUsed, fieldNotUsed, fieldNotUsed, null, null, null)) diff --git a/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml b/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml index 7304203450..5e48599319 100644 --- a/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml +++ b/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml @@ -268,8 +268,8 @@ - - + + diff --git a/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml.cs b/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml.cs index 85305135b9..73c1de3df7 100644 --- a/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml.cs +++ b/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml.cs @@ -90,8 +90,8 @@ private void PopulateChoices() productMassToleranceComboBox.Items.Add("Da"); productMassToleranceComboBox.Items.Add("ppm"); - childScanMassToleranceComboBox.Items.Add("Da"); - childScanMassToleranceComboBox.Items.Add("ppm"); + productMassTolerance_LowResComboBox.Items.Add("Da"); + productMassTolerance_LowResComboBox.Items.Add("ppm"); foreach (var hm in GlobalVariables.AllModsKnown.Where(b => b.ValidModification == true).GroupBy(b => b.ModificationType)) { @@ -186,8 +186,8 @@ private void UpdateFieldsFromTask(GlycoSearchTask task) TxtBoxMaxModPerPep.Text = task.CommonParameters.DigestionParams.MaxMods.ToString(CultureInfo.InvariantCulture); productMassToleranceTextBox.Text = task.CommonParameters.ProductMassTolerance.Value.ToString(CultureInfo.InvariantCulture); productMassToleranceComboBox.SelectedIndex = task.CommonParameters.ProductMassTolerance is AbsoluteTolerance ? 0 : 1; - childScanMassToleranceTextBox.Text = task.CommonParameters.ProductMassTolerance_LowRes?.Value.ToString(CultureInfo.InvariantCulture); - childScanMassToleranceComboBox.SelectedIndex = task.CommonParameters.ProductMassTolerance_LowRes is AbsoluteTolerance ? 0 : 1; + productMassTolerance_LowResTextBox.Text = task.CommonParameters.ProductMassTolerance_LowRes?.Value.ToString(CultureInfo.InvariantCulture); + productMassTolerance_LowResComboBox.SelectedIndex = task.CommonParameters.ProductMassTolerance_LowRes is AbsoluteTolerance ? 0 : 1; minScoreAllowed.Text = task.CommonParameters.ScoreCutoff.ToString(CultureInfo.InvariantCulture); numberOfDatabaseSearchesTextBox.Text = task.CommonParameters.TotalPartitions.ToString(CultureInfo.InvariantCulture); maxThreadsTextBox.Text = task.CommonParameters.MaxThreadsToUsePerFile.ToString(CultureInfo.InvariantCulture); @@ -261,7 +261,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) { string fieldNotUsed = "1"; - if (!TaskValidator.CheckTaskSettingsValidity(PrecusorMsTlTextBox.Text, productMassToleranceTextBox.Text, childScanMassToleranceTextBox.Text, missedCleavagesTextBox.Text, + if (!TaskValidator.CheckTaskSettingsValidity(PrecusorMsTlTextBox.Text, productMassToleranceTextBox.Text, productMassTolerance_LowResTextBox.Text, missedCleavagesTextBox.Text, maxModificationIsoformsTextBox.Text, MinPeptideLengthTextBox.Text, MaxPeptideLengthTextBox.Text, maxThreadsTextBox.Text, minScoreAllowed.Text, fieldNotUsed, fieldNotUsed, fieldNotUsed, DeconHostViewModel.PrecursorDeconvolutionParameters.MaxAssumedChargeState.ToString(), TopNPeaksTextBox.Text, MinRatioTextBox.Text, null, null, numberOfDatabaseSearchesTextBox.Text, TxtBoxMaxModPerPep.Text, fieldNotUsed, null, null, null)) @@ -367,20 +367,21 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) } Tolerance ProductMassTolerance_lowRes; - var childScanMassToleranceText = childScanMassToleranceTextBox.Text; - if (string.IsNullOrWhiteSpace(childScanMassToleranceText)) + var productMassTolerance_LowResToleranceText = productMassTolerance_LowResTextBox.Text; + if (string.IsNullOrWhiteSpace(productMassTolerance_LowResToleranceText)) { // If no child scan mass tolerance is specified, fall back to product mass tolerance - ProductMassTolerance_lowRes = ProductMassTolerance; + ProductMassTolerance_lowRes = new AbsoluteTolerance(0.35); } else { - if (!double.TryParse(childScanMassToleranceText, NumberStyles.Any, CultureInfo.InvariantCulture, out double parsedChildTolerance)) + // we already validate via non-positive TaskValidator, but this local guard is still useful as defensive programming in case validation flow changes later. + if (!double.TryParse(productMassTolerance_LowResToleranceText, NumberStyles.Any, CultureInfo.InvariantCulture, out double parsedChildTolerance) || parsedChildTolerance <= 0) { - MessageBox.Show("The child scan mass tolerance is invalid. Please enter a positive number."); + MessageBox.Show("The low-resolution product mass tolerance is invalid. Please enter a positive number."); return; } - if (childScanMassToleranceComboBox.SelectedIndex == 0) + if (productMassTolerance_LowResComboBox.SelectedIndex == 0) { ProductMassTolerance_lowRes = new AbsoluteTolerance(parsedChildTolerance); } diff --git a/MetaMorpheus/GUI/TaskWindows/SearchTaskWindow.xaml.cs b/MetaMorpheus/GUI/TaskWindows/SearchTaskWindow.xaml.cs index 792e351641..df2f2e58ea 100644 --- a/MetaMorpheus/GUI/TaskWindows/SearchTaskWindow.xaml.cs +++ b/MetaMorpheus/GUI/TaskWindows/SearchTaskWindow.xaml.cs @@ -456,7 +456,6 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) if (!TaskValidator.CheckTaskSettingsValidity( PrecursorMassToleranceTextBox.Text, ProductMassToleranceTextBox.Text, - null, MissedCleavagesTextBox.Text, maxModificationIsoformsTextBox.Text, MinPeptideLengthTextBox.Text, diff --git a/MetaMorpheus/GUI/TaskWindows/XLSearchTaskWindow.xaml.cs b/MetaMorpheus/GUI/TaskWindows/XLSearchTaskWindow.xaml.cs index 6d7b51e036..1a893dcd9d 100644 --- a/MetaMorpheus/GUI/TaskWindows/XLSearchTaskWindow.xaml.cs +++ b/MetaMorpheus/GUI/TaskWindows/XLSearchTaskWindow.xaml.cs @@ -240,7 +240,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) { string fieldNotUsed = "1"; - if (!TaskValidator.CheckTaskSettingsValidity(XLPrecusorMsTlTextBox.Text, productMassToleranceTextBox.Text, null, missedCleavagesTextBox.Text, + if (!TaskValidator.CheckTaskSettingsValidity(XLPrecusorMsTlTextBox.Text, productMassToleranceTextBox.Text, missedCleavagesTextBox.Text, maxModificationIsoformsTextBox.Text, MinPeptideLengthTextBox.Text, MaxPeptideLengthTextBox.Text, maxThreadsTextBox.Text, minScoreAllowed.Text, fieldNotUsed, fieldNotUsed, fieldNotUsed, DeconHostViewModel.PrecursorDeconvolutionParameters.MaxAssumedChargeState.ToString(), TopNPeaksTextBox.Text, MinRatioTextBox.Text, null, null, numberOfDatabaseSearchesTextBox.Text, fieldNotUsed, fieldNotUsed, null, null, null)) diff --git a/MetaMorpheus/GUI/Util/TaskValidator.cs b/MetaMorpheus/GUI/Util/TaskValidator.cs index ee37727610..4da4165e92 100644 --- a/MetaMorpheus/GUI/Util/TaskValidator.cs +++ b/MetaMorpheus/GUI/Util/TaskValidator.cs @@ -12,12 +12,65 @@ namespace MetaMorpheusGUI /// public static class TaskValidator { + /// + /// Backward-compatible overload without low-res tolerance parameter. + /// + public static bool CheckTaskSettingsValidity( + string precursorMassTolerance, + string productMassTolerance, + string maxMissedCleavages, + string maxModificationIsoforms, + string minPeptideLength, + string maxPeptideLength, + string maxThreads, + string minScore, + string peakFindingTolerance, + string mbrFdrThreshold, + string histogramBinWidth, + string deconMaxAssumedCharge, + string numberOfPeaksToKeepPerWindow, + string minimumAllowedIntensityRatio, + string windowWidthThomsons, + string numberOfWindows, + string numberOfDatabaseSearches, + string maxModsPerPeptide, + string maxFragmentMass, + string qValueFilter, + string pepqValueFilter, + string minInternalIonLength) + { + return CheckTaskSettingsValidity( + precursorMassTolerance: precursorMassTolerance, + productMassTolerance: productMassTolerance, + productMassTolerance_LowRes: null, + maxMissedCleavages: maxMissedCleavages, + maxModificationIsoforms: maxModificationIsoforms, + minPeptideLength: minPeptideLength, + maxPeptideLength: maxPeptideLength, + maxThreads: maxThreads, + minScore: minScore, + peakFindingTolerance: peakFindingTolerance, + mbrFdrThreshold: mbrFdrThreshold, + histogramBinWidth: histogramBinWidth, + deconMaxAssumedCharge: deconMaxAssumedCharge, + numberOfPeaksToKeepPerWindow: numberOfPeaksToKeepPerWindow, + minimumAllowedIntensityRatio: minimumAllowedIntensityRatio, + windowWidthThomsons: windowWidthThomsons, + numberOfWindows: numberOfWindows, + numberOfDatabaseSearches: numberOfDatabaseSearches, + maxModsPerPeptide: maxModsPerPeptide, + maxFragmentMass: maxFragmentMass, + qValueFilter: qValueFilter, + pepqValueFilter: pepqValueFilter, + minInternalIonLength: minInternalIonLength); + } + /// /// Checks the validity of each setting passed from the GUI task windows /// public static bool CheckTaskSettingsValidity(string precursorMassTolerance, string productMassTolerance, - string childScanMassTolerance, + string productMassTolerance_LowRes, string maxMissedCleavages, string maxModificationIsoforms, string minPeptideLength, @@ -47,7 +100,7 @@ string minInternalIonLength { (CheckPrecursorMassTolerance(precursorMassTolerance)), (CheckProductMassTolerance(productMassTolerance)), - (string.IsNullOrWhiteSpace(childScanMassTolerance) || CheckChildScanMassTolerance(childScanMassTolerance)), + (string.IsNullOrWhiteSpace(productMassTolerance_LowRes) || CheckProductMassTolerance_LowRes(productMassTolerance_LowRes)), (CheckMaxMissedCleavages(maxMissedCleavages)), (CheckMaxModificationIsoForms(maxModificationIsoforms)), (CheckPeptideLength(minPeptideLength, maxPeptideLength)), @@ -174,11 +227,11 @@ public static bool CheckProductMassTolerance(string text) return true; } - public static bool CheckChildScanMassTolerance(string text) + public static bool CheckProductMassTolerance_LowRes(string text) { if (!double.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out double childScanMassTolerance) || childScanMassTolerance <= 0) { - MessageBox.Show("The child scan mass tolerance is invalid. \n You entered " + '"' + text + '"' + "\n Please enter a positive number."); + MessageBox.Show("The Low-Res product mass tolerance is invalid. \n You entered " + '"' + text + '"' + "\n Please enter a positive number."); return false; } return true; diff --git a/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs b/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs index b7a582178c..360c0c81d2 100644 --- a/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs +++ b/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs @@ -541,7 +541,7 @@ public static CommonParameters SetAllFileSpecificCommonParams(CommonParameters c // set the rest of the file-specific parameters Tolerance precursorMassTolerance = fileSpecificParams.PrecursorMassTolerance ?? commonParams.PrecursorMassTolerance; Tolerance productMassTolerance = fileSpecificParams.ProductMassTolerance ?? commonParams.ProductMassTolerance; - Tolerance productMassTolerance_LowRes = fileSpecificParams.ProductMassTolerance_LowRes ?? commonParams.ProductMassTolerance; + Tolerance productMassTolerance_LowRes = fileSpecificParams.ProductMassTolerance_LowRes ?? commonParams.ProductMassTolerance_LowRes; DissociationType dissociationType = fileSpecificParams.DissociationType ?? commonParams.DissociationType; string separationType = fileSpecificParams.SeparationType ?? commonParams.SeparationType; diff --git a/MetaMorpheus/Test/GlycoSearchEngineTest.cs b/MetaMorpheus/Test/GlycoSearchEngineTest.cs index 4f72489362..540d25483b 100644 --- a/MetaMorpheus/Test/GlycoSearchEngineTest.cs +++ b/MetaMorpheus/Test/GlycoSearchEngineTest.cs @@ -1,8 +1,11 @@ using Chemistry; +using Easy.Common.Extensions; using EngineLayer; +using EngineLayer.DatabaseLoading; using EngineLayer.GlycoSearch; using MassSpectrometry; using MzLibUtil; +using Nett; using NUnit.Framework; using Omics.Modifications; using Proteomics; @@ -71,21 +74,66 @@ public void CreateGsm_WithWideProductTolerance_ScanInfo_p_IsCappedToOne() } [Test] - public static void TestChildScanToleranceConstruction_Default() + public static void TestLowResToleranceConstruction_Default() { var commonParameters = new CommonParameters(); Assert.That(commonParameters.ProductMassTolerance_LowRes, Is.Not.Null, "ChildScanMassTolerance should be initialized by default."); - // Default product tolerance is a PpmTolerance of 20 ppm; verify type and numeric width equivalence - Assert.That(commonParameters.ProductMassTolerance, Is.TypeOf()); - Assert.That(commonParameters.ProductMassTolerance.GetRange(1000).Width, Is.EqualTo(new PpmTolerance(20).GetRange(1000).Width)); + // Default product tolerance is a AbsoluteTolerance of 0.35 Da; verify type and numeric width equivalence + Assert.That(commonParameters.ProductMassTolerance_LowRes, Is.TypeOf()); + Assert.That(commonParameters.ProductMassTolerance_LowRes.GetRange(1000).Width, Is.EqualTo(new AbsoluteTolerance(0.35).GetRange(1000).Width)); + } + + [Test] + public static void TestLowResToleranceConstruction_Default2() + { + // In this toml settng, we omit the productMassTolerance_LowRes. The test confirms that the default value is 0.35 Da. + string outputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "TESTGlycoData"); + Directory.CreateDirectory(outputFolder); + + try + { + var inputTask = Toml.ReadFile( + Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\GlycoSearchTaskconfigOGlycoTest_ToleranceTracking.toml"), + MetaMorpheusTask.tomlConfig); + + // Confirm read-in behavior (tracking expected when low-res omitted) + Assert.That(inputTask.CommonParameters.ProductMassTolerance, Is.TypeOf()); + Assert.That(inputTask.CommonParameters.ProductMassTolerance.Value, Is.EqualTo(40).Within(1e-9)); + Assert.That(inputTask.CommonParameters.ProductMassTolerance_LowRes, Is.TypeOf()); + Assert.That(inputTask.CommonParameters.ProductMassTolerance_LowRes.Value, Is.EqualTo(0.35)); - // By default child tolerance should equal product tolerance - Assert.That(commonParameters.ProductMassTolerance_LowRes.GetRange(1000).Width, Is.EqualTo(commonParameters.ProductMassTolerance.GetRange(1000).Width)); + DbForTask db = new(Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\P16150.fasta"), false); + string spectraFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\2019_09_16_StcEmix_35trig_EThcD25_rep1_9906.mgf"); + + new EverythingRunnerEngine( + new List<(string, MetaMorpheusTask)> { ("Task", inputTask) }, + new List { spectraFile }, + new List { db }, + outputFolder).Run(); + + // Confirm written TOML exists + string writtenTaskToml = Path.Combine(outputFolder, "Task Settings", "Taskconfig.toml"); + Assert.That(File.Exists(writtenTaskToml), Is.True, "Expected task config TOML was not written."); + + // Confirm written TOML preserves tracked values + var writtenTask = Toml.ReadFile(writtenTaskToml, MetaMorpheusTask.tomlConfig); + Assert.That(writtenTask.CommonParameters.ProductMassTolerance, Is.TypeOf()); + Assert.That(writtenTask.CommonParameters.ProductMassTolerance.Value, Is.EqualTo(40).Within(1e-9)); + Assert.That(writtenTask.CommonParameters.ProductMassTolerance_LowRes, Is.TypeOf()); + Assert.That(writtenTask.CommonParameters.ProductMassTolerance_LowRes.Value, Is.EqualTo(0.35)); + } + finally + { + if (Directory.Exists(outputFolder)) + { + Directory.Delete(outputFolder, true); + } + } } [Test] - public static void TestChildScanTolerance_Da() + public static void TestLowResTolerance_Da() { // Purpose: // - Validate that fragment matching use `ChildScanMassTolerance` for child scans (isChildScan flag). @@ -93,14 +141,16 @@ public static void TestChildScanTolerance_Da() // - For matched fragment ions, verify the observed m/z (converted to mass) is within the correct tolerance. // --- initialize tolerances for test - var childScanTolerance = new AbsoluteTolerance(0.5); - + var lowResTolerance = new AbsoluteTolerance(0.5); + var highResTolerance = new PpmTolerance(20); // Create CommonParameters with explicit tolerances var commonParameters = new CommonParameters( - productMassTolerance_LowRes: childScanTolerance, + productMassTolerance: highResTolerance, + productMassTolerance_LowRes: lowResTolerance, dissociationType: DissociationType.ETD, trimMsMsPeaks: false); - Assert.That(commonParameters.ProductMassTolerance_LowRes.GetRange(1000).Width, Is.EqualTo(childScanTolerance.GetRange(1000).Width)); + Assert.That(commonParameters.ProductMassTolerance_LowRes.GetRange(1000).Width, Is.EqualTo(lowResTolerance.GetRange(1000).Width)); + Assert.That(commonParameters.ProductMassTolerance.GetRange(1000).Width, Is.EqualTo(highResTolerance.GetRange(1000).Width)); // Sanity-check the parameter values were set string spectraFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\2019_09_16_StcEmix_35trig_EThcD25_rep1_4565.mgf"); @@ -122,12 +172,25 @@ public static void TestChildScanTolerance_Da() double theoreticalMass = ion.NeutralTheoreticalProduct.NeutralMass; // Verify matched ion is within child-scan tolerance Assert.That(commonParameters.ProductMassTolerance_LowRes.Within(matchedMass, theoreticalMass)); - Assert.That(childScanTolerance.Within(matchedMass, theoreticalMass)); + Assert.That(lowResTolerance.Within(matchedMass, theoreticalMass)); } + + var childMatchedIonsHighRes = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters, isLowRes: false); + Assert.That(childMatchedIonsHighRes.Count, Is.GreaterThan(0)); + foreach (var ion in childMatchedIonsHighRes) + { + double matchedMass = ion.Mz.ToMass(ion.Charge); + double theoreticalMass = ion.NeutralTheoreticalProduct.NeutralMass; + // Verify matched ion is within high-res tolerance + Assert.That(commonParameters.ProductMassTolerance.Within(matchedMass, theoreticalMass)); + Assert.That(highResTolerance.Within(matchedMass, theoreticalMass)); + } + // With a wider tolerance, we expect to match the same or more ions than with a narrower tolerance. + Assert.That(childMatchedIons.Count >= childMatchedIonsHighRes.Count); } [Test] - public static void TestChildScanTolerance_ppm() + public static void TestLowResTolerance_ppm() { // Purpose: // - Validate that fragment matching use `ChildScanMassTolerance` for child scans (isChildScan flag). @@ -135,14 +198,16 @@ public static void TestChildScanTolerance_ppm() // - For matched fragment ions, verify the observed m/z (converted to mass) is within the correct tolerance. // --- initialize tolerances for test - var childScanTolerance = new PpmTolerance(200); + var lowResTolerance = new PpmTolerance(200); + var highResTolerance = new PpmTolerance(20); // Create CommonParameters with explicit tolerances var commonParameters = new CommonParameters( - productMassTolerance_LowRes: childScanTolerance, + productMassTolerance: highResTolerance, + productMassTolerance_LowRes: lowResTolerance, dissociationType: DissociationType.ETD, trimMsMsPeaks: false); - Assert.That(commonParameters.ProductMassTolerance_LowRes.GetRange(1000).Width, Is.EqualTo(childScanTolerance.GetRange(1000).Width)); + Assert.That(commonParameters.ProductMassTolerance_LowRes.GetRange(1000).Width, Is.EqualTo(lowResTolerance.GetRange(1000).Width)); // Sanity-check the parameter values were set string spectraFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\2019_09_16_StcEmix_35trig_EThcD25_rep1_4565.mgf"); @@ -164,8 +229,120 @@ public static void TestChildScanTolerance_ppm() double theoreticalMass = ion.NeutralTheoreticalProduct.NeutralMass; // Verify matched ion is within child-scan tolerance Assert.That(commonParameters.ProductMassTolerance_LowRes.Within(matchedMass, theoreticalMass)); - Assert.That(childScanTolerance.Within(matchedMass, theoreticalMass)); + Assert.That(lowResTolerance.Within(matchedMass, theoreticalMass)); + } + + // Match fragments with narrow tolerance, and the expected matches should be satisfied high-res tolerance. + var childMatchedIonsHighRes = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters, isLowRes: false); + Assert.That(childMatchedIonsHighRes.Count, Is.GreaterThan(0)); + foreach (var ion in childMatchedIonsHighRes) + { + double matchedMass = ion.Mz.ToMass(ion.Charge); + double theoreticalMass = ion.NeutralTheoreticalProduct.NeutralMass; + // Verify matched ion is within child-scan tolerance + Assert.That(commonParameters.ProductMassTolerance.Within(matchedMass, theoreticalMass)); + Assert.That(highResTolerance.Within(matchedMass, theoreticalMass)); + } + + // With a wider tolerance, we expect to match the same or more ions than with a narrower tolerance. + Assert.That(childMatchedIons.Count >= childMatchedIonsHighRes.Count); + } + + + [Test] + public static void LowRes_CalibrateAndSearch() + { + // This test verifies that calibration-updated precursor/product tolerances are propagated to the subsequent GlycoSearch via the calibration-generated file-specific TOML, and confirms the GlycoSearch task config TOML is written with expected settings. + string outputRoot = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestCalibThenGlyco"); + Directory.CreateDirectory(outputRoot); + + try + { + // GlycoSearchEngine expects these DB names to exist in global path lists. + string oglycanPath = "OGlycan.gdb"; + string nglycanPath = "NGlycan_ForNoSearch.gdb"; + if (!GlobalVariables.OGlycanDatabasePaths.Contains(oglycanPath)) + { + GlobalVariables.OGlycanDatabasePaths.Add(oglycanPath); + } + + if (!GlobalVariables.NGlycanDatabasePaths.Contains(nglycanPath)) + { + GlobalVariables.NGlycanDatabasePaths.Add(nglycanPath); + } + + // Use a calibratable mzML for calibration. + string rawFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\SmallCalibratible_Yeast.mzML"); + string database = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\smalldb.fasta"); + + Tolerance originalPrecursorTolerance = new PpmTolerance(5); + Tolerance originalProductTolerance = new PpmTolerance(20); + + // 1) Calibration task + var calibrationTask = new CalibrationTask(); + calibrationTask.CommonParameters = new CommonParameters( + precursorMassTolerance: originalPrecursorTolerance, + productMassTolerance: originalProductTolerance); + + // 2) Glyco task (will consume calibrated file-specific toml generated by calibration) + var glycoTask = new GlycoSearchTask(); + glycoTask.CommonParameters = new CommonParameters( + precursorMassTolerance: originalPrecursorTolerance, + productMassTolerance: originalProductTolerance); + + var runner = new EverythingRunnerEngine( + new List<(string, MetaMorpheusTask)> + { + ("Calibration", calibrationTask), + ("Glyco", glycoTask) + }, + new List { rawFile }, + new List { new DbForTask(database, false) }, + outputRoot); + + runner.Run(); + + // Calibration writes calibrated mzML and a file-specific toml next to it. + string calibratedTomlPath = Path.Combine(outputRoot, "Calibration", "SmallCalibratible_Yeast-calib.toml"); + Assert.That(File.Exists(calibratedTomlPath), Is.True, "Calibration file-specific toml was not written."); + + // Parse calibrated tolerances from the calibration-generated file-specific toml. + TomlTable calibratedTable = Toml.ReadFile(calibratedTomlPath, MetaMorpheusTask.tomlConfig); + var calibratedFileSpecific = new FileSpecificParameters(calibratedTable); + Assert.That(calibratedFileSpecific.PrecursorMassTolerance, Is.Not.Null); + Assert.That(calibratedFileSpecific.ProductMassTolerance, Is.Not.Null); + + // Verify Glyco task consumed file-specific params from the calibrated file. + Assert.That(glycoTask.FileSpecificParameters, Is.Not.Null); + Assert.That(glycoTask.FileSpecificParameters.Count, Is.EqualTo(1)); + + // Verify Glyco task consumed file-specific params from the calibrated file. + Assert.That(glycoTask.FileSpecificParameters, Is.Not.Null); + Assert.That(glycoTask.FileSpecificParameters.Count, Is.EqualTo(1)); + + CommonParameters glycoFileSpecificParams = glycoTask.FileSpecificParameters[0].Parameters; + Assert.That(glycoFileSpecificParams.PrecursorMassTolerance.GetType(), Is.EqualTo(calibratedFileSpecific.PrecursorMassTolerance.GetType())); + Assert.That(glycoFileSpecificParams.ProductMassTolerance.GetType(), Is.EqualTo(calibratedFileSpecific.ProductMassTolerance.GetType())); + Assert.That(glycoFileSpecificParams.PrecursorMassTolerance.Value, Is.EqualTo(calibratedFileSpecific.PrecursorMassTolerance.Value).Within(1e-9)); + Assert.That(glycoFileSpecificParams.ProductMassTolerance.Value, Is.EqualTo(calibratedFileSpecific.ProductMassTolerance.Value).Within(1e-9)); + + // Low-res tolerance remains independent (default 0.35 Da unless explicitly set). + Assert.That(glycoFileSpecificParams.ProductMassTolerance_LowRes, Is.TypeOf()); + Assert.That(glycoFileSpecificParams.ProductMassTolerance_LowRes.Value, Is.EqualTo(0.35).Within(1e-9)); + + // Confirm Glyco task TOML is written. + string glycoTaskTomlPath = Path.Combine(outputRoot, "Task Settings", "Glycoconfig.toml"); + Assert.That(File.Exists(glycoTaskTomlPath), Is.True, "Glyco task config toml was not written."); + } + finally + { + if (Directory.Exists(outputRoot)) + { + Directory.Delete(outputRoot, true); + } } + } + } } diff --git a/MetaMorpheus/Test/GlycoTestData/GlycoSearchTaskconfigOGlycoTest_ToleranceTracking.toml b/MetaMorpheus/Test/GlycoTestData/GlycoSearchTaskconfigOGlycoTest_ToleranceTracking.toml new file mode 100644 index 0000000000..9591e7a89d --- /dev/null +++ b/MetaMorpheus/Test/GlycoTestData/GlycoSearchTaskconfigOGlycoTest_ToleranceTracking.toml @@ -0,0 +1,65 @@ +TaskType = "GlycoSearch" + +[_glycoSearchParameters] +OGlycanDatabasefile = "OGlycan.gdb" +NGlycanDatabasefile = "NGlycan.gdb" +GlycoSearchType = "OGlycanSearch" +OxoniumIonFilt = true +DecoyType = "Reverse" +GlycoSearchTopNum = 50 +MaximumOGlycanAllowed = 4 +DoParsimony = true +NoOneHitWonders = false +ModPeptidesAreDifferent = false +WriteIndividualFiles = false +WriteDecoys = true +WriteContaminants = true + +[CommonParameters] +TaskDescriptor = "GlycoSearchTask" +MaxThreadsToUsePerFile = 7 +ListOfModsFixed = "Common Fixed\tCarbamidomethyl on C\t\tCommon Fixed\tCarbamidomethyl on U" +ListOfModsVariable = "Common Variable\tOxidation on M" +DoPrecursorDeconvolution = true +UseProvidedPrecursorInfo = true +DeconvolutionIntensityRatio = 3.0 +DeconvolutionMaxAssumedChargeState = 12 +DeconvolutionMassTolerance = "±4.0000 PPM" +TotalPartitions = 1 +ProductMassTolerance = "±40.0000 PPM" +PrecursorMassTolerance = "±10.0000 PPM" +AddCompIons = false +ScoreCutoff = 3.0 +ReportAllAmbiguity = true +NumberOfPeaksToKeepPerWindow = 1000 +MinimumAllowedIntensityRatioToBasePeak = 0.01 +NormalizePeaksAccrossAllWindows = false +TrimMs1Peaks = false +TrimMsMsPeaks = false +UseDeltaScore = false +QValueOutputFilter = 1.0 +PepQValueOutputFilter = 1.0 +CustomIons = ["c", "zDot"] +AssumeOrphanPeaksAreZ1Fragments = true +MaxHeterozygousVariants = 4 +MinVariantDepth = 1 +AddTruncations = false +DissociationType = "EThcD" +SeparationType = "HPLC" +MS2ChildScanDissociationType = "Unknown" +MS3ChildScanDissociationType = "Unknown" + +[CommonParameters.DigestionParams] +MaxMissedCleavages = 5 +InitiatorMethionineBehavior = "Variable" +MinPeptideLength = 5 +MaxPeptideLength = 60 +MaxModificationIsoforms = 1024 +MaxModsForPeptide = 2 +Protease = "StcE-trypsin" +SearchModeType = "Full" +FragmentationTerminus = "Both" +SpecificProtease = "StcE-trypsin" +GeneratehUnlabeledProteinsForSilac = true +KeepNGlycopeptide = false +KeepOGlycopeptide = false diff --git a/MetaMorpheus/Test/Test.csproj b/MetaMorpheus/Test/Test.csproj index 7ca29075c6..e549c2bc55 100644 --- a/MetaMorpheus/Test/Test.csproj +++ b/MetaMorpheus/Test/Test.csproj @@ -106,6 +106,9 @@ Always + + Always + Always From bacfd44957e259ea8b689f3178fc0309f2863610 Mon Sep 17 00:00:00 2001 From: trishorts Date: Wed, 8 Apr 2026 09:03:24 -0500 Subject: [PATCH 11/12] couple fixes --- .../CMD/Properties/launchSettings.json | 11 +++++++++ MetaMorpheus/EngineLayer/CommonParameters.cs | 2 +- .../GlycoSearch/GlycoSearchEngine.cs | 6 ++--- .../EngineLayer/MetaMorpheusEngine.cs | 23 +++++++++++++++---- MetaMorpheus/TaskLayer/MetaMorpheusTask.cs | 2 -- MetaMorpheus/Test/GlycoSearchEngineTest.cs | 8 +++---- 6 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 MetaMorpheus/CMD/Properties/launchSettings.json diff --git a/MetaMorpheus/CMD/Properties/launchSettings.json b/MetaMorpheus/CMD/Properties/launchSettings.json new file mode 100644 index 0000000000..c92b8a2b35 --- /dev/null +++ b/MetaMorpheus/CMD/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "CMD": { + "commandName": "Project" + }, + "WSL": { + "commandName": "WSL2", + "distributionName": "" + } + } +} \ No newline at end of file diff --git a/MetaMorpheus/EngineLayer/CommonParameters.cs b/MetaMorpheus/EngineLayer/CommonParameters.cs index c6876c1d14..139edccc1c 100644 --- a/MetaMorpheus/EngineLayer/CommonParameters.cs +++ b/MetaMorpheus/EngineLayer/CommonParameters.cs @@ -86,7 +86,7 @@ public CommonParameters( MaxThreadsToUsePerFile = maxThreadsToUsePerFile == -1 ? Environment.ProcessorCount > 1 ? Environment.ProcessorCount - 1 : 1 : maxThreadsToUsePerFile; ProductMassTolerance = productMassTolerance ?? new PpmTolerance(20); PrecursorMassTolerance = precursorMassTolerance ?? new PpmTolerance(5); - ProductMassTolerance_LowRes = productMassTolerance_LowRes ?? new AbsoluteTolerance(0.35); + ProductMassTolerance_LowRes = productMassTolerance_LowRes ?? ProductMassTolerance; DeconvolutionMassTolerance = deconvolutionMassTolerance ?? new PpmTolerance(4); DigestionParams = digestionParams ?? new DigestionParams(); DissociationType = dissociationType; diff --git a/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs b/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs index f1c31a60cc..9b4b418d13 100644 --- a/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs +++ b/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs @@ -323,7 +323,8 @@ private GlycoSpectralMatch CreateGsm(Ms2ScanWithSpecificMass theScan, int scanIn var PeptideScore = score - DiagnosticIonScore; - var p = theScan.TheScan.MassSpectrum.Size * CommonParameters.ProductMassTolerance.GetRange(1000).Width / theScan.TheScan.MassSpectrum.Range.Width; + var parentProductTolerance = IsLowResolutionScan(theScan) ? CommonParameters.ProductMassTolerance_LowRes : CommonParameters.ProductMassTolerance; + var p = theScan.TheScan.MassSpectrum.Size * parentProductTolerance.GetRange(1000).Width / theScan.TheScan.MassSpectrum.Range.Width; int n = fragmentsForEachGlycoPeptide.Where(v => v.ProductType == ProductType.c || v.ProductType == ProductType.zDot).Count(); @@ -332,8 +333,7 @@ private GlycoSpectralMatch CreateGsm(Ms2ScanWithSpecificMass theScan, int scanIn foreach (var childScan in theScan.ChildScans) { var childFragments = GlycoPeptides.OGlyGetTheoreticalFragments(CommonParameters.MS2ChildScanDissociationType, CommonParameters.CustomIons, peptide, peptideWithMod); - bool isIonTrapData = childScan.TheScan.MzAnalyzer == MZAnalyzerType.IonTrap2D || childScan.TheScan.MzAnalyzer == MZAnalyzerType.IonTrap3D; - var matchedChildIons = MatchFragmentIons(childScan, childFragments, CommonParameters, isLowRes : isIonTrapData); + var matchedChildIons = MatchFragmentIons(childScan, childFragments, CommonParameters); n += childFragments.Where(v => v.ProductType == ProductType.c || v.ProductType == ProductType.zDot).Count(); diff --git a/MetaMorpheus/EngineLayer/MetaMorpheusEngine.cs b/MetaMorpheus/EngineLayer/MetaMorpheusEngine.cs index cd40266f77..71960e8c71 100644 --- a/MetaMorpheus/EngineLayer/MetaMorpheusEngine.cs +++ b/MetaMorpheus/EngineLayer/MetaMorpheusEngine.cs @@ -128,13 +128,14 @@ private static double CalculateAllChargesPeptideScore(MsDataScan thisScan, List< } - public static List MatchFragmentIons(Ms2ScanWithSpecificMass scan, List theoreticalProducts, CommonParameters commonParameters, bool matchAllCharges = false, bool isLowRes = false) + public static List MatchFragmentIons(Ms2ScanWithSpecificMass scan, List theoreticalProducts, CommonParameters commonParameters, bool matchAllCharges = false) { - // if this is a child scan and it's an ion trap 2D scan, we want to use the wider tolerance for matching - var productMassTolerance = isLowRes? commonParameters.ProductMassTolerance_LowRes : commonParameters.ProductMassTolerance; + // auto-detect low-res scans from mass analyzer metadata and use wider tolerance for ion trap child scans + bool isLowRes = IsLowResolutionScan(scan); + var productMassTolerance = isLowRes ? commonParameters.ProductMassTolerance_LowRes : commonParameters.ProductMassTolerance; if (matchAllCharges) { - return MatchFragmentIonsOfAllCharges(scan, theoreticalProducts, commonParameters, isLowRes); + return MatchFragmentIonsOfAllCharges(scan, theoreticalProducts, commonParameters); } var matchedFragmentIons = new List(); @@ -231,8 +232,9 @@ public static List MatchFragmentIons(Ms2ScanWithSpecificMass //Used only when user wants to generate spectral library. //Normal search only looks for one match ion for one fragment, and if it accepts it then it doesn't try to look for different charge states of that same fragment. //But for library generation, we need find all the matched peaks with all the different charges. - private static List MatchFragmentIonsOfAllCharges(Ms2ScanWithSpecificMass scan, List theoreticalProducts, CommonParameters commonParameters, bool isLowRes = false) + private static List MatchFragmentIonsOfAllCharges(Ms2ScanWithSpecificMass scan, List theoreticalProducts, CommonParameters commonParameters) { + bool isLowRes = IsLowResolutionScan(scan); var productMassTolerance = isLowRes ? commonParameters.ProductMassTolerance_LowRes : commonParameters.ProductMassTolerance; var matchedFragmentIons = new List(); var ions = new List(); @@ -278,6 +280,17 @@ private static List MatchFragmentIonsOfAllCharges(Ms2ScanWit return matchedFragmentIons; } + + /// + /// Determines whether the given scan was acquired on a low-resolution mass analyzer (e.g., ion trap). + /// Used to select the appropriate product mass tolerance for fragment ion matching. + /// + internal static bool IsLowResolutionScan(Ms2ScanWithSpecificMass scan) + { + var analyzer = scan.TheScan.MzAnalyzer; + return analyzer == MZAnalyzerType.IonTrap2D || analyzer == MZAnalyzerType.IonTrap3D; + } + protected abstract MetaMorpheusEngineResults RunSpecific(); public MetaMorpheusEngineResults Run() diff --git a/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs b/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs index 360c0c81d2..0e70b3f8eb 100644 --- a/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs +++ b/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs @@ -1,4 +1,3 @@ -using Chemistry; using EngineLayer; using EngineLayer.Indexing; using MassSpectrometry; @@ -29,7 +28,6 @@ using Transcriptomics.Digestion; using EngineLayer.Util; using EngineLayer.DIA; -using EngineLayer.SpectrumMatch; using Omics.Fragmentation; namespace TaskLayer diff --git a/MetaMorpheus/Test/GlycoSearchEngineTest.cs b/MetaMorpheus/Test/GlycoSearchEngineTest.cs index 540d25483b..461df02698 100644 --- a/MetaMorpheus/Test/GlycoSearchEngineTest.cs +++ b/MetaMorpheus/Test/GlycoSearchEngineTest.cs @@ -164,7 +164,7 @@ public static void TestLowResTolerance_Da() // Match fragments treating the scan as a child scan (isChildScan = true). // Expect matches to satisfy child scan tolerance (potentially wider). - var childMatchedIons = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters, isLowRes: true); + var childMatchedIons = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters); Assert.That(childMatchedIons.Count, Is.GreaterThan(0)); foreach (var ion in childMatchedIons) { @@ -175,7 +175,7 @@ public static void TestLowResTolerance_Da() Assert.That(lowResTolerance.Within(matchedMass, theoreticalMass)); } - var childMatchedIonsHighRes = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters, isLowRes: false); + var childMatchedIonsHighRes = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters); Assert.That(childMatchedIonsHighRes.Count, Is.GreaterThan(0)); foreach (var ion in childMatchedIonsHighRes) { @@ -221,7 +221,7 @@ public static void TestLowResTolerance_ppm() // Match fragments treating the scan as a child scan (isChildScan = true). // Expect matches to satisfy child scan tolerance (potentially wider). - var childMatchedIons = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters, isLowRes: true); + var childMatchedIons = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters); Assert.That(childMatchedIons.Count, Is.GreaterThan(0)); foreach (var ion in childMatchedIons) { @@ -233,7 +233,7 @@ public static void TestLowResTolerance_ppm() } // Match fragments with narrow tolerance, and the expected matches should be satisfied high-res tolerance. - var childMatchedIonsHighRes = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters, isLowRes: false); + var childMatchedIonsHighRes = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters); Assert.That(childMatchedIonsHighRes.Count, Is.GreaterThan(0)); foreach (var ion in childMatchedIonsHighRes) { From 7058cee327a162f4978f47f5db7053f4ecf42be7 Mon Sep 17 00:00:00 2001 From: trishorts Date: Wed, 8 Apr 2026 12:29:21 -0500 Subject: [PATCH 12/12] fix rays bugs --- .../GlycoSearch/GlycoSearchEngine.cs | 2 +- .../TaskWindows/GlycoSearchTaskWindow.xaml.cs | 11 ++-- MetaMorpheus/GUI/Util/TaskValidator.cs | 60 +------------------ MetaMorpheus/Test/GlycoSearchEngineTest.cs | 25 ++++---- 4 files changed, 24 insertions(+), 74 deletions(-) diff --git a/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs b/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs index 9b4b418d13..3648d4ffe4 100644 --- a/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs +++ b/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs @@ -355,7 +355,7 @@ private GlycoSpectralMatch CreateGsm(Ms2ScanWithSpecificMass theScan, int scanIn //TO THINK:may think a different way to use childScore score += childScore; - var productTolerance = isIonTrapData ? CommonParameters.ProductMassTolerance_LowRes : CommonParameters.ProductMassTolerance; + var productTolerance = IsLowResolutionScan(childScan) ? CommonParameters.ProductMassTolerance_LowRes : CommonParameters.ProductMassTolerance; p += childScan.TheScan.MassSpectrum.Size * productTolerance.GetRange(1000).Width / childScan.TheScan.MassSpectrum.Range.Width; } diff --git a/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml.cs b/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml.cs index 73c1de3df7..00d6eee99c 100644 --- a/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml.cs +++ b/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml.cs @@ -187,7 +187,7 @@ private void UpdateFieldsFromTask(GlycoSearchTask task) productMassToleranceTextBox.Text = task.CommonParameters.ProductMassTolerance.Value.ToString(CultureInfo.InvariantCulture); productMassToleranceComboBox.SelectedIndex = task.CommonParameters.ProductMassTolerance is AbsoluteTolerance ? 0 : 1; productMassTolerance_LowResTextBox.Text = task.CommonParameters.ProductMassTolerance_LowRes?.Value.ToString(CultureInfo.InvariantCulture); - productMassTolerance_LowResComboBox.SelectedIndex = task.CommonParameters.ProductMassTolerance_LowRes is AbsoluteTolerance ? 0 : 1; + productMassTolerance_LowResComboBox.SelectedIndex = task.CommonParameters.ProductMassTolerance_LowRes == null || task.CommonParameters.ProductMassTolerance_LowRes is AbsoluteTolerance ? 0 : 1; minScoreAllowed.Text = task.CommonParameters.ScoreCutoff.ToString(CultureInfo.InvariantCulture); numberOfDatabaseSearchesTextBox.Text = task.CommonParameters.TotalPartitions.ToString(CultureInfo.InvariantCulture); maxThreadsTextBox.Text = task.CommonParameters.MaxThreadsToUsePerFile.ToString(CultureInfo.InvariantCulture); @@ -261,10 +261,11 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) { string fieldNotUsed = "1"; - if (!TaskValidator.CheckTaskSettingsValidity(PrecusorMsTlTextBox.Text, productMassToleranceTextBox.Text, productMassTolerance_LowResTextBox.Text, missedCleavagesTextBox.Text, + if (!TaskValidator.CheckTaskSettingsValidity(PrecusorMsTlTextBox.Text, productMassToleranceTextBox.Text, missedCleavagesTextBox.Text, maxModificationIsoformsTextBox.Text, MinPeptideLengthTextBox.Text, MaxPeptideLengthTextBox.Text, maxThreadsTextBox.Text, minScoreAllowed.Text, - fieldNotUsed, fieldNotUsed, fieldNotUsed, DeconHostViewModel.PrecursorDeconvolutionParameters.MaxAssumedChargeState.ToString(), TopNPeaksTextBox.Text, MinRatioTextBox.Text, null, null, numberOfDatabaseSearchesTextBox.Text, TxtBoxMaxModPerPep.Text, - fieldNotUsed, null, null, null)) + fieldNotUsed, fieldNotUsed, fieldNotUsed, DeconHostViewModel.PrecursorDeconvolutionParameters.MaxAssumedChargeState.ToString(), TopNPeaksTextBox.Text, MinRatioTextBox.Text, null, null, numberOfDatabaseSearchesTextBox.Text, TxtBoxMaxModPerPep.Text, + fieldNotUsed, null, null, null, + productMassTolerance_LowRes: productMassTolerance_LowResTextBox.Text)) { return; } @@ -371,7 +372,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) if (string.IsNullOrWhiteSpace(productMassTolerance_LowResToleranceText)) { // If no child scan mass tolerance is specified, fall back to product mass tolerance - ProductMassTolerance_lowRes = new AbsoluteTolerance(0.35); + ProductMassTolerance_lowRes = ProductMassTolerance; } else { diff --git a/MetaMorpheus/GUI/Util/TaskValidator.cs b/MetaMorpheus/GUI/Util/TaskValidator.cs index 4da4165e92..c15690777c 100644 --- a/MetaMorpheus/GUI/Util/TaskValidator.cs +++ b/MetaMorpheus/GUI/Util/TaskValidator.cs @@ -1,5 +1,4 @@ -using Org.BouncyCastle.Tls; -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; @@ -12,65 +11,11 @@ namespace MetaMorpheusGUI /// public static class TaskValidator { - /// - /// Backward-compatible overload without low-res tolerance parameter. - /// - public static bool CheckTaskSettingsValidity( - string precursorMassTolerance, - string productMassTolerance, - string maxMissedCleavages, - string maxModificationIsoforms, - string minPeptideLength, - string maxPeptideLength, - string maxThreads, - string minScore, - string peakFindingTolerance, - string mbrFdrThreshold, - string histogramBinWidth, - string deconMaxAssumedCharge, - string numberOfPeaksToKeepPerWindow, - string minimumAllowedIntensityRatio, - string windowWidthThomsons, - string numberOfWindows, - string numberOfDatabaseSearches, - string maxModsPerPeptide, - string maxFragmentMass, - string qValueFilter, - string pepqValueFilter, - string minInternalIonLength) - { - return CheckTaskSettingsValidity( - precursorMassTolerance: precursorMassTolerance, - productMassTolerance: productMassTolerance, - productMassTolerance_LowRes: null, - maxMissedCleavages: maxMissedCleavages, - maxModificationIsoforms: maxModificationIsoforms, - minPeptideLength: minPeptideLength, - maxPeptideLength: maxPeptideLength, - maxThreads: maxThreads, - minScore: minScore, - peakFindingTolerance: peakFindingTolerance, - mbrFdrThreshold: mbrFdrThreshold, - histogramBinWidth: histogramBinWidth, - deconMaxAssumedCharge: deconMaxAssumedCharge, - numberOfPeaksToKeepPerWindow: numberOfPeaksToKeepPerWindow, - minimumAllowedIntensityRatio: minimumAllowedIntensityRatio, - windowWidthThomsons: windowWidthThomsons, - numberOfWindows: numberOfWindows, - numberOfDatabaseSearches: numberOfDatabaseSearches, - maxModsPerPeptide: maxModsPerPeptide, - maxFragmentMass: maxFragmentMass, - qValueFilter: qValueFilter, - pepqValueFilter: pepqValueFilter, - minInternalIonLength: minInternalIonLength); - } - /// /// Checks the validity of each setting passed from the GUI task windows /// public static bool CheckTaskSettingsValidity(string precursorMassTolerance, string productMassTolerance, - string productMassTolerance_LowRes, string maxMissedCleavages, string maxModificationIsoforms, string minPeptideLength, @@ -90,7 +35,8 @@ public static bool CheckTaskSettingsValidity(string precursorMassTolerance, string maxFragmentMass, string qValueFilter, string pepqValueFilter, - string minInternalIonLength + string minInternalIonLength, + string productMassTolerance_LowRes = null ) { maxMissedCleavages = MaxValueConversion(maxMissedCleavages); diff --git a/MetaMorpheus/Test/GlycoSearchEngineTest.cs b/MetaMorpheus/Test/GlycoSearchEngineTest.cs index 461df02698..9057570ea7 100644 --- a/MetaMorpheus/Test/GlycoSearchEngineTest.cs +++ b/MetaMorpheus/Test/GlycoSearchEngineTest.cs @@ -79,9 +79,9 @@ public static void TestLowResToleranceConstruction_Default() var commonParameters = new CommonParameters(); Assert.That(commonParameters.ProductMassTolerance_LowRes, Is.Not.Null, "ChildScanMassTolerance should be initialized by default."); - // Default product tolerance is a AbsoluteTolerance of 0.35 Da; verify type and numeric width equivalence - Assert.That(commonParameters.ProductMassTolerance_LowRes, Is.TypeOf()); - Assert.That(commonParameters.ProductMassTolerance_LowRes.GetRange(1000).Width, Is.EqualTo(new AbsoluteTolerance(0.35).GetRange(1000).Width)); + // Default low-res tolerance inherits from ProductMassTolerance (PpmTolerance 20 ppm) + Assert.That(commonParameters.ProductMassTolerance_LowRes, Is.TypeOf()); + Assert.That(commonParameters.ProductMassTolerance_LowRes.Value, Is.EqualTo(commonParameters.ProductMassTolerance.Value)); } [Test] @@ -97,11 +97,13 @@ public static void TestLowResToleranceConstruction_Default2() Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\GlycoSearchTaskconfigOGlycoTest_ToleranceTracking.toml"), MetaMorpheusTask.tomlConfig); - // Confirm read-in behavior (tracking expected when low-res omitted) + // Confirm read-in behavior: ProductMassTolerance is set to 40 ppm from TOML. + // Low-res inherits from the default ProductMassTolerance (20 ppm) at construction time, + // since TOML deserialization sets ProductMassTolerance via the public setter after construction. Assert.That(inputTask.CommonParameters.ProductMassTolerance, Is.TypeOf()); Assert.That(inputTask.CommonParameters.ProductMassTolerance.Value, Is.EqualTo(40).Within(1e-9)); - Assert.That(inputTask.CommonParameters.ProductMassTolerance_LowRes, Is.TypeOf()); - Assert.That(inputTask.CommonParameters.ProductMassTolerance_LowRes.Value, Is.EqualTo(0.35)); + Assert.That(inputTask.CommonParameters.ProductMassTolerance_LowRes, Is.TypeOf()); + Assert.That(inputTask.CommonParameters.ProductMassTolerance_LowRes.Value, Is.EqualTo(20).Within(1e-9)); DbForTask db = new(Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\P16150.fasta"), false); string spectraFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\2019_09_16_StcEmix_35trig_EThcD25_rep1_9906.mgf"); @@ -120,8 +122,8 @@ public static void TestLowResToleranceConstruction_Default2() var writtenTask = Toml.ReadFile(writtenTaskToml, MetaMorpheusTask.tomlConfig); Assert.That(writtenTask.CommonParameters.ProductMassTolerance, Is.TypeOf()); Assert.That(writtenTask.CommonParameters.ProductMassTolerance.Value, Is.EqualTo(40).Within(1e-9)); - Assert.That(writtenTask.CommonParameters.ProductMassTolerance_LowRes, Is.TypeOf()); - Assert.That(writtenTask.CommonParameters.ProductMassTolerance_LowRes.Value, Is.EqualTo(0.35)); + Assert.That(writtenTask.CommonParameters.ProductMassTolerance_LowRes, Is.TypeOf()); + Assert.That(writtenTask.CommonParameters.ProductMassTolerance_LowRes.Value, Is.EqualTo(20).Within(1e-9)); } finally { @@ -326,9 +328,10 @@ public static void LowRes_CalibrateAndSearch() Assert.That(glycoFileSpecificParams.PrecursorMassTolerance.Value, Is.EqualTo(calibratedFileSpecific.PrecursorMassTolerance.Value).Within(1e-9)); Assert.That(glycoFileSpecificParams.ProductMassTolerance.Value, Is.EqualTo(calibratedFileSpecific.ProductMassTolerance.Value).Within(1e-9)); - // Low-res tolerance remains independent (default 0.35 Da unless explicitly set). - Assert.That(glycoFileSpecificParams.ProductMassTolerance_LowRes, Is.TypeOf()); - Assert.That(glycoFileSpecificParams.ProductMassTolerance_LowRes.Value, Is.EqualTo(0.35).Within(1e-9)); + // Low-res tolerance inherits from the original ProductMassTolerance at construction (20 ppm), + // not the calibration-narrowed value, since calibration does not update low-res tolerance. + Assert.That(glycoFileSpecificParams.ProductMassTolerance_LowRes, Is.TypeOf()); + Assert.That(glycoFileSpecificParams.ProductMassTolerance_LowRes.Value, Is.EqualTo(originalProductTolerance.Value).Within(1e-9)); // Confirm Glyco task TOML is written. string glycoTaskTomlPath = Path.Combine(outputRoot, "Task Settings", "Glycoconfig.toml");