From f2b470e5f8375bd6ed98a249554acabdff047a70 Mon Sep 17 00:00:00 2001 From: Nic Bollis Date: Thu, 24 Jul 2025 11:36:32 -0500 Subject: [PATCH 01/11] NonVariant- pick best by delta mass, not by mod --- MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs | 105 +++++++++--------- 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs index 076c69d485..7850dc7f03 100644 --- a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs +++ b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs @@ -9,6 +9,7 @@ using System.Collections.Concurrent; using MassSpectrometry; using Omics.Fragmentation; +using System.Threading; namespace EngineLayer.Gptmd { @@ -90,51 +91,72 @@ protected override MetaMorpheusEngineResults RunSpecific() { for (int i = range.Item1; i < range.Item2; i++) { - foreach (var pepWithSetMods in psms[i].BestMatchingBioPolymersWithSetMods.Select(v => v.SpecificBioPolymer as PeptideWithSetModifications)) + // 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(); + + foreach (var pepWithSetMods in psm.BestMatchingBioPolymersWithSetMods.Select(v => v.SpecificBioPolymer as PeptideWithSetModifications)) { var isVariantProtein = pepWithSetMods.Parent != pepWithSetMods.Protein.NonVariantProtein; - var possibleModifications = GetPossibleMods(psms[i].ScanPrecursorMass, GptmdModifications, Combos, FilePathToPrecursorMassTolerance[psms[i].FullFilePath], pepWithSetMods); + var possibleModifications = GetPossibleMods(psm.ScanPrecursorMass, GptmdModifications, Combos, FilePathToPrecursorMassTolerance[psm.FullFilePath], pepWithSetMods); + + double bestScore = originalScore; // Initialize with the original score of the PSM to ensure gptmd only adds if new modified forms scores higher + IBioPolymerWithSetMods bestPeptide = null; + Modification bestMod = null; + int bestIndex = -1; if (!isVariantProtein) { + // Iterate through all possible modifications with this mass shift foreach (var mod in possibleModifications) { - List possibleIndices = Enumerable.Range(0, pepWithSetMods.Length).Where(i => ModFits(mod, pepWithSetMods.Parent, i + 1, pepWithSetMods.Length, pepWithSetMods.OneBasedStartResidue + i)).ToList(); - if (possibleIndices.Any()) - { - List newPeptides = new(); - foreach (int index in possibleIndices) - { - if (mod.MonoisotopicMass.HasValue) - { - newPeptides.Add((PeptideWithSetModifications)pepWithSetMods.Localize(index, mod.MonoisotopicMass.Value)); - } - } + if (!mod.MonoisotopicMass.HasValue) + continue; - if (newPeptides.Any()) - { - var scores = new List(); - var dissociationType = CommonParameters.DissociationType == DissociationType.Autodetect ? - psms[i].Ms2Scan.DissociationType.Value : CommonParameters.DissociationType; + // Find all possible indices for the modification on the peptide + List possibleIndices = Enumerable.Range(0, pepWithSetMods.Length) + .Where(j => ModFits(mod, pepWithSetMods.Parent, j + 1, pepWithSetMods.Length, pepWithSetMods.OneBasedStartResidue + j)) + .ToList(); - scores = CalculatePeptideScores(newPeptides, dissociationType, psms[i]); + foreach (int index in possibleIndices) + { + // Create a new peptide with the modification at the current index + var newPep = pepWithSetMods.Localize(index, mod.MonoisotopicMass.Value); + peptideTheorProducts.Clear(); + newPep.Fragment(dissociationType, + CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts); - // 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(); + ms2ScanWithSpecificMass ??= new Ms2ScanWithSpecificMass(scan, precursorMass, + precursorCharge, fileName, CommonParameters); + var matchedIons = MetaMorpheusEngine.MatchFragmentIons(ms2ScanWithSpecificMass, + peptideTheorProducts, CommonParameters, matchAllCharges: false); + double score = CalculatePeptideScore(scan, matchedIons, false); - foreach (var index in highScoreIndices) - { - AddIndexedMod(modDict, pepWithSetMods.Protein.Accession, new Tuple(pepWithSetMods.OneBasedStartResidue + possibleIndices[index], mod)); - System.Threading.Interlocked.Increment(ref modsAdded); ; - } + if (score > bestScore) + { + bestScore = score; + bestPeptide = newPep; + bestMod = mod; + bestIndex = index; } } } + + // If a modified peptide scored higher than the unmodified peptide, add it to the mod dictionary + if (bestPeptide != null && bestMod != null) + { + int proteinIndex = pepWithSetMods.OneBasedStartResidue + bestIndex; + AddIndexedMod(modDict, pepWithSetMods.Protein.Accession, new Tuple(proteinIndex, bestMod)); + Interlocked.Increment(ref modsAdded); + } } // if a variant protein, index to variant protein if on variant, or to the original protein if not else @@ -193,27 +215,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, From 63c4d60de6b54156e6840cf43650d0a4666730dc Mon Sep 17 00:00:00 2001 From: Nic Bollis Date: Thu, 24 Jul 2025 11:44:30 -0500 Subject: [PATCH 02/11] Generalized and unified all --- MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs | 242 ++++++++++++------ 1 file changed, 164 insertions(+), 78 deletions(-) diff --git a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs index 7850dc7f03..4ce8a38e9a 100644 --- a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs +++ b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs @@ -106,106 +106,192 @@ protected override MetaMorpheusEngineResults RunSpecific() foreach (var pepWithSetMods in psm.BestMatchingBioPolymersWithSetMods.Select(v => v.SpecificBioPolymer as PeptideWithSetModifications)) { var isVariantProtein = pepWithSetMods.Parent != pepWithSetMods.Protein.NonVariantProtein; - var possibleModifications = GetPossibleMods(psm.ScanPrecursorMass, GptmdModifications, Combos, FilePathToPrecursorMassTolerance[psm.FullFilePath], pepWithSetMods); - + var possibleModifications = GetPossibleMods(precursorMass, GptmdModifications, Combos, + FilePathToPrecursorMassTolerance[fileName], pepWithSetMods); + double bestScore = originalScore; // Initialize with the original score of the PSM to ensure gptmd only adds if new modified forms scores higher - IBioPolymerWithSetMods bestPeptide = null; Modification bestMod = null; int bestIndex = -1; + string bestProteinAccession = null; - if (!isVariantProtein) + foreach (var mod in possibleModifications) { - // Iterate through all possible modifications with this mass shift - foreach (var mod in possibleModifications) + if (!mod.MonoisotopicMass.HasValue) + continue; + + for (int j = 0; j < pepWithSetMods.Length; j++) { - if (!mod.MonoisotopicMass.HasValue) + int indexInProtein = pepWithSetMods.OneBasedStartResidue + j; + if (!ModFits(mod, pepWithSetMods.Parent, j + 1, pepWithSetMods.Length, indexInProtein)) + continue; + + var newPep = (PeptideWithSetModifications)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); + + if (score <= bestScore) continue; - // Find all possible indices for the modification on the peptide - List possibleIndices = Enumerable.Range(0, pepWithSetMods.Length) - .Where(j => ModFits(mod, pepWithSetMods.Parent, j + 1, pepWithSetMods.Length, pepWithSetMods.OneBasedStartResidue + j)) - .ToList(); + bestScore = score; + bestMod = mod; + bestIndex = j; + bestProteinAccession = null; - foreach (int index in possibleIndices) + if (!isVariantProtein) { - // Create a new peptide with the modification at the current index - var newPep = pepWithSetMods.Localize(index, mod.MonoisotopicMass.Value); - peptideTheorProducts.Clear(); - newPep.Fragment(dissociationType, - CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts); - - ms2ScanWithSpecificMass ??= new Ms2ScanWithSpecificMass(scan, precursorMass, - precursorCharge, fileName, CommonParameters); - var matchedIons = MetaMorpheusEngine.MatchFragmentIons(ms2ScanWithSpecificMass, - peptideTheorProducts, CommonParameters, matchAllCharges: false); - double score = CalculatePeptideScore(scan, matchedIons, false); - - if (score > bestScore) - { - bestScore = score; - bestPeptide = newPep; - bestMod = mod; - bestIndex = index; - } + bestProteinAccession = pepWithSetMods.Protein.Accession; } - } - - // If a modified peptide scored higher than the unmodified peptide, add it to the mod dictionary - if (bestPeptide != null && bestMod != null) - { - int proteinIndex = pepWithSetMods.OneBasedStartResidue + bestIndex; - AddIndexedMod(modDict, pepWithSetMods.Protein.Accession, new Tuple(proteinIndex, bestMod)); - Interlocked.Increment(ref modsAdded); - } - } - // 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++) + else { - int indexInProtein = pepWithSetMods.OneBasedStartResidue + j; - - if (ModFits(mod, pepWithSetMods.Parent, j + 1, pepWithSetMods.Length, indexInProtein)) + int offset = 0; + foreach (var variant in pepWithSetMods.Protein.AppliedSequenceVariations.OrderBy(v => v.OneBasedBeginPosition)) { - bool foundSite = false; - int offset = 0; - foreach (var variant in pepWithSetMods.Protein.AppliedSequenceVariations.OrderBy(v => v.OneBasedBeginPosition)) + bool modIsBeforeVariant = indexInProtein < variant.OneBasedBeginPosition + offset; + bool modIsOnVariant = variant.OneBasedBeginPosition + offset <= indexInProtein + && indexInProtein <= variant.OneBasedEndPosition + offset; + + if (modIsOnVariant) { - 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.Protein.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.Protein.NonVariantProtein.Accession, new Tuple(indexInProtein - offset, mod)); - foundSite = true; - System.Threading.Interlocked.Increment(ref modsAdded); ; - break; - } - - offset += variant.VariantSequence.Length - variant.OriginalSequence.Length; + bestProteinAccession = pepWithSetMods.Protein.Accession; + break; } - if (!foundSite) + + if (modIsBeforeVariant) { - AddIndexedMod(modDict, pepWithSetMods.Protein.NonVariantProtein.Accession, new Tuple(indexInProtein - offset, mod)); - System.Threading.Interlocked.Increment(ref modsAdded); ; + bestProteinAccession = pepWithSetMods.Protein.NonVariantProtein.Accession; + indexInProtein -= offset; + break; } + + offset += variant.VariantSequence.Length - variant.OriginalSequence.Length; } + + if (bestProteinAccession == null) + { + bestProteinAccession = pepWithSetMods.Protein.NonVariantProtein.Accession; + indexInProtein -= offset; + } + + bestIndex = indexInProtein - pepWithSetMods.OneBasedStartResidue; } } } + + if (bestMod != null && bestProteinAccession != null) + { + int modSite = pepWithSetMods.OneBasedStartResidue + bestIndex; + AddIndexedMod(modDict, bestProteinAccession, new Tuple(modSite, bestMod)); + Interlocked.Increment(ref modsAdded); + } } + + //foreach (var pepWithSetMods in psm.BestMatchingBioPolymersWithSetMods.Select(v => v.SpecificBioPolymer as PeptideWithSetModifications)) + //{ + // var isVariantProtein = pepWithSetMods.Parent != pepWithSetMods.Protein.NonVariantProtein; + // var possibleModifications = GetPossibleMods(psm.ScanPrecursorMass, GptmdModifications, Combos, FilePathToPrecursorMassTolerance[psm.FullFilePath], pepWithSetMods); + + // double bestScore = originalScore; // Initialize with the original score of the PSM to ensure gptmd only adds if new modified forms scores higher + // IBioPolymerWithSetMods bestPeptide = null; + // Modification bestMod = null; + // int bestIndex = -1; + + // if (!isVariantProtein) + // { + // // Iterate through all possible modifications with this mass shift + // foreach (var mod in possibleModifications) + // { + // if (!mod.MonoisotopicMass.HasValue) + // continue; + + // // Find all possible indices for the modification on the peptide + // List possibleIndices = Enumerable.Range(0, pepWithSetMods.Length) + // .Where(j => ModFits(mod, pepWithSetMods.Parent, j + 1, pepWithSetMods.Length, pepWithSetMods.OneBasedStartResidue + j)) + // .ToList(); + + // foreach (int index in possibleIndices) + // { + // // Create a new peptide with the modification at the current index + // var newPep = pepWithSetMods.Localize(index, mod.MonoisotopicMass.Value); + // peptideTheorProducts.Clear(); + // newPep.Fragment(dissociationType, + // CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts); + + // ms2ScanWithSpecificMass ??= new Ms2ScanWithSpecificMass(scan, precursorMass, + // precursorCharge, fileName, CommonParameters); + // var matchedIons = MetaMorpheusEngine.MatchFragmentIons(ms2ScanWithSpecificMass, + // peptideTheorProducts, CommonParameters, matchAllCharges: false); + // double score = CalculatePeptideScore(scan, matchedIons, false); + + // if (score > bestScore) + // { + // bestScore = score; + // bestPeptide = newPep; + // bestMod = mod; + // bestIndex = index; + // } + // } + // } + + // // If a modified peptide scored higher than the unmodified peptide, add it to the mod dictionary + // if (bestPeptide != null && bestMod != null) + // { + // int proteinIndex = pepWithSetMods.OneBasedStartResidue + bestIndex; + // AddIndexedMod(modDict, pepWithSetMods.Protein.Accession, new Tuple(proteinIndex, bestMod)); + // Interlocked.Increment(ref modsAdded); + // } + // } + // // 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)) + // { + // 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.Protein.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.Protein.NonVariantProtein.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.Protein.NonVariantProtein.Accession, new Tuple(indexInProtein - offset, mod)); + // System.Threading.Interlocked.Increment(ref modsAdded); ; + // } + // } + // } + // } + // } + //} } }); From a5f2f683e886f69c996a4122095828b71d4bcd45 Mon Sep 17 00:00:00 2001 From: Nic Bollis Date: Thu, 24 Jul 2025 12:31:45 -0500 Subject: [PATCH 03/11] Built and tested filters --- MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs | 12 +- .../EngineLayer/Gptmd/IGptmdFilter.cs | 132 ++++++ MetaMorpheus/Test/IGptmdFilterTests.cs | 422 ++++++++++++++++++ 3 files changed, 562 insertions(+), 4 deletions(-) create mode 100644 MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs create mode 100644 MetaMorpheus/Test/IGptmdFilterTests.cs diff --git a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs index 4ce8a38e9a..8cb00a5083 100644 --- a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs +++ b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs @@ -23,6 +23,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, @@ -32,7 +33,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; @@ -40,6 +42,8 @@ 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) @@ -109,7 +113,7 @@ protected override MetaMorpheusEngineResults RunSpecific() var possibleModifications = GetPossibleMods(precursorMass, GptmdModifications, Combos, FilePathToPrecursorMassTolerance[fileName], pepWithSetMods); - double bestScore = originalScore; // Initialize with the original score of the PSM to ensure gptmd only adds if new modified forms scores higher + // Initialize with the original score of the PSM to ensure gptmd only adds if new modified forms scores higher Modification bestMod = null; int bestIndex = -1; string bestProteinAccession = null; @@ -133,10 +137,10 @@ protected override MetaMorpheusEngineResults RunSpecific() var matchedIons = MatchFragmentIons(ms2ScanWithSpecificMass, peptideTheorProducts, CommonParameters, matchAllCharges: false); double score = CalculatePeptideScore(scan, matchedIons, false); - if (score <= bestScore) + // plus 2 is to translate from zero based string array index to OneBasedModification index + if (!Filters.All(f => f.Passes(newPep, psm, score, originalScore, matchedIons, j + 2, pepWithSetMods.Length))) continue; - bestScore = score; bestMod = mod; bestIndex = j; bestProteinAccession = null; diff --git a/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs b/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs new file mode 100644 index 0000000000..da6eeab921 --- /dev/null +++ b/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs @@ -0,0 +1,132 @@ +using Omics.Fragmentation; +using System.Collections.Generic; +using Omics; +using Proteomics.ProteolyticDigestion; +using System.Linq; + +namespace EngineLayer.Gptmd; + +public interface IGptmdFilter +{ + bool Passes( + PeptideWithSetModifications candidatePeptide, + SpectralMatch psm, + double newScore, + double originalScore, + List matchedIons, + int peptideOneBasedModSite, + int peptideLength); +} + +/// +/// Requires that the new score is greater than the original score. +/// +public sealed class ImprovedScoreFilter : IGptmdFilter +{ + public bool Passes( + PeptideWithSetModifications candidatePeptide, + SpectralMatch psm, + double newScore, + double originalScore, + List matchedIons, + int peptideOneBasedModSite, + int peptideLength) + { + 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( + PeptideWithSetModifications candidatePeptide, + SpectralMatch psm, + double newScore, + double originalScore, + List matchedIons, + int peptideOneBasedModSite, + int peptideLength) + { + if (matchedIons == null || matchedIons.Count == 0) + return false; + + int site = peptideOneBasedModSite; + + bool coveredFromNTerm = matchedIons.Any(m => + m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.N or FragmentationTerminus.FivePrime && + m.NeutralTheoreticalProduct.ResiduePosition >= site); + + bool coveredFromCTerm = matchedIons.Any(m => + m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.C or FragmentationTerminus.ThreePrime && + m.NeutralTheoreticalProduct.ResiduePosition < site); + + 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( + PeptideWithSetModifications candidatePeptide, + SpectralMatch psm, + double newScore, + double originalScore, + List matchedIons, + int peptideOneBasedModSite, + int peptideLength) + { + if (matchedIons == null || matchedIons.Count == 0) + return false; + + int site = peptideOneBasedModSite; + + bool coveredFromNTerm = matchedIons.Any(m => + m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.N or FragmentationTerminus.FivePrime && + m.NeutralTheoreticalProduct.ResiduePosition >= site); + + bool coveredFromCTerm = matchedIons.Any(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 +{ + public bool Passes( + PeptideWithSetModifications candidatePeptide, + SpectralMatch psm, + double newScore, + double originalScore, + List matchedIons, + int peptideOneBasedModSite, + int peptideLength) + { + 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); + + return leftFlank && rightFlank; + } +} + diff --git a/MetaMorpheus/Test/IGptmdFilterTests.cs b/MetaMorpheus/Test/IGptmdFilterTests.cs new file mode 100644 index 0000000000..eeeb6fa342 --- /dev/null +++ b/MetaMorpheus/Test/IGptmdFilterTests.cs @@ -0,0 +1,422 @@ +using EngineLayer.Gptmd; +using NUnit.Framework; +using Omics.Fragmentation; +using Proteomics.ProteolyticDigestion; +using System.Collections.Generic; +using Chemistry; +using EngineLayer; +using MassSpectrometry; +using Polarity = ThermoFisher.CommonCore.Data.Business.Polarity; + +namespace Test +{ + [TestFixture] + public class IGptmdFilterTests + { + // Helper to create a dummy PeptideWithSetModifications + private PeptideWithSetModifications DummyPeptide() => new("PEPTIDE", []); + + // 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + Assert.That(result, Is.False); + } + } +} + From f15cfba78fabdfec239b4bd7c071bbb8b71ae11c Mon Sep 17 00:00:00 2001 From: Nic Bollis Date: Thu, 24 Jul 2025 13:11:04 -0500 Subject: [PATCH 04/11] Gptmd filter in toml and passed through task --- .../EngineLayer/Gptmd/IGptmdFilter.cs | 52 +++++++++++++------ .../TaskLayer/GPTMDTask/GPTMDParameters.cs | 3 ++ MetaMorpheus/TaskLayer/GPTMDTask/GPTMDTask.cs | 2 +- MetaMorpheus/TaskLayer/MetaMorpheusTask.cs | 19 +++++++ MetaMorpheus/Test/IGptmdFilterTests.cs | 2 - 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs b/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs index da6eeab921..3823c1da41 100644 --- a/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs +++ b/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs @@ -1,21 +1,28 @@ -using Omics.Fragmentation; +using System; +using Omics.Fragmentation; using System.Collections.Generic; using Omics; -using Proteomics.ProteolyticDigestion; using System.Linq; -namespace EngineLayer.Gptmd; +namespace EngineLayer; -public interface IGptmdFilter +public interface IGptmdFilter : IEquatable { + public static string GetFilterTypeName(IGptmdFilter filter) => filter.GetType().Name; + bool Passes( - PeptideWithSetModifications candidatePeptide, + IBioPolymerWithSetMods candidatePeptide, SpectralMatch psm, double newScore, double originalScore, List matchedIons, int peptideOneBasedModSite, int peptideLength); + + bool IEquatable.Equals(IGptmdFilter? other) + { + return other != null && GetType() == other.GetType(); + } } /// @@ -24,7 +31,7 @@ bool Passes( public sealed class ImprovedScoreFilter : IGptmdFilter { public bool Passes( - PeptideWithSetModifications candidatePeptide, + IBioPolymerWithSetMods candidatePeptide, SpectralMatch psm, double newScore, double originalScore, @@ -42,8 +49,9 @@ public bool Passes( /// public sealed class DualDirectionalIonCoverageFilter : 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( - PeptideWithSetModifications candidatePeptide, + IBioPolymerWithSetMods candidatePeptide, SpectralMatch psm, double newScore, double originalScore, @@ -57,12 +65,16 @@ public bool Passes( int site = peptideOneBasedModSite; bool coveredFromNTerm = matchedIons.Any(m => - m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.N or FragmentationTerminus.FivePrime && - m.NeutralTheoreticalProduct.ResiduePosition >= site); + 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.Terminus is FragmentationTerminus.C or FragmentationTerminus.ThreePrime && - m.NeutralTheoreticalProduct.ResiduePosition < site); + m.NeutralTheoreticalProduct.ProductType == ProductType.M || + (m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.C or FragmentationTerminus.ThreePrime && + m.NeutralTheoreticalProduct.ResiduePosition < site) + ); return coveredFromNTerm && coveredFromCTerm; } @@ -74,8 +86,9 @@ m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.C or Fragmentation /// public sealed class UniDirectionalIonCoverageFilter : 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( - PeptideWithSetModifications candidatePeptide, + IBioPolymerWithSetMods candidatePeptide, SpectralMatch psm, double newScore, double originalScore, @@ -89,12 +102,16 @@ public bool Passes( int site = peptideOneBasedModSite; bool coveredFromNTerm = matchedIons.Any(m => - m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.N or FragmentationTerminus.FivePrime && - m.NeutralTheoreticalProduct.ResiduePosition >= site); + 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.Terminus is FragmentationTerminus.C or FragmentationTerminus.ThreePrime && - m.NeutralTheoreticalProduct.ResiduePosition < site); + m.NeutralTheoreticalProduct.ProductType == ProductType.M || + (m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.C or FragmentationTerminus.ThreePrime && + m.NeutralTheoreticalProduct.ResiduePosition < site) + ); return coveredFromNTerm || coveredFromCTerm; } @@ -106,8 +123,9 @@ m.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.C or Fragmentation /// 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( - PeptideWithSetModifications candidatePeptide, + IBioPolymerWithSetMods candidatePeptide, SpectralMatch psm, double newScore, double originalScore, 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 cdef04842b..fab8f6d0cc 100644 --- a/MetaMorpheus/TaskLayer/GPTMDTask/GPTMDTask.cs +++ b/MetaMorpheus/TaskLayer/GPTMDTask/GPTMDTask.cs @@ -166,7 +166,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 90768ed6d7..c1eebe40fe 100644 --- a/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs +++ b/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs @@ -26,6 +26,7 @@ using Transcriptomics; using Transcriptomics.Digestion; using Easy.Common.Extensions; +using EngineLayer.Gptmd; using Readers; namespace TaskLayer @@ -134,6 +135,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/IGptmdFilterTests.cs b/MetaMorpheus/Test/IGptmdFilterTests.cs index eeeb6fa342..12ff542102 100644 --- a/MetaMorpheus/Test/IGptmdFilterTests.cs +++ b/MetaMorpheus/Test/IGptmdFilterTests.cs @@ -1,4 +1,3 @@ -using EngineLayer.Gptmd; using NUnit.Framework; using Omics.Fragmentation; using Proteomics.ProteolyticDigestion; @@ -6,7 +5,6 @@ using Chemistry; using EngineLayer; using MassSpectrometry; -using Polarity = ThermoFisher.CommonCore.Data.Business.Polarity; namespace Test { From 10de4e7eadc9a46aef4e27f398281d850f1215de Mon Sep 17 00:00:00 2001 From: Nic Bollis Date: Thu, 24 Jul 2025 13:11:11 -0500 Subject: [PATCH 05/11] Added filters to GUI --- .../GUI/TaskWindows/GPTMDTaskWindow.xaml | 16 +++++++++- .../GUI/TaskWindows/GPTMDTaskWindow.xaml.cs | 31 +++++++++++++++++++ .../ViewModels/GptmdFilterViewModel.cs | 25 +++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 MetaMorpheus/GuiFunctions/ViewModels/GptmdFilterViewModel.cs 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 From e0e9c5692a00c3628b7a1a5f80f43c013093a581 Mon Sep 17 00:00:00 2001 From: Nic Bollis Date: Thu, 24 Jul 2025 13:23:41 -0500 Subject: [PATCH 06/11] Added back in multi mods if scores are tied --- MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs | 154 ++++-------------- 1 file changed, 30 insertions(+), 124 deletions(-) diff --git a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs index 8cb00a5083..e418dc2c89 100644 --- a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs +++ b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs @@ -113,10 +113,8 @@ protected override MetaMorpheusEngineResults RunSpecific() var possibleModifications = GetPossibleMods(precursorMass, GptmdModifications, Combos, FilePathToPrecursorMassTolerance[fileName], pepWithSetMods); - // Initialize with the original score of the PSM to ensure gptmd only adds if new modified forms scores higher - Modification bestMod = null; - int bestIndex = -1; - string bestProteinAccession = null; + double bestScore = originalScore; // Initialize with the original score of the PSM to ensure gptmd only adds if new modified forms scores higher + List<(int site, Modification mod, string proteinAccession)> bestMatches = []; foreach (var mod in possibleModifications) { @@ -138,164 +136,72 @@ protected override MetaMorpheusEngineResults RunSpecific() 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 + 1; if (!Filters.All(f => f.Passes(newPep, psm, score, originalScore, matchedIons, j + 2, pepWithSetMods.Length))) continue; - bestMod = mod; - bestIndex = j; - bestProteinAccession = null; + if (score < bestScore - ScoreTolerance) + continue; + // resolve variant protein location + string accession; + int adjustedSite = modSite; if (!isVariantProtein) { - bestProteinAccession = pepWithSetMods.Protein.Accession; + accession = pepWithSetMods.Protein.Accession; } else { + accession = null; int offset = 0; foreach (var variant in pepWithSetMods.Protein.AppliedSequenceVariations.OrderBy(v => v.OneBasedBeginPosition)) { bool modIsBeforeVariant = indexInProtein < variant.OneBasedBeginPosition + offset; - bool modIsOnVariant = variant.OneBasedBeginPosition + offset <= indexInProtein - && indexInProtein <= variant.OneBasedEndPosition + offset; + bool modIsOnVariant = variant.OneBasedBeginPosition + offset <= indexInProtein && + indexInProtein <= variant.OneBasedEndPosition + offset; if (modIsOnVariant) { - bestProteinAccession = pepWithSetMods.Protein.Accession; + accession = pepWithSetMods.Protein.Accession; break; } if (modIsBeforeVariant) { - bestProteinAccession = pepWithSetMods.Protein.NonVariantProtein.Accession; - indexInProtein -= offset; + accession = pepWithSetMods.Protein.NonVariantProtein.Accession; + adjustedSite = indexInProtein - offset; break; } offset += variant.VariantSequence.Length - variant.OriginalSequence.Length; } - if (bestProteinAccession == null) + if (accession == null) { - bestProteinAccession = pepWithSetMods.Protein.NonVariantProtein.Accession; - indexInProtein -= offset; + accession = pepWithSetMods.Protein.NonVariantProtein.Accession; + adjustedSite = indexInProtein - offset; } + } - bestIndex = indexInProtein - pepWithSetMods.OneBasedStartResidue; + 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)); } } } - if (bestMod != null && bestProteinAccession != null) + foreach (var match in bestMatches) { - int modSite = pepWithSetMods.OneBasedStartResidue + bestIndex; - AddIndexedMod(modDict, bestProteinAccession, new Tuple(modSite, bestMod)); + AddIndexedMod(modDict, match.proteinAccession, new Tuple(match.site, match.mod)); Interlocked.Increment(ref modsAdded); } } - - //foreach (var pepWithSetMods in psm.BestMatchingBioPolymersWithSetMods.Select(v => v.SpecificBioPolymer as PeptideWithSetModifications)) - //{ - // var isVariantProtein = pepWithSetMods.Parent != pepWithSetMods.Protein.NonVariantProtein; - // var possibleModifications = GetPossibleMods(psm.ScanPrecursorMass, GptmdModifications, Combos, FilePathToPrecursorMassTolerance[psm.FullFilePath], pepWithSetMods); - - // double bestScore = originalScore; // Initialize with the original score of the PSM to ensure gptmd only adds if new modified forms scores higher - // IBioPolymerWithSetMods bestPeptide = null; - // Modification bestMod = null; - // int bestIndex = -1; - - // if (!isVariantProtein) - // { - // // Iterate through all possible modifications with this mass shift - // foreach (var mod in possibleModifications) - // { - // if (!mod.MonoisotopicMass.HasValue) - // continue; - - // // Find all possible indices for the modification on the peptide - // List possibleIndices = Enumerable.Range(0, pepWithSetMods.Length) - // .Where(j => ModFits(mod, pepWithSetMods.Parent, j + 1, pepWithSetMods.Length, pepWithSetMods.OneBasedStartResidue + j)) - // .ToList(); - - // foreach (int index in possibleIndices) - // { - // // Create a new peptide with the modification at the current index - // var newPep = pepWithSetMods.Localize(index, mod.MonoisotopicMass.Value); - // peptideTheorProducts.Clear(); - // newPep.Fragment(dissociationType, - // CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts); - - // ms2ScanWithSpecificMass ??= new Ms2ScanWithSpecificMass(scan, precursorMass, - // precursorCharge, fileName, CommonParameters); - // var matchedIons = MetaMorpheusEngine.MatchFragmentIons(ms2ScanWithSpecificMass, - // peptideTheorProducts, CommonParameters, matchAllCharges: false); - // double score = CalculatePeptideScore(scan, matchedIons, false); - - // if (score > bestScore) - // { - // bestScore = score; - // bestPeptide = newPep; - // bestMod = mod; - // bestIndex = index; - // } - // } - // } - - // // If a modified peptide scored higher than the unmodified peptide, add it to the mod dictionary - // if (bestPeptide != null && bestMod != null) - // { - // int proteinIndex = pepWithSetMods.OneBasedStartResidue + bestIndex; - // AddIndexedMod(modDict, pepWithSetMods.Protein.Accession, new Tuple(proteinIndex, bestMod)); - // Interlocked.Increment(ref modsAdded); - // } - // } - // // 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)) - // { - // 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.Protein.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.Protein.NonVariantProtein.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.Protein.NonVariantProtein.Accession, new Tuple(indexInProtein - offset, mod)); - // System.Threading.Interlocked.Increment(ref modsAdded); ; - // } - // } - // } - // } - // } - //} } }); From 01c161bbd8fbb4be963b9daa959f6f65eee22b46 Mon Sep 17 00:00:00 2001 From: Nic Bollis Date: Thu, 24 Jul 2025 13:25:01 -0500 Subject: [PATCH 07/11] Generalized to IBioPolymer --- MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs index e418dc2c89..05af122316 100644 --- a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs +++ b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs @@ -107,9 +107,9 @@ protected override MetaMorpheusEngineResults RunSpecific() Ms2ScanWithSpecificMass ms2ScanWithSpecificMass = null; var peptideTheorProducts = new List(); - foreach (var pepWithSetMods in psm.BestMatchingBioPolymersWithSetMods.Select(v => v.SpecificBioPolymer as PeptideWithSetModifications)) + foreach (var pepWithSetMods in psm.BestMatchingBioPolymersWithSetMods.Select(v => v.SpecificBioPolymer)) { - var isVariantProtein = pepWithSetMods.Parent != pepWithSetMods.Protein.NonVariantProtein; + var isVariantProtein = pepWithSetMods.Parent != pepWithSetMods.Parent.ConsensusVariant; var possibleModifications = GetPossibleMods(precursorMass, GptmdModifications, Combos, FilePathToPrecursorMassTolerance[fileName], pepWithSetMods); @@ -127,7 +127,7 @@ protected override MetaMorpheusEngineResults RunSpecific() if (!ModFits(mod, pepWithSetMods.Parent, j + 1, pepWithSetMods.Length, indexInProtein)) continue; - var newPep = (PeptideWithSetModifications)pepWithSetMods.Localize(j, mod.MonoisotopicMass.Value); + var newPep = pepWithSetMods.Localize(j, mod.MonoisotopicMass.Value); peptideTheorProducts.Clear(); newPep.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts); @@ -148,13 +148,13 @@ protected override MetaMorpheusEngineResults RunSpecific() int adjustedSite = modSite; if (!isVariantProtein) { - accession = pepWithSetMods.Protein.Accession; + accession = pepWithSetMods.Parent.Accession; } else { accession = null; int offset = 0; - foreach (var variant in pepWithSetMods.Protein.AppliedSequenceVariations.OrderBy(v => v.OneBasedBeginPosition)) + foreach (var variant in pepWithSetMods.Parent.AppliedSequenceVariations.OrderBy(v => v.OneBasedBeginPosition)) { bool modIsBeforeVariant = indexInProtein < variant.OneBasedBeginPosition + offset; bool modIsOnVariant = variant.OneBasedBeginPosition + offset <= indexInProtein && @@ -162,13 +162,13 @@ protected override MetaMorpheusEngineResults RunSpecific() if (modIsOnVariant) { - accession = pepWithSetMods.Protein.Accession; + accession = pepWithSetMods.Parent.Accession; break; } if (modIsBeforeVariant) { - accession = pepWithSetMods.Protein.NonVariantProtein.Accession; + accession = pepWithSetMods.Parent.ConsensusVariant.Accession; adjustedSite = indexInProtein - offset; break; } @@ -178,7 +178,7 @@ protected override MetaMorpheusEngineResults RunSpecific() if (accession == null) { - accession = pepWithSetMods.Protein.NonVariantProtein.Accession; + accession = pepWithSetMods.Parent.ConsensusVariant.Accession; adjustedSite = indexInProtein - offset; } } @@ -222,7 +222,7 @@ private static void AddIndexedMod(ConcurrentDictionary GetPossibleMods(double totalMassToGetTo, IEnumerable allMods, IEnumerable> combos, Tolerance precursorTolerance, PeptideWithSetModifications peptideWithSetModifications) + private static IEnumerable GetPossibleMods(double totalMassToGetTo, IEnumerable allMods, IEnumerable> combos, Tolerance precursorTolerance, IBioPolymerWithSetMods peptideWithSetModifications) { foreach (var Mod in allMods.Where(b => b.ValidModification == true)) { From c6bb9573da167e050ad62ac87f67912322415732 Mon Sep 17 00:00:00 2001 From: nbollis Date: Fri, 25 Jul 2025 14:50:03 -0500 Subject: [PATCH 08/11] Collection optimization --- MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs index 05af122316..acdb8ebdf1 100644 --- a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs +++ b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs @@ -106,15 +106,16 @@ protected override MetaMorpheusEngineResults RunSpecific() 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(precursorMass, GptmdModifications, Combos, FilePathToPrecursorMassTolerance[fileName], pepWithSetMods); double bestScore = originalScore; // Initialize with the original score of the PSM to ensure gptmd only adds if new modified forms scores higher - List<(int site, Modification mod, string proteinAccession)> bestMatches = []; foreach (var mod in possibleModifications) { From cc8d5049289555e09aa7b1630de5b0c596c61c11 Mon Sep 17 00:00:00 2001 From: nbollis Date: Thu, 31 Jul 2025 15:50:18 -0500 Subject: [PATCH 09/11] Added exceptions for terminal mods --- MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs | 2 +- .../EngineLayer/Gptmd/IGptmdFilter.cs | 27 +- MetaMorpheus/Test/IGptmdFilterTests.cs | 857 ++++++++++-------- 3 files changed, 485 insertions(+), 401 deletions(-) diff --git a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs index acdb8ebdf1..c3da640426 100644 --- a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs +++ b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs @@ -138,7 +138,7 @@ protected override MetaMorpheusEngineResults RunSpecific() // plus 2 is to translate from zero based string array index to OneBasedModification index int modSite = pepWithSetMods.OneBasedStartResidue + j + 1; - if (!Filters.All(f => f.Passes(newPep, psm, score, originalScore, matchedIons, j + 2, pepWithSetMods.Length))) + if (!Filters.All(f => f.Passes(newPep, psm, score, originalScore, matchedIons, j + 2, pepWithSetMods.Length, mod))) continue; if (score < bestScore - ScoreTolerance) diff --git a/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs b/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs index 3823c1da41..66d95e0072 100644 --- a/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs +++ b/MetaMorpheus/EngineLayer/Gptmd/IGptmdFilter.cs @@ -1,8 +1,10 @@ -using System; +#nullable enable +using System; using Omics.Fragmentation; using System.Collections.Generic; using Omics; using System.Linq; +using Omics.Modifications; namespace EngineLayer; @@ -17,7 +19,8 @@ bool Passes( double originalScore, List matchedIons, int peptideOneBasedModSite, - int peptideLength); + int peptideLength, + Modification modAttemptingToAdd); bool IEquatable.Equals(IGptmdFilter? other) { @@ -37,7 +40,8 @@ public bool Passes( double originalScore, List matchedIons, int peptideOneBasedModSite, - int peptideLength) + int peptideLength, + Modification modAttemptingToAdd) { return newScore > originalScore; } @@ -49,7 +53,6 @@ public bool Passes( /// public sealed class DualDirectionalIonCoverageFilter : 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, @@ -57,7 +60,8 @@ public bool Passes( double originalScore, List matchedIons, int peptideOneBasedModSite, - int peptideLength) + int peptideLength, + Modification modAttemptingToAdd) { if (matchedIons == null || matchedIons.Count == 0) return false; @@ -76,6 +80,9 @@ public bool Passes( m.NeutralTheoreticalProduct.ResiduePosition < site) ); + if (modAttemptingToAdd.LocationRestriction.Contains("terminal", StringComparison.InvariantCultureIgnoreCase)) + return coveredFromCTerm || coveredFromNTerm; + return coveredFromNTerm && coveredFromCTerm; } } @@ -86,7 +93,6 @@ public bool Passes( /// public sealed class UniDirectionalIonCoverageFilter : 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, @@ -94,7 +100,8 @@ public bool Passes( double originalScore, List matchedIons, int peptideOneBasedModSite, - int peptideLength) + int peptideLength, + Modification modAttemptingToAdd) { if (matchedIons == null || matchedIons.Count == 0) return false; @@ -131,7 +138,8 @@ public bool Passes( double originalScore, List matchedIons, int peptideOneBasedModSite, - int peptideLength) + int peptideLength, + Modification modAttemptingToAdd) { if (matchedIons == null || matchedIons.Count == 0) return false; @@ -144,6 +152,9 @@ public bool Passes( 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/Test/IGptmdFilterTests.cs b/MetaMorpheus/Test/IGptmdFilterTests.cs index 12ff542102..113f028941 100644 --- a/MetaMorpheus/Test/IGptmdFilterTests.cs +++ b/MetaMorpheus/Test/IGptmdFilterTests.cs @@ -5,416 +5,489 @@ using Chemistry; using EngineLayer; using MassSpectrometry; +using Omics.Modifications; +using System.Linq; -namespace Test +namespace Test; + +[TestFixture] +public class GptmdFilterTests { - [TestFixture] - public class IGptmdFilterTests + // 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) { - // Helper to create a dummy PeptideWithSetModifications - private PeptideWithSetModifications DummyPeptide() => new("PEPTIDE", []); - - // 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); - } + 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); - - 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); - - 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); - - 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); - - 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); - - 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); - - 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); - - Assert.That(result, Is.True); - } - - [Test] - public void FlankingIonCoverageFilter_Passes_ReturnsFalseIfOnlyLeftFlankCovered() + [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 { - 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); - - Assert.That(result, Is.False); - } - - [Test] - public void FlankingIonCoverageFilter_Passes_ReturnsFalseIfOnlyRightFlankCovered() + 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 { - 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); - - Assert.That(result, Is.False); - } - - [Test] - public void ImprovedScoreFilter_Passes_ReturnsFalseIfScoresAreEqual() + 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 { - var filter = new ImprovedScoreFilter(); - bool result = filter.Passes( - DummyPeptide(), - DummySpectralMatch(), - newScore: 1.0, - originalScore: 1.0, - matchedIons: null, - peptideOneBasedModSite: 1, - peptideLength: 7); - - Assert.That(result, Is.False); - } - - [Test] - public void DualDirectionalIonCoverageFilter_Passes_ReturnsFalseIfEmptyMatchedIons() + 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 { - var filter = new DualDirectionalIonCoverageFilter(); - bool result = filter.Passes( - DummyPeptide(), - DummySpectralMatch(), - 0, 0, - new List(), - peptideOneBasedModSite: 3, - peptideLength: 7); - - Assert.That(result, Is.False); - } - - [Test] - public void DualDirectionalIonCoverageFilter_Passes_ReturnsTrueIfFivePrimeAndThreePrimeCovered() + 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 { - 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); - - Assert.That(result, Is.True); - } - - [Test] - public void FlankingIonCoverageFilter_Passes_ReturnsFalseIfEmptyMatchedIons() + 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 { - var filter = new FlankingIonCoverageFilter(); - bool result = filter.Passes( - DummyPeptide(), - DummySpectralMatch(), - 0, 0, - new List(), - peptideOneBasedModSite: 3, - peptideLength: 7); - - Assert.That(result, Is.False); - } - - [Test] - public void FlankingIonCoverageFilter_Passes_ReturnsFalseIfBothFlanksAreSameIon() + 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 { - 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); - - Assert.That(result, Is.False); - } - - [Test] - public void UniDirectionalIonCoverageFilter_Passes_ReturnsFalseIfNoMatchedIons() + 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 { - var filter = new UniDirectionalIonCoverageFilter(); - bool result = filter.Passes( - DummyPeptide(), - DummySpectralMatch(), - 0, 0, - matchedIons: null, - peptideOneBasedModSite: 3, - peptideLength: 7); - - Assert.That(result, Is.False); - } - - [Test] - public void UniDirectionalIonCoverageFilter_Passes_ReturnsFalseIfEmptyMatchedIons() + 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 { - var filter = new UniDirectionalIonCoverageFilter(); - bool result = filter.Passes( - DummyPeptide(), - DummySpectralMatch(), - 0, 0, - new List(), - peptideOneBasedModSite: 3, - peptideLength: 7); - - Assert.That(result, Is.False); - } - - [Test] - public void UniDirectionalIonCoverageFilter_Passes_ReturnsTrueIfCoveredFromNTerm() + 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 { - 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); - - Assert.That(result, Is.True); - } - - [Test] - public void UniDirectionalIonCoverageFilter_Passes_ReturnsTrueIfCoveredFromCTerm() + 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 { - 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); - - Assert.That(result, Is.True); - } - - [Test] - public void UniDirectionalIonCoverageFilter_Passes_ReturnsTrueIfCoveredFromFivePrime() + 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 { - 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); - - Assert.That(result, Is.True); - } - - [Test] - public void UniDirectionalIonCoverageFilter_Passes_ReturnsTrueIfCoveredFromThreePrime() + 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 { - 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); - - Assert.That(result, Is.True); - } - - [Test] - public void UniDirectionalIonCoverageFilter_Passes_ReturnsFalseIfNeitherDirectionCovered() + 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 { - 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); - - Assert.That(result, Is.False); - } + 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); } + } From 0044f02ab8848975970aa6bb43d6fe55f518b3d4 Mon Sep 17 00:00:00 2001 From: nbollis Date: Thu, 31 Jul 2025 16:11:35 -0500 Subject: [PATCH 10/11] Cleanup --- MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs | 2 -- MetaMorpheus/TaskLayer/MetaMorpheusTask.cs | 4 ---- .../Test/{IGptmdFilterTests.cs => GptmdFilterTests.cs} | 0 3 files changed, 6 deletions(-) rename MetaMorpheus/Test/{IGptmdFilterTests.cs => GptmdFilterTests.cs} (100%) diff --git a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs index efc1ae12b5..201079c366 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; @@ -43,7 +42,6 @@ public GptmdEngine( FilePathToPrecursorMassTolerance = filePathToPrecursorMassTolerance; ModDictionary = modDictionary ?? new Dictionary>>(); Filters = filters ?? new List(); - } public static bool ModFits(Modification attemptToLocalize, IBioPolymer protein, int peptideOneBasedIndex, int peptideLength, int proteinOneBasedIndex) diff --git a/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs b/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs index 3a5f676663..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,9 +24,6 @@ using Proteomics.ProteolyticDigestion; using Transcriptomics; using Transcriptomics.Digestion; -using Easy.Common.Extensions; -using EngineLayer.Gptmd; -using Readers; namespace TaskLayer { diff --git a/MetaMorpheus/Test/IGptmdFilterTests.cs b/MetaMorpheus/Test/GptmdFilterTests.cs similarity index 100% rename from MetaMorpheus/Test/IGptmdFilterTests.cs rename to MetaMorpheus/Test/GptmdFilterTests.cs From b040195e7cf0a8716bf31a8c6f0924d02c6b0842 Mon Sep 17 00:00:00 2001 From: Zhuoxin Shi Date: Mon, 11 Aug 2025 18:23:13 -0500 Subject: [PATCH 11/11] bug --- MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs index 201079c366..6ac1822715 100644 --- a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs +++ b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs @@ -135,7 +135,7 @@ protected override MetaMorpheusEngineResults RunSpecific() 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 + 1; + int modSite = pepWithSetMods.OneBasedStartResidue + j; if (!Filters.All(f => f.Passes(newPep, psm, score, originalScore, matchedIons, j + 2, pepWithSetMods.Length, mod))) continue;