diff --git a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs index 10c5ba7409..6ac1822715 100644 --- a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs +++ b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs @@ -1,5 +1,4 @@ using MzLibUtil; -using Proteomics.ProteolyticDigestion; using System; using System.Collections.Generic; using System.Linq; @@ -9,6 +8,7 @@ using System.Collections.Concurrent; using MassSpectrometry; using Omics.Fragmentation; +using System.Threading; namespace EngineLayer.Gptmd { @@ -22,6 +22,7 @@ public class GptmdEngine : MetaMorpheusEngine //The ScoreTolerance property is used to differentiatie when a PTM candidate is added to a peptide. We check the score at each position and then add that mod where the score is highest. private readonly double ScoreTolerance = 0.1; public Dictionary>> ModDictionary { get; init; } + private readonly List Filters; public GptmdEngine( List allIdentifications, @@ -31,7 +32,8 @@ public GptmdEngine( CommonParameters commonParameters, List<(string fileName, CommonParameters fileSpecificParameters)> fileSpecificParameters, List nestedIds, - Dictionary>> modDictionary) + Dictionary>> modDictionary, + List filters = null) : base(commonParameters, fileSpecificParameters, nestedIds) { AllIdentifications = allIdentifications; @@ -39,6 +41,7 @@ public GptmdEngine( Combos = combos; FilePathToPrecursorMassTolerance = filePathToPrecursorMassTolerance; ModDictionary = modDictionary ?? new Dictionary>>(); + Filters = filters ?? new List(); } public static bool ModFits(Modification attemptToLocalize, IBioPolymer protein, int peptideOneBasedIndex, int peptideLength, int proteinOneBasedIndex) @@ -90,99 +93,113 @@ protected override MetaMorpheusEngineResults RunSpecific() { for (int i = range.Item1; i < range.Item2; i++) { - foreach (var pepWithSetMods in psms[i].BestMatchingBioPolymersWithSetMods.Select(v => v.SpecificBioPolymer)) + // Extract all necessary information from the PSM + var psm = psms[i]; + var dissociationType = CommonParameters.DissociationType == DissociationType.Autodetect ? + psms[i].Ms2Scan.DissociationType.Value : CommonParameters.DissociationType; + var scan = psm.Ms2Scan; + var precursorMass = psm.ScanPrecursorMass; + var precursorCharge = psm.ScanPrecursorCharge; + var fileName = psm.FullFilePath; + var originalScore = psm.Score; + Ms2ScanWithSpecificMass ms2ScanWithSpecificMass = null; + var peptideTheorProducts = new List(); + List<(int site, Modification mod, string proteinAccession)> bestMatches = []; + + foreach (var pepWithSetMods in psm.BestMatchingBioPolymersWithSetMods.Select(v => v.SpecificBioPolymer)) { + bestMatches.Clear(); var isVariantProtein = pepWithSetMods.Parent != pepWithSetMods.Parent.ConsensusVariant; - var possibleModifications = GetPossibleMods(psms[i].ScanPrecursorMass, GptmdModifications, Combos, FilePathToPrecursorMassTolerance[psms[i].FullFilePath], pepWithSetMods); + var possibleModifications = GetPossibleMods(precursorMass, GptmdModifications, Combos, + FilePathToPrecursorMassTolerance[fileName], pepWithSetMods); - if (!isVariantProtein) + double bestScore = originalScore; // Initialize with the original score of the PSM to ensure gptmd only adds if new modified forms scores higher + + foreach (var mod in possibleModifications) { - foreach (var mod in possibleModifications) + if (!mod.MonoisotopicMass.HasValue) + continue; + + for (int j = 0; j < pepWithSetMods.Length; j++) { - List possibleIndices = Enumerable.Range(0, pepWithSetMods.Length).Where(i => ModFits(mod, pepWithSetMods.Parent, i + 1, pepWithSetMods.Length, pepWithSetMods.OneBasedStartResidue + i)).ToList(); - if (possibleIndices.Any()) + int indexInProtein = pepWithSetMods.OneBasedStartResidue + j; + if (!ModFits(mod, pepWithSetMods.Parent, j + 1, pepWithSetMods.Length, indexInProtein)) + continue; + + var newPep = pepWithSetMods.Localize(j, mod.MonoisotopicMass.Value); + peptideTheorProducts.Clear(); + newPep.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts); + + ms2ScanWithSpecificMass ??= new Ms2ScanWithSpecificMass(scan, precursorMass, precursorCharge, fileName, CommonParameters); + var matchedIons = MatchFragmentIons(ms2ScanWithSpecificMass, peptideTheorProducts, CommonParameters, matchAllCharges: false); + double score = CalculatePeptideScore(scan, matchedIons, false); + + // plus 2 is to translate from zero based string array index to OneBasedModification index + int modSite = pepWithSetMods.OneBasedStartResidue + j; + if (!Filters.All(f => f.Passes(newPep, psm, score, originalScore, matchedIons, j + 2, pepWithSetMods.Length, mod))) + continue; + + if (score < bestScore - ScoreTolerance) + continue; + + // resolve variant protein location + string accession; + int adjustedSite = modSite; + if (!isVariantProtein) + { + accession = pepWithSetMods.Parent.Accession; + } + else { - List newPeptides = new(); - foreach (int index in possibleIndices) + accession = null; + int offset = 0; + foreach (var variant in pepWithSetMods.Parent.AppliedSequenceVariations.OrderBy(v => v.OneBasedBeginPosition)) { - if (mod.MonoisotopicMass.HasValue) + bool modIsBeforeVariant = indexInProtein < variant.OneBasedBeginPosition + offset; + bool modIsOnVariant = variant.OneBasedBeginPosition + offset <= indexInProtein && + indexInProtein <= variant.OneBasedEndPosition + offset; + + if (modIsOnVariant) { - newPeptides.Add(pepWithSetMods.Localize(index, mod.MonoisotopicMass.Value)); + accession = pepWithSetMods.Parent.Accession; + break; } - } - if (newPeptides.Any()) - { - var scores = new List(); - var dissociationType = CommonParameters.DissociationType == DissociationType.Autodetect ? - psms[i].Ms2Scan.DissociationType.Value : CommonParameters.DissociationType; - - scores = CalculatePeptideScores(newPeptides, dissociationType, psms[i]); - - // If the score is within tolerance of the highest score, add the mod to the peptide - // If the tolerance is too tight, then the number of identifications in subsequent searches will be reduced - - var highScoreIndices = scores.Select((item, index) => new { item, index }) - .Where(x => x.item > (scores.Max() - ScoreTolerance)) - .Select(x => x.index) - .ToList(); - - foreach (var index in highScoreIndices) + if (modIsBeforeVariant) { - AddIndexedMod(modDict, pepWithSetMods.Parent.Accession, new Tuple(pepWithSetMods.OneBasedStartResidue + possibleIndices[index], mod)); - System.Threading.Interlocked.Increment(ref modsAdded); ; + accession = pepWithSetMods.Parent.ConsensusVariant.Accession; + adjustedSite = indexInProtein - offset; + break; } + + offset += variant.VariantSequence.Length - variant.OriginalSequence.Length; } - } - } - } - // if a variant protein, index to variant protein if on variant, or to the original protein if not - else - { - foreach (var mod in possibleModifications) - { - for (int j = 0; j < pepWithSetMods.Length; j++) - { - int indexInProtein = pepWithSetMods.OneBasedStartResidue + j; - if (ModFits(mod, pepWithSetMods.Parent, j + 1, pepWithSetMods.Length, indexInProtein)) + if (accession == null) { - bool foundSite = false; - int offset = 0; - foreach (var variant in pepWithSetMods.Parent.AppliedSequenceVariations.OrderBy(v => v.OneBasedBeginPosition)) - { - bool modIsBeforeVariant = indexInProtein < variant.OneBasedBeginPosition + offset; - bool modIsOnVariant = variant.OneBasedBeginPosition + offset <= indexInProtein && indexInProtein <= variant.OneBasedEndPosition + offset; - - // if a variant protein and the mod is on the variant, index to the variant protein sequence - if (modIsOnVariant) - { - AddIndexedMod(modDict, pepWithSetMods.Parent.Accession, new Tuple(indexInProtein, mod)); - foundSite = true; - System.Threading.Interlocked.Increment(ref modsAdded); ; - break; - } - - // otherwise back calculate the index to the original protein sequence - if (modIsBeforeVariant) - { - AddIndexedMod(modDict, pepWithSetMods.Parent.ConsensusVariant.Accession, new Tuple(indexInProtein - offset, mod)); - foundSite = true; - System.Threading.Interlocked.Increment(ref modsAdded); ; - break; - } - - offset += variant.VariantSequence.Length - variant.OriginalSequence.Length; - } - if (!foundSite) - { - AddIndexedMod(modDict, pepWithSetMods.Parent.ConsensusVariant.Accession, new Tuple(indexInProtein - offset, mod)); - System.Threading.Interlocked.Increment(ref modsAdded); ; - } + accession = pepWithSetMods.Parent.ConsensusVariant.Accession; + adjustedSite = indexInProtein - offset; } } + + if (score > bestScore + ScoreTolerance) // new high score, reset list + { + bestScore = score; + bestMatches.Clear(); + bestMatches.Add((adjustedSite, mod, accession)); + } + else if (Math.Abs(score - bestScore) <= ScoreTolerance) + { + bestMatches.Add((adjustedSite, mod, accession)); + } } } + + foreach (var match in bestMatches) + { + AddIndexedMod(modDict, match.proteinAccession, new Tuple(match.site, match.mod)); + Interlocked.Increment(ref modsAdded); + } } } }); @@ -193,27 +210,6 @@ protected override MetaMorpheusEngineResults RunSpecific() return new GptmdResults(this, ModDictionary, modsAdded); } - private List CalculatePeptideScores(List newPeptides, DissociationType dissociationType, SpectralMatch psm) - { - var scores = new List(); - - foreach (var peptide in newPeptides) - { - var peptideTheorProducts = new List(); - peptide.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts); - - var scan = psm.Ms2Scan; - var precursorMass = psm.ScanPrecursorMass; - var precursorCharge = psm.ScanPrecursorCharge; - var fileName = psm.FullFilePath; - List matchedIons = MatchFragmentIons(new Ms2ScanWithSpecificMass(scan, precursorMass, precursorCharge, fileName, CommonParameters), peptideTheorProducts, CommonParameters, matchAllCharges: false); - - scores.Add(CalculatePeptideScore(scan, matchedIons, false)); - } - - return scores; - } - private static void AddIndexedMod(ConcurrentDictionary>> modDict, string proteinAccession, Tuple indexedMod) { modDict.AddOrUpdate(proteinAccession, diff --git a/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs b/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs new file mode 100644 index 0000000000..66d95e0072 --- /dev/null +++ b/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs @@ -0,0 +1,161 @@ +#nullable enable +using System; +using Omics.Fragmentation; +using System.Collections.Generic; +using Omics; +using System.Linq; +using Omics.Modifications; + +namespace EngineLayer; + +public interface IGptmdFilter : IEquatable +{ + public static string GetFilterTypeName(IGptmdFilter filter) => filter.GetType().Name; + + bool Passes( + IBioPolymerWithSetMods candidatePeptide, + SpectralMatch psm, + double newScore, + double originalScore, + List matchedIons, + int peptideOneBasedModSite, + int peptideLength, + Modification modAttemptingToAdd); + + bool IEquatable.Equals(IGptmdFilter? other) + { + return other != null && GetType() == other.GetType(); + } +} + +/// +/// Requires that the new score is greater than the original score. +/// +public sealed class ImprovedScoreFilter : IGptmdFilter +{ + public bool Passes( + IBioPolymerWithSetMods candidatePeptide, + SpectralMatch psm, + double newScore, + double originalScore, + List matchedIons, + int peptideOneBasedModSite, + int peptideLength, + Modification modAttemptingToAdd) + { + return newScore > originalScore; + } +} + +/// +/// Requires the mod site to be covered by at least one N-terminal and one C-terminal ion. +/// That is, ions from both directions must include the mod, even if not flanking it. +/// +public sealed class DualDirectionalIonCoverageFilter : IGptmdFilter +{ + public bool Passes( + IBioPolymerWithSetMods candidatePeptide, + SpectralMatch psm, + double newScore, + double originalScore, + List matchedIons, + int peptideOneBasedModSite, + int peptideLength, + Modification modAttemptingToAdd) + { + if (matchedIons == null || matchedIons.Count == 0) + return false; + + int site = peptideOneBasedModSite; + + bool coveredFromNTerm = matchedIons.Any(m => + m.NeutralTheoreticalProduct.ProductType == ProductType.M || + (m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.N or FragmentationTerminus.FivePrime && + m.NeutralTheoreticalProduct.ResiduePosition >= site) + ); + + bool coveredFromCTerm = matchedIons.Any(m => + m.NeutralTheoreticalProduct.ProductType == ProductType.M || + (m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.C or FragmentationTerminus.ThreePrime && + m.NeutralTheoreticalProduct.ResiduePosition < site) + ); + + if (modAttemptingToAdd.LocationRestriction.Contains("terminal", StringComparison.InvariantCultureIgnoreCase)) + return coveredFromCTerm || coveredFromNTerm; + + return coveredFromNTerm && coveredFromCTerm; + } +} + +/// +/// Requires the mod site to be covered by at least one N-terminal or one C-terminal ion. +/// That is, ions from one direction must include the mod, even if not flanking it. +/// +public sealed class UniDirectionalIonCoverageFilter : IGptmdFilter +{ + public bool Passes( + IBioPolymerWithSetMods candidatePeptide, + SpectralMatch psm, + double newScore, + double originalScore, + List matchedIons, + int peptideOneBasedModSite, + int peptideLength, + Modification modAttemptingToAdd) + { + if (matchedIons == null || matchedIons.Count == 0) + return false; + + int site = peptideOneBasedModSite; + + bool coveredFromNTerm = matchedIons.Any(m => + m.NeutralTheoreticalProduct.ProductType == ProductType.M || + (m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.N or FragmentationTerminus.FivePrime && + m.NeutralTheoreticalProduct.ResiduePosition >= site) + ); + + bool coveredFromCTerm = matchedIons.Any(m => + m.NeutralTheoreticalProduct.ProductType == ProductType.M || + (m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.C or FragmentationTerminus.ThreePrime && + m.NeutralTheoreticalProduct.ResiduePosition < site) + ); + + return coveredFromNTerm || coveredFromCTerm; + } +} + +/// +/// Requires flanking ions — a fragment from *before* and one from *after* the mod site, +/// regardless of fragmentation direction. +/// +public sealed class FlankingIonCoverageFilter : IGptmdFilter +{ + // TODO: Consider N-terminal and C-terminal mods as special cases, where the flanking ion not possible except for the M ion. + public bool Passes( + IBioPolymerWithSetMods candidatePeptide, + SpectralMatch psm, + double newScore, + double originalScore, + List matchedIons, + int peptideOneBasedModSite, + int peptideLength, + Modification modAttemptingToAdd) + { + if (matchedIons == null || matchedIons.Count == 0) + return false; + + int site = peptideOneBasedModSite; + + bool leftFlank = matchedIons.Any(m => + m.NeutralTheoreticalProduct.ResiduePosition == site - 1); + + bool rightFlank = matchedIons.Any(m => + m.NeutralTheoreticalProduct.ResiduePosition == site); + + if (modAttemptingToAdd.LocationRestriction.Contains("terminal", StringComparison.InvariantCultureIgnoreCase)) + return leftFlank || rightFlank; + + return leftFlank && rightFlank; + } +} + diff --git a/MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml b/MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml index 2341862a1c..f3a252a0e1 100644 --- a/MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml +++ b/MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml @@ -441,7 +441,21 @@ - + + + + + + + + + + + + + diff --git a/MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml.cs b/MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml.cs index 5693f9984f..1dabad54fd 100644 --- a/MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml.cs +++ b/MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml.cs @@ -15,6 +15,7 @@ using System.Windows.Input; using TaskLayer; using GuiFunctions; +using EngineLayer; namespace MetaMorpheusGUI { @@ -23,6 +24,7 @@ namespace MetaMorpheusGUI /// public partial class GptmdTaskWindow : Window { + public ObservableCollection FilterOptions { get; } = new(); private readonly ObservableCollection FixedModTypeForTreeViewObservableCollection = new ObservableCollection(); private readonly ObservableCollection VariableModTypeForTreeViewObservableCollection = new ObservableCollection(); private readonly ObservableCollection LocalizeModTypeForTreeViewObservableCollection = new ObservableCollection(); @@ -38,6 +40,7 @@ public GptmdTaskWindow(GptmdTask myGPTMDtask) AutomaticallyAskAndOrUpdateParametersBasedOnProtease = false; PopulateChoices(); + FilterOptionsListBox.ItemsSource = FilterOptions; UpdateFieldsFromTask(TheTask); AutomaticallyAskAndOrUpdateParametersBasedOnProtease = true; DeisotopingControl.DataContext = DeconHostViewModel; @@ -183,6 +186,11 @@ private void UpdateFieldsFromTask(GptmdTask task) { ye.VerifyCheckState(); } + + foreach (var filter in FilterOptions) + { + filter.IsSelected = TheTask.GptmdParameters.GptmdFilters.Any(f => f.GetType() == filter.Filter.GetType()); + } } private void PopulateChoices() @@ -240,6 +248,28 @@ private void PopulateChoices() } } gptmdModsTreeView.DataContext = GptmdModTypeForTreeViewObservableCollection; + + FilterOptions.Clear(); + FilterOptions.Add(new GptmdFilterViewModel( + new ImprovedScoreFilter(), + "Improved Score", + "Requires that the new score is greater than the original score." + )); + FilterOptions.Add(new GptmdFilterViewModel( + new DualDirectionalIonCoverageFilter(), + "Dual Directional Ion Coverage", + "Requires the mod site to be covered by at least one N-terminal and one C-terminal ion. That is, ions from both directions must include the mod, even if not flanking it." + )); + FilterOptions.Add(new GptmdFilterViewModel( + new UniDirectionalIonCoverageFilter(), + "Uni-Directional Ion Coverage", + "Requires the mod site to be covered by at least one N-terminal or one C-terminal ion. That is, ions from one direction must include the mod, even if not flanking it." + )); + FilterOptions.Add(new GptmdFilterViewModel( + new FlankingIonCoverageFilter(), + "Flanking Ion Coverage", + "Requires flanking ions — a fragment from *before* and one from *after* the mod site, regardless of fragmentation direction." + )); } private void CancelButton_Click(object sender, RoutedEventArgs e) @@ -489,6 +519,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) { TheTask.GptmdParameters.ListOfModsGptmd.AddRange(heh.Children.Where(b => b.Use).Select(b => (b.Parent.DisplayName, b.ModName))); } + TheTask.GptmdParameters.GptmdFilters = FilterOptions.Where(f => f.IsSelected).Select(f => f.Filter).ToList(); TheTask.CommonParameters = commonParamsToSave; diff --git a/MetaMorpheus/GuiFunctions/ViewModels/GptmdFilterViewModel.cs b/MetaMorpheus/GuiFunctions/ViewModels/GptmdFilterViewModel.cs new file mode 100644 index 0000000000..0455b018a5 --- /dev/null +++ b/MetaMorpheus/GuiFunctions/ViewModels/GptmdFilterViewModel.cs @@ -0,0 +1,25 @@ +using EngineLayer; + +namespace GuiFunctions; + +public class GptmdFilterViewModel : BaseViewModel +{ + private bool _isSelected = false; + public IGptmdFilter Filter { get; } + public string Name { get; } + public string Summary { get; } + + public bool IsSelected + { + get => _isSelected; + set { _isSelected = value; OnPropertyChanged(nameof(IsSelected)); } + } + + public GptmdFilterViewModel(IGptmdFilter filter, string name, string summary, bool isSelected = true) + { + Filter = filter; + Name = name; + Summary = summary; + _isSelected = isSelected; + } +} \ No newline at end of file diff --git a/MetaMorpheus/TaskLayer/GPTMDTask/GPTMDParameters.cs b/MetaMorpheus/TaskLayer/GPTMDTask/GPTMDParameters.cs index cc197462c0..7c6c2c1d22 100644 --- a/MetaMorpheus/TaskLayer/GPTMDTask/GPTMDParameters.cs +++ b/MetaMorpheus/TaskLayer/GPTMDTask/GPTMDParameters.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using EngineLayer.Gptmd; namespace EngineLayer { @@ -10,6 +11,7 @@ public class GptmdParameters /// public GptmdParameters() { + GptmdFilters = new(); ListOfModsGptmd = GlobalVariables.AllModsKnown.Where(b => b.ModificationType.Equals("Common Artifact") || b.ModificationType.Equals("Common Biological") @@ -19,5 +21,6 @@ public GptmdParameters() } public List<(string, string)> ListOfModsGptmd { get; set; } + public List GptmdFilters { get; set; } } } \ No newline at end of file diff --git a/MetaMorpheus/TaskLayer/GPTMDTask/GPTMDTask.cs b/MetaMorpheus/TaskLayer/GPTMDTask/GPTMDTask.cs index 46459c7ae4..9185bd0295 100644 --- a/MetaMorpheus/TaskLayer/GPTMDTask/GPTMDTask.cs +++ b/MetaMorpheus/TaskLayer/GPTMDTask/GPTMDTask.cs @@ -175,7 +175,7 @@ protected override MyTaskResults RunSpecific(string OutputFolder, List { taskId }, doPEP: false).Run(); Dictionary>> allModDictionary = new(); - new GptmdEngine(allPsms, gptmdModifications, combos, filePathToPrecursorMassTolerance, CommonParameters, this.FileSpecificParameters, new List { taskId }, allModDictionary).Run(); + new GptmdEngine(allPsms, gptmdModifications, combos, filePathToPrecursorMassTolerance, CommonParameters, this.FileSpecificParameters, new List { taskId }, allModDictionary, GptmdParameters.GptmdFilters).Run(); //Move this text after search because proteins don't get loaded until search begins. ProseCreatedWhileRunning.Append("The combined search database contained " + proteinList.Count(p => !p.IsDecoy) + $" non-decoy {GlobalVariables.AnalyteType.GetBioPolymerLabel().ToLower()} entries including " + proteinList.Where(p => p.IsContaminant).Count() + " contaminant sequences. "); diff --git a/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs b/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs index 7ad039a603..edeb71ced2 100644 --- a/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs +++ b/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs @@ -16,7 +16,6 @@ using SpectralAveraging; using Omics; using Omics.Digestion; -using Omics.Fragmentation.Peptide; using Omics.Modifications; using Omics.SpectrumMatch; using UsefulProteomicsDatabases; @@ -25,8 +24,6 @@ using Proteomics.ProteolyticDigestion; using Transcriptomics; using Transcriptomics.Digestion; -using Easy.Common.Extensions; -using Readers; namespace TaskLayer { @@ -134,6 +131,24 @@ public abstract class MetaMorpheusTask .ConfigureType(type => type .WithConversionFor(convert => convert .ToToml(custom => custom.GetType().Name))) + .ConfigureType>(type => type + .WithConversionFor(convert => convert + .ToToml(filters => string.Join("\t", filters.Select(f => f.GetType().Name))) + .FromToml(tmlString => tmlString.Value + .Split('\t', StringSplitOptions.RemoveEmptyEntries) + .Select(typeName => + // Find the type in the current AppDomain by name + AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => a.GetTypes()) + .FirstOrDefault(t => t.Name == typeName && typeof(IGptmdFilter).IsAssignableFrom(t)) + ) + .Where(t => t != null) + .Select(t => Activator.CreateInstance(t) as IGptmdFilter) + .Where(f => f != null) + .ToList() + ) + ) + ) ); diff --git a/MetaMorpheus/Test/GptmdFilterTests.cs b/MetaMorpheus/Test/GptmdFilterTests.cs new file mode 100644 index 0000000000..113f028941 --- /dev/null +++ b/MetaMorpheus/Test/GptmdFilterTests.cs @@ -0,0 +1,493 @@ +using NUnit.Framework; +using Omics.Fragmentation; +using Proteomics.ProteolyticDigestion; +using System.Collections.Generic; +using Chemistry; +using EngineLayer; +using MassSpectrometry; +using Omics.Modifications; +using System.Linq; + +namespace Test; + +[TestFixture] +public class GptmdFilterTests +{ + // Helper to create a dummy PeptideWithSetModifications + private PeptideWithSetModifications DummyPeptide() => new("PEPTIDE", []); + private Modification DummyMod() => new("a", "a", "a", "a", DummyMotif(), "Anywhere.", null, 20); + private ModificationMotif DummyMotif() => ModificationMotif.TryGetMotif("X", out var motif) ? motif : null; + // Helper to create a dummy SpectralMatch + private SpectralMatch DummySpectralMatch() => new PeptideSpectralMatch(DummyPeptide(), 0, 0, 0, + new Ms2ScanWithSpecificMass( + new MsDataScan(new MzSpectrum(new double[] { 1 }, new double[] { 1 }, false), 0, 1, true, + MassSpectrometry.Polarity.Positive, double.NaN, null, null, MZAnalyzerType.Orbitrap, double.NaN, + null, null, "scan=1", double.NaN, null, null, double.NaN, null, DissociationType.AnyActivationType, + 0, null), + (new Proteomics.AminoAcidPolymer.Peptide(DummyPeptide().BaseSequence).MonoisotopicMass + 21.981943) + .ToMz(1), 1, "filepath", new CommonParameters()) + , + new(), new List()); + + // Helper to create a dummy MatchedFragmentIon + private MatchedFragmentIon CreateIon(FragmentationTerminus terminus, int fragmentNumber, int residuePosition) + { + var product = new Product(ProductType.b, terminus, 0, fragmentNumber, residuePosition, 0, 0); + return new MatchedFragmentIon(product, 100, 100, 1); + } + + [Test] + public void ImprovedScoreFilter_Passes_ReturnsTrueIfNewScoreGreater() + { + var filter = new ImprovedScoreFilter(); + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + newScore: 2.0, + originalScore: 1.0, + matchedIons: null, + peptideOneBasedModSite: 1, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.True); + } + + [Test] + public void ImprovedScoreFilter_Passes_ReturnsFalseIfNewScoreNotGreater() + { + var filter = new ImprovedScoreFilter(); + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + newScore: 1.0, + originalScore: 2.0, + matchedIons: null, + peptideOneBasedModSite: 1, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void DualDirectionalIonCoverageFilter_Passes_ReturnsFalseIfNoMatchedIons() + { + var filter = new DualDirectionalIonCoverageFilter(); + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + matchedIons: null, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void DualDirectionalIonCoverageFilter_Passes_ReturnsTrueIfBothDirectionsCovered() + { + var filter = new DualDirectionalIonCoverageFilter(); + var ions = new List + { + CreateIon(FragmentationTerminus.N, fragmentNumber: 5, residuePosition: 5), // covers N-term + CreateIon(FragmentationTerminus.C, fragmentNumber: 6, residuePosition: 1) // covers C-term + }; + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.True); + } + + [Test] + public void DualDirectionalIonCoverageFilter_Passes_ReturnsFalseIfOnlyOneDirectionCovered() + { + var filter = new DualDirectionalIonCoverageFilter(); + var ions = new List + { + CreateIon(FragmentationTerminus.N, fragmentNumber: 3, residuePosition: 2) + }; + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void FlankingIonCoverageFilter_Passes_ReturnsFalseIfNoMatchedIons() + { + var filter = new FlankingIonCoverageFilter(); + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + matchedIons: null, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void FlankingIonCoverageFilter_Passes_ReturnsTrueIfBothFlanksCovered() + { + var filter = new FlankingIonCoverageFilter(); + var ions = new List + { + CreateIon(FragmentationTerminus.N, fragmentNumber: 2, residuePosition: 2), // left flank + CreateIon(FragmentationTerminus.C, fragmentNumber: 3, residuePosition: 3) // right flank + }; + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.True); + } + + [Test] + public void FlankingIonCoverageFilter_Passes_ReturnsFalseIfOnlyLeftFlankCovered() + { + var filter = new FlankingIonCoverageFilter(); + var ions = new List + { + CreateIon(FragmentationTerminus.N, fragmentNumber: 2, residuePosition: 2) + }; + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void FlankingIonCoverageFilter_Passes_ReturnsFalseIfOnlyRightFlankCovered() + { + var filter = new FlankingIonCoverageFilter(); + var ions = new List + { + CreateIon(FragmentationTerminus.C, fragmentNumber: 3, residuePosition: 3) + }; + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void ImprovedScoreFilter_Passes_ReturnsFalseIfScoresAreEqual() + { + var filter = new ImprovedScoreFilter(); + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + newScore: 1.0, + originalScore: 1.0, + matchedIons: null, + peptideOneBasedModSite: 1, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void DualDirectionalIonCoverageFilter_Passes_ReturnsFalseIfEmptyMatchedIons() + { + var filter = new DualDirectionalIonCoverageFilter(); + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + new List(), + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void DualDirectionalIonCoverageFilter_Passes_ReturnsTrueIfFivePrimeAndThreePrimeCovered() + { + var filter = new DualDirectionalIonCoverageFilter(); + var ions = new List + { + CreateIon(FragmentationTerminus.FivePrime, fragmentNumber: 4, residuePosition: 4), // N-term + CreateIon(FragmentationTerminus.ThreePrime, fragmentNumber: 5, residuePosition: 1) // C-term + }; + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.True); + } + + [Test] + public void FlankingIonCoverageFilter_Passes_ReturnsFalseIfEmptyMatchedIons() + { + var filter = new FlankingIonCoverageFilter(); + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + new List(), + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void FlankingIonCoverageFilter_Passes_ReturnsFalseIfBothFlanksAreSameIon() + { + var filter = new FlankingIonCoverageFilter(); + var ions = new List + { + CreateIon(FragmentationTerminus.N, fragmentNumber: 2, residuePosition: 2), // left flank only + CreateIon(FragmentationTerminus.N, fragmentNumber: 2, residuePosition: 2) // duplicate left flank + }; + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void UniDirectionalIonCoverageFilter_Passes_ReturnsFalseIfNoMatchedIons() + { + var filter = new UniDirectionalIonCoverageFilter(); + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + matchedIons: null, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void UniDirectionalIonCoverageFilter_Passes_ReturnsFalseIfEmptyMatchedIons() + { + var filter = new UniDirectionalIonCoverageFilter(); + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + new List(), + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void UniDirectionalIonCoverageFilter_Passes_ReturnsTrueIfCoveredFromNTerm() + { + var filter = new UniDirectionalIonCoverageFilter(); + var ions = new List + { + CreateIon(FragmentationTerminus.N, fragmentNumber: 4, residuePosition: 3) // covers N-term, residuePosition >= site + }; + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.True); + } + + [Test] + public void UniDirectionalIonCoverageFilter_Passes_ReturnsTrueIfCoveredFromCTerm() + { + var filter = new UniDirectionalIonCoverageFilter(); + var ions = new List + { + CreateIon(FragmentationTerminus.C, fragmentNumber: 2, residuePosition: 2) // covers C-term, residuePosition < site + }; + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.True); + } + + [Test] + public void UniDirectionalIonCoverageFilter_Passes_ReturnsTrueIfCoveredFromFivePrime() + { + var filter = new UniDirectionalIonCoverageFilter(); + var ions = new List + { + CreateIon(FragmentationTerminus.FivePrime, fragmentNumber: 5, residuePosition: 4) // covers N-term, residuePosition >= site + }; + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.True); + } + + [Test] + public void UniDirectionalIonCoverageFilter_Passes_ReturnsTrueIfCoveredFromThreePrime() + { + var filter = new UniDirectionalIonCoverageFilter(); + var ions = new List + { + CreateIon(FragmentationTerminus.ThreePrime, fragmentNumber: 2, residuePosition: 2) // covers C-term, residuePosition < site + }; + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.True); + } + + [Test] + public void UniDirectionalIonCoverageFilter_Passes_ReturnsFalseIfNeitherDirectionCovered() + { + var filter = new UniDirectionalIonCoverageFilter(); + var ions = new List + { + CreateIon(FragmentationTerminus.N, fragmentNumber: 2, residuePosition: 1), // residuePosition < site, not covered + CreateIon(FragmentationTerminus.C, fragmentNumber: 4, residuePosition: 5) // residuePosition >= site, not covered + }; + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 3, + peptideLength: 7, + DummyMod()); + + Assert.That(result, Is.False); + } + + [Test] + public void DualDirectionalIonCoverageFilter_Passes_NTerminalAcetylation_PassesWithOneDirection() + { + var filter = new DualDirectionalIonCoverageFilter(); + // Only N-term ion present + var ions = new List + { + CreateIon(FragmentationTerminus.N, fragmentNumber: 1, residuePosition: 1) + }; + var nTermAcetyl = GlobalVariables.AllModsKnown.First(p => p.IdWithMotif.Contains("Acetylation") && p.LocationRestriction.Contains("terminal")); + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 1, + peptideLength: 7, + nTermAcetyl); + + Assert.That(result, Is.True); + } + + [Test] + public void FlankingIonCoverageFilter_Passes_NTerminalAcetylation_PassesWithOneFlank() + { + var filter = new FlankingIonCoverageFilter(); + + // Only right flank present (site-1) + var ions = new List + { + CreateIon(FragmentationTerminus.N, fragmentNumber: 1, residuePosition: 1) + }; + + // N-terminal acetylation mod with "terminal" in LocationRestriction + var nTermAcetyl = GlobalVariables.AllModsKnown.First(p => p.IdWithMotif.Contains("Acetylation") && p.LocationRestriction.Contains("terminal")); + + bool result = filter.Passes( + DummyPeptide(), + DummySpectralMatch(), + 0, 0, + ions, + peptideOneBasedModSite: 1, + peptideLength: 7, + nTermAcetyl); + + Assert.That(result, Is.True); + } + +} +