diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000..5b462cccda
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,4 @@
+root = true
+
+[*]
+insert_final_newline = true
diff --git a/.gitignore b/.gitignore
index 9f446d4ced..0a3cf251d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -234,4 +234,6 @@ _Pvt_Extensions
# Macintosh files
**/.DS_Store
+/.opencode/
/AGENTS.md
+/.serena
diff --git a/MetaMorpheus/CMD/CMD.csproj b/MetaMorpheus/CMD/CMD.csproj
index 13fa31edc9..370c3f309e 100644
--- a/MetaMorpheus/CMD/CMD.csproj
+++ b/MetaMorpheus/CMD/CMD.csproj
@@ -24,7 +24,7 @@
-
+
@@ -36,4 +36,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/MetaMorpheus/CMD/Properties/launchSettings.json b/MetaMorpheus/CMD/Properties/launchSettings.json
new file mode 100644
index 0000000000..c92b8a2b35
--- /dev/null
+++ b/MetaMorpheus/CMD/Properties/launchSettings.json
@@ -0,0 +1,11 @@
+{
+ "profiles": {
+ "CMD": {
+ "commandName": "Project"
+ },
+ "WSL": {
+ "commandName": "WSL2",
+ "distributionName": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/MetaMorpheus/EngineLayer/ClassicSearch/ClassicSearchEngine.cs b/MetaMorpheus/EngineLayer/ClassicSearch/ClassicSearchEngine.cs
index a275e1e678..4c1c0af479 100644
--- a/MetaMorpheus/EngineLayer/ClassicSearch/ClassicSearchEngine.cs
+++ b/MetaMorpheus/EngineLayer/ClassicSearch/ClassicSearchEngine.cs
@@ -158,7 +158,7 @@ protected override MetaMorpheusEngineResults RunSpecific()
// check if we've already generated theoretical fragments for this peptide+dissociation type
if (peptideTheorProducts.Count == 0)
{
- specificBioPolymer.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts);
+ specificBioPolymer.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts, CommonParameters.FragmentationParameters);
}
// match theoretical target ions to spectrum
@@ -205,7 +205,7 @@ private void DecoyScoreForSpectralLibrarySearch(ScanWithIndexAndNotchInfo scan,
if (decoyTheoreticalFragments.Count == 0)
{
- reversedOnTheFlyDecoy.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, decoyTheoreticalFragments);
+ reversedOnTheFlyDecoy.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, decoyTheoreticalFragments, CommonParameters.FragmentationParameters);
}
Ms2ScanWithSpecificMass theScan = ArrayOfSortedMS2Scans[scan.ScanIndex];
var decoyMatchedIons = MatchFragmentIons(theScan, decoyTheoreticalFragments, CommonParameters,
diff --git a/MetaMorpheus/EngineLayer/CommonParameters.cs b/MetaMorpheus/EngineLayer/CommonParameters.cs
index f745ce5f48..139edccc1c 100644
--- a/MetaMorpheus/EngineLayer/CommonParameters.cs
+++ b/MetaMorpheus/EngineLayer/CommonParameters.cs
@@ -7,9 +7,9 @@
using System.Reflection;
using Nett;
using Omics.Digestion;
-using Omics.Fragmentation.Peptide;
using Transcriptomics.Digestion;
using EngineLayer.DIA;
+using Transcriptomics;
namespace EngineLayer
{
@@ -47,7 +47,8 @@ public CommonParameters(
bool trimMs1Peaks = false,
bool trimMsMsPeaks = true,
Tolerance productMassTolerance = null,
- Tolerance precursorMassTolerance = null,
+ Tolerance precursorMassTolerance = null,
+ Tolerance productMassTolerance_LowRes = null,
Tolerance deconvolutionMassTolerance = null,
int maxThreadsToUsePerFile = -1,
IDigestionParams digestionParams = null,
@@ -60,7 +61,8 @@ public CommonParameters(
DeconvolutionParameters precursorDeconParams = null,
DeconvolutionParameters productDeconParams = null,
bool useMostAbundantPrecursorIntensity = true,
- DIAparameters diaParameters = null)
+ DIAparameters diaParameters = null,
+ IFragmentationParams fragmentationParams = null)
{
TaskDescriptor = taskDescriptor;
@@ -84,6 +86,7 @@ public CommonParameters(
MaxThreadsToUsePerFile = maxThreadsToUsePerFile == -1 ? Environment.ProcessorCount > 1 ? Environment.ProcessorCount - 1 : 1 : maxThreadsToUsePerFile;
ProductMassTolerance = productMassTolerance ?? new PpmTolerance(20);
PrecursorMassTolerance = precursorMassTolerance ?? new PpmTolerance(5);
+ ProductMassTolerance_LowRes = productMassTolerance_LowRes ?? ProductMassTolerance;
DeconvolutionMassTolerance = deconvolutionMassTolerance ?? new PpmTolerance(4);
DigestionParams = digestionParams ?? new DigestionParams();
DissociationType = dissociationType;
@@ -119,11 +122,13 @@ public CommonParameters(
ListOfModsFixed = listOfModsFixed ?? new List<(string, string)>();
PrecursorDeconvolutionParameters.AverageResidueModel = new OxyriboAveragine();
ProductDeconvolutionParameters.AverageResidueModel = new OxyriboAveragine();
+ FragmentationParameters = fragmentationParams ?? RnaFragmentationParams.Default;
}
else
{
ListOfModsVariable = listOfModsVariable ?? new List<(string, string)> { ("Common Variable", "Oxidation on M") };
ListOfModsFixed = listOfModsFixed ?? new List<(string, string)> { ("Common Fixed", "Carbamidomethyl on C"), ("Common Fixed", "Carbamidomethyl on U") };
+ FragmentationParameters = fragmentationParams ?? new FragmentationParams();
}
CustomIons = digestionParams.ProductsFromDissociationType()[DissociationType.Custom];
@@ -157,8 +162,9 @@ public int DeconvolutionMaxAssumedChargeState
[TomlIgnore] public Tolerance DeconvolutionMassTolerance { get; private set; }
public int TotalPartitions { get; set; }
public Tolerance ProductMassTolerance { get; set; } // public setter required for calibration task
+ public Tolerance ProductMassTolerance_LowRes { get; set; }// Wider mass tolerance for lower resolution analyzer (e.g. ion trap). For now, this is a independent parameter, will not be modified by the calibration task.
public Tolerance PrecursorMassTolerance { get; set; } // public setter required for calibration task
- public bool AddCompIons { get; private set; }
+ public bool AddCompIons { get; set; }
///
/// Only peptides/PSMs with Q-Value and Q-Value Notch below this threshold are used for quantification and
/// spectral library generation. If SearchParameters.WriteHighQValuePsms is set to false, only
@@ -199,6 +205,7 @@ public int DeconvolutionMaxAssumedChargeState
public bool UseMostAbundantPrecursorIntensity { get; set; }
public DIAparameters? DIAparameters { get; set; } //only for DIA analysis involving pseudo ms2 scan generation
+ public IFragmentationParams FragmentationParameters { get; set; }
public CommonParameters Clone()
{
@@ -257,6 +264,7 @@ public CommonParameters CloneWithNewTerminus(FragmentationTerminus? terminus = n
TrimMsMsPeaks,
ProductMassTolerance,
PrecursorMassTolerance,
+ ProductMassTolerance_LowRes,
DeconvolutionMassTolerance,
MaxThreadsToUsePerFile,
DigestionParams.Clone(terminus),
@@ -267,7 +275,10 @@ public CommonParameters CloneWithNewTerminus(FragmentationTerminus? terminus = n
MinVariantDepth,
AddTruncations,
PrecursorDeconvolutionParameters,
- ProductDeconvolutionParameters);
+ ProductDeconvolutionParameters,
+ UseMostAbundantPrecursorIntensity,
+ DIAparameters,
+ FragmentationParameters);
}
public void SetCustomProductTypes()
diff --git a/MetaMorpheus/EngineLayer/EngineLayer.csproj b/MetaMorpheus/EngineLayer/EngineLayer.csproj
index d2f7beb085..0cc8ac4775 100644
--- a/MetaMorpheus/EngineLayer/EngineLayer.csproj
+++ b/MetaMorpheus/EngineLayer/EngineLayer.csproj
@@ -29,7 +29,7 @@
-
+
diff --git a/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs b/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs
index 8681df4139..3648d4ffe4 100644
--- a/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs
+++ b/MetaMorpheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs
@@ -55,7 +55,6 @@ public GlycoSearchEngine(List[] globalCsms, Ms2ScanWithSpeci
this.OxoniumIonFilter = oxoniumIonFilter;
this._oglycanDatabase = oglycanDatabase;
this._nglycanDatabase = nglycanDatabase;
-
SecondFragmentIndex = secondFragmentIndex;
PrecusorSearchMode = commonParameters.PrecursorMassTolerance;
ProductSearchMode = new SinglePpmAroundZeroSearchMode(20); //For Oxonium ion only
@@ -324,7 +323,8 @@ private GlycoSpectralMatch CreateGsm(Ms2ScanWithSpecificMass theScan, int scanIn
var PeptideScore = score - DiagnosticIonScore;
- var p = theScan.TheScan.MassSpectrum.Size * CommonParameters.ProductMassTolerance.GetRange(1000).Width / theScan.TheScan.MassSpectrum.Range.Width;
+ var parentProductTolerance = IsLowResolutionScan(theScan) ? CommonParameters.ProductMassTolerance_LowRes : CommonParameters.ProductMassTolerance;
+ var p = theScan.TheScan.MassSpectrum.Size * parentProductTolerance.GetRange(1000).Width / theScan.TheScan.MassSpectrum.Range.Width;
int n = fragmentsForEachGlycoPeptide.Where(v => v.ProductType == ProductType.c || v.ProductType == ProductType.zDot).Count();
@@ -333,7 +333,6 @@ private GlycoSpectralMatch CreateGsm(Ms2ScanWithSpecificMass theScan, int scanIn
foreach (var childScan in theScan.ChildScans)
{
var childFragments = GlycoPeptides.OGlyGetTheoreticalFragments(CommonParameters.MS2ChildScanDissociationType, CommonParameters.CustomIons, peptide, peptideWithMod);
-
var matchedChildIons = MatchFragmentIons(childScan, childFragments, CommonParameters);
n += childFragments.Where(v => v.ProductType == ProductType.c || v.ProductType == ProductType.zDot).Count();
@@ -356,7 +355,8 @@ private GlycoSpectralMatch CreateGsm(Ms2ScanWithSpecificMass theScan, int scanIn
//TO THINK:may think a different way to use childScore
score += childScore;
- p += childScan.TheScan.MassSpectrum.Size * CommonParameters.ProductMassTolerance.GetRange(1000).Width / childScan.TheScan.MassSpectrum.Range.Width;
+ var productTolerance = IsLowResolutionScan(childScan) ? CommonParameters.ProductMassTolerance_LowRes : CommonParameters.ProductMassTolerance;
+ p += childScan.TheScan.MassSpectrum.Size * productTolerance.GetRange(1000).Width / childScan.TheScan.MassSpectrum.Range.Width;
}
@@ -439,12 +439,16 @@ private void FindOGlycan(Ms2ScanWithSpecificMass theScan, int scanIndex, int sco
SortedDictionary modPos = GlycoSpectralMatch.GetPossibleModSites(theScanBestPeptide, Motifs); //list all of the possible glycoslation site/postition
var localizationScan = theScan;
+ var toleranceForLocalizationScan = CommonParameters.ProductMassTolerance;
List products = new List(); // product list for the theoretical fragment ions
//For HCD-pd-ETD or CD-pd-EThcD type of data, we generate the different rpoducts.
if (theScan.ChildScans.Count > 0 && GlycoPeptides.DissociationTypeContainETD(CommonParameters.MS2ChildScanDissociationType, CommonParameters.CustomIons))
{
localizationScan = theScan.ChildScans.First();
+ // For the localization scan, if it is from ion trap, we will use a wider tolerance for the localization.
+ toleranceForLocalizationScan = localizationScan.TheScan.MzAnalyzer == MZAnalyzerType.IonTrap2D ||
+ localizationScan.TheScan.MzAnalyzer == MZAnalyzerType.IonTrap3D ? CommonParameters.ProductMassTolerance_LowRes : CommonParameters.ProductMassTolerance;
theScanBestPeptide.Fragment(DissociationType.ETD, FragmentationTerminus.Both, products);
}
@@ -477,7 +481,7 @@ private void FindOGlycan(Ms2ScanWithSpecificMass theScan, int scanIndex, int sco
if (GraphCheck(modPos, GlycanBoxes[iDLow])) // the glycosite number should be larger than the possible glycan number.
{
LocalizationGraph localizationGraph = new LocalizationGraph(modPos, GlycanBoxes[iDLow], GlycanBoxes[iDLow].ChildGlycanBoxes, iDLow);
- LocalizationGraph.LocalizeOGlycan(localizationGraph, localizationScan, CommonParameters.ProductMassTolerance, products); //create the localization graph with the glycan mass and the possible glycosite.
+ LocalizationGraph.LocalizeOGlycan(localizationGraph, localizationScan, toleranceForLocalizationScan, products); //create the localization graph with the glycan mass and the possible glycosite.
double currentLocalizationScore = localizationGraph.TotalScore;
if (currentLocalizationScore > bestLocalizedScore) //Try to find the best glycanBox with the highest score.
diff --git a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs
index 99c1c4a423..ded6bade32 100644
--- a/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs
+++ b/MetaMorpheus/EngineLayer/Gptmd/GptmdEngine.cs
@@ -136,7 +136,7 @@ protected override MetaMorpheusEngineResults RunSpecific()
var newPep = pepWithSetMods.Localize(pepSeqIndex, mod.MonoisotopicMass.Value);
peptideTheorProducts.Clear();
- newPep.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts);
+ newPep.Fragment(dissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts, CommonParameters.FragmentationParameters);
ms2ScanWithSpecificMass ??= new Ms2ScanWithSpecificMass(scan, precursorMass, precursorCharge, fileName, CommonParameters);
var matchedIons = MatchFragmentIons(ms2ScanWithSpecificMass, peptideTheorProducts, CommonParameters, matchAllCharges: false);
diff --git a/MetaMorpheus/EngineLayer/Indexing/IndexingEngine.cs b/MetaMorpheus/EngineLayer/Indexing/IndexingEngine.cs
index ec51eda000..daf416cb0b 100644
--- a/MetaMorpheus/EngineLayer/Indexing/IndexingEngine.cs
+++ b/MetaMorpheus/EngineLayer/Indexing/IndexingEngine.cs
@@ -160,7 +160,7 @@ protected override MetaMorpheusEngineResults RunSpecific()
for (int peptideId = 0; peptideId < peptides.Count; peptideId++)
{
- peptides[peptideId].Fragment(CommonParameters.DissociationType, CommonParameters.DigestionParams.FragmentationTerminus, fragments);
+ peptides[peptideId].Fragment(CommonParameters.DissociationType, CommonParameters.DigestionParams.FragmentationTerminus, fragments, CommonParameters.FragmentationParameters);
foreach (var theoreticalFragment in fragments)
{
diff --git a/MetaMorpheus/EngineLayer/Localization/LocalizationEngine.cs b/MetaMorpheus/EngineLayer/Localization/LocalizationEngine.cs
index da2f989a44..9d25ba2eea 100644
--- a/MetaMorpheus/EngineLayer/Localization/LocalizationEngine.cs
+++ b/MetaMorpheus/EngineLayer/Localization/LocalizationEngine.cs
@@ -65,7 +65,7 @@ protected override MetaMorpheusEngineResults RunSpecific()
var peptideWithLocalizedMassDiff = peptide.Localize(r, massDifference);
// this is the list of theoretical products for this peptide with mass-difference on this residue
- peptideWithLocalizedMassDiff.Fragment(CommonParameters.DissociationType, CommonParameters.DigestionParams.FragmentationTerminus, productsWithLocalizedMassDiff);
+ peptideWithLocalizedMassDiff.Fragment(CommonParameters.DissociationType, CommonParameters.DigestionParams.FragmentationTerminus, productsWithLocalizedMassDiff, CommonParameters.FragmentationParameters);
var matchedIons = MatchFragmentIons(scanWithSpecificMass, productsWithLocalizedMassDiff, CommonParameters);
diff --git a/MetaMorpheus/EngineLayer/MetaMorpheusEngine.cs b/MetaMorpheus/EngineLayer/MetaMorpheusEngine.cs
index f8191d7fa9..71960e8c71 100644
--- a/MetaMorpheus/EngineLayer/MetaMorpheusEngine.cs
+++ b/MetaMorpheus/EngineLayer/MetaMorpheusEngine.cs
@@ -130,6 +130,9 @@ private static double CalculateAllChargesPeptideScore(MsDataScan thisScan, List<
public static List MatchFragmentIons(Ms2ScanWithSpecificMass scan, List theoreticalProducts, CommonParameters commonParameters, bool matchAllCharges = false)
{
+ // auto-detect low-res scans from mass analyzer metadata and use wider tolerance for ion trap child scans
+ bool isLowRes = IsLowResolutionScan(scan);
+ var productMassTolerance = isLowRes ? commonParameters.ProductMassTolerance_LowRes : commonParameters.ProductMassTolerance;
if (matchAllCharges)
{
return MatchFragmentIonsOfAllCharges(scan, theoreticalProducts, commonParameters);
@@ -153,7 +156,8 @@ public static List MatchFragmentIons(Ms2ScanWithSpecificMass
double theoreticalFragmentMz = Math.Round(product.NeutralMass.ToMz(1) / 1.0005079, 0) * 1.0005079;
var closestMzIndex = scan.TheScan.MassSpectrum.GetClosestPeakIndex(theoreticalFragmentMz);
- if (commonParameters.ProductMassTolerance.Within(scan.TheScan.MassSpectrum.XArray[closestMzIndex], theoreticalFragmentMz))
+
+ if (productMassTolerance.Within(scan.TheScan.MassSpectrum.XArray[closestMzIndex], theoreticalFragmentMz))
{
matchedFragmentIons.Add(new MatchedFragmentIon(product, theoreticalFragmentMz, scan.TheScan.MassSpectrum.YArray[closestMzIndex], 1));
}
@@ -183,7 +187,7 @@ public static List MatchFragmentIons(Ms2ScanWithSpecificMass
// is the mass error acceptable?
if (closestExperimentalMass != null
- && commonParameters.ProductMassTolerance.Within(closestExperimentalMass.MonoisotopicMass, product.NeutralMass)
+ && productMassTolerance.Within(closestExperimentalMass.MonoisotopicMass, product.NeutralMass)
&& Math.Abs(closestExperimentalMass.Charge) <= Math.Abs(scan.PrecursorCharge))//TODO apply this filter before picking the envelope
{
matchedFragmentIons.Add(new MatchedFragmentIon(product, closestExperimentalMass.MonoisotopicMass.ToMz(closestExperimentalMass.Charge),
@@ -211,7 +215,7 @@ public static List MatchFragmentIons(Ms2ScanWithSpecificMass
IsotopicEnvelope closestExperimentalMass = scan.GetClosestExperimentalIsotopicEnvelope(compIonMass);
// is the mass error acceptable?
- if (commonParameters.ProductMassTolerance.Within(closestExperimentalMass.MonoisotopicMass, compIonMass) && closestExperimentalMass.Charge <= scan.PrecursorCharge)
+ if (productMassTolerance.Within(closestExperimentalMass.MonoisotopicMass, compIonMass) && closestExperimentalMass.Charge <= scan.PrecursorCharge)
{
//found the peak, but we don't want to save that m/z because it's the complementary of the observed ion that we "added". Need to create a fake ion instead.
double mz = (scan.PrecursorMass + protonMassShift - closestExperimentalMass.MonoisotopicMass).ToMz(closestExperimentalMass.Charge);
@@ -230,6 +234,8 @@ public static List MatchFragmentIons(Ms2ScanWithSpecificMass
//But for library generation, we need find all the matched peaks with all the different charges.
private static List MatchFragmentIonsOfAllCharges(Ms2ScanWithSpecificMass scan, List theoreticalProducts, CommonParameters commonParameters)
{
+ bool isLowRes = IsLowResolutionScan(scan);
+ var productMassTolerance = isLowRes ? commonParameters.ProductMassTolerance_LowRes : commonParameters.ProductMassTolerance;
var matchedFragmentIons = new List();
var ions = new List();
@@ -249,8 +255,8 @@ private static List MatchFragmentIonsOfAllCharges(Ms2ScanWit
}
//get the range we can accept
- var minMass = commonParameters.ProductMassTolerance.GetMinimumValue(product.NeutralMass);
- var maxMass = commonParameters.ProductMassTolerance.GetMaximumValue(product.NeutralMass);
+ var minMass = productMassTolerance.GetMinimumValue(product.NeutralMass);
+ var maxMass = productMassTolerance.GetMaximumValue(product.NeutralMass);
var closestExperimentalMassList = scan.GetClosestExperimentalIsotopicEnvelopeList(minMass, maxMass);
if (closestExperimentalMassList != null)
{
@@ -259,7 +265,7 @@ private static List MatchFragmentIonsOfAllCharges(Ms2ScanWit
String ion = $"{product.ProductType.ToString()}{ product.FragmentNumber}^{x.Charge}-{product.NeutralLoss}";
if (x != null
&& !ions.Contains(ion)
- && commonParameters.ProductMassTolerance.Within(x.MonoisotopicMass, product.NeutralMass)
+ && productMassTolerance.Within(x.MonoisotopicMass, product.NeutralMass)
&& Math.Abs(x.Charge) <= Math.Abs(scan.PrecursorCharge))//TODO apply this filter before picking the envelope
{
Product temProduct = product;
@@ -274,6 +280,17 @@ private static List MatchFragmentIonsOfAllCharges(Ms2ScanWit
return matchedFragmentIons;
}
+
+ ///
+ /// Determines whether the given scan was acquired on a low-resolution mass analyzer (e.g., ion trap).
+ /// Used to select the appropriate product mass tolerance for fragment ion matching.
+ ///
+ internal static bool IsLowResolutionScan(Ms2ScanWithSpecificMass scan)
+ {
+ var analyzer = scan.TheScan.MzAnalyzer;
+ return analyzer == MZAnalyzerType.IonTrap2D || analyzer == MZAnalyzerType.IonTrap3D;
+ }
+
protected abstract MetaMorpheusEngineResults RunSpecific();
public MetaMorpheusEngineResults Run()
diff --git a/MetaMorpheus/EngineLayer/ModernSearch/ModernSearchEngine.cs b/MetaMorpheus/EngineLayer/ModernSearch/ModernSearchEngine.cs
index 3d1660c149..5292a56c30 100644
--- a/MetaMorpheus/EngineLayer/ModernSearch/ModernSearchEngine.cs
+++ b/MetaMorpheus/EngineLayer/ModernSearch/ModernSearchEngine.cs
@@ -340,7 +340,7 @@ protected SpectralMatch FineScorePeptide(int id, Ms2ScanWithSpecificMass scan, i
{
PeptideWithSetModifications peptide = PeptideIndex[id];
- peptide.Fragment(CommonParameters.DissociationType, FragmentationTerminus.Both, peptideTheorProducts);
+ peptide.Fragment(CommonParameters.DissociationType, FragmentationTerminus.Both, peptideTheorProducts, CommonParameters.FragmentationParameters);
List matchedIons = MatchFragmentIons(scan, peptideTheorProducts, CommonParameters);
diff --git a/MetaMorpheus/EngineLayer/Mods/RnaMods.txt b/MetaMorpheus/EngineLayer/Mods/RnaMods.txt
index 03c1289752..3fe4e6c080 100644
--- a/MetaMorpheus/EngineLayer/Mods/RnaMods.txt
+++ b/MetaMorpheus/EngineLayer/Mods/RnaMods.txt
@@ -1,4 +1,56 @@
-################################## METALS
+
+---------------------------------------------------------------------------
+ UniProt Knowledgebase:
+ Swiss-Prot Protein Knowledgebase
+ TrEMBL Protein Database
+ SIB Swiss Institute of Bioinformatics; Geneva, Switzerland
+ European Bioinformatics Institute (EBI); Hinxton, United Kingdom
+ Protein Information Resource (PIR); Washington DC, USA
+---------------------------------------------------------------------------
+
+Description: Controlled vocabulary of posttranslational modifications (PTM)
+Name: ptmlist.txt
+Release: 2018_01 of 31-Jan-2018
+
+---------------------------------------------------------------------------
+
+ This document lists the posttranslational modifications used in the
+ UniProt knowledgebase (Swiss-Prot and TrEMBL).
+
+ The definition of the posttranslational modifications usage as well as
+ other information is provided in the following format:
+
+ --------- --------------------------- ------------------------------
+ Line code Content Occurrence in an entry
+ --------- --------------------------- ------------------------------
+ ID Identifier (FT description) Once; starts a PTM entry
+ AC Accession (PTM-xxxx) Once
+ FT Feature key Once
+ TG Target Once; two targets separated by
+ a dash in case of intrachain
+ crosslinks
+ PA Position of the modification Optional; once
+ on the amino acid
+ PP Position of the modification Optional; once
+ in the polypeptide
+ CF Correction formula Optional; once
+ MM Monoisotopic mass difference Optional; once
+ MA Average mass difference Optional; once
+ LC Cellular location Optional; once; alternatives
+ can be proposed
+ TR Taxonomic range Optional; once or more
+ KW Keyword Optional; once or more
+ DR Cross-reference to PTM Optional; once or more
+ databases
+ BM Backbone modification Optional; once; alternatives can be
+ proposed; if present, the
+ affected fragment types are
+ listed after the modification
+ // Terminator Once; ends an entry
+
+
+___________________________________________________________________________
+################################## METALS
ID Sodium
TG A or C or G or U
PP Anywhere.
@@ -14,40 +66,77 @@ CF H-1 K1
DR Unimod; 530.
//
################################## Biological
-ID Methylation
-TG A or C or G or U or T or Y
+ID 2'-O-Methyladenosine
+TG A
+PP Anywhere.
+MT Biological
+CF C1 H2
+BL Suppressed
+DR Unimod; 34.
+//
+ID N6-methyladenosine
+TG A
+PP Anywhere.
+MT Biological
+CF C1 H2
+BL Modified
+DR Unimod; 34.
+//
+ID 2'-O-Methyluridine
+TG U
+PP Anywhere.
+MT Biological
+CF C1 H2
+BL Suppressed
+DR Unimod; 34.
+//
+ID 5-Methylcytidine
+TG C
PP Anywhere.
MT Biological
CF C1 H2
+BL Modified:C1H2
DR Unimod; 34.
//
+ID N6,2'-O-dimethyladenosine
+TG A
+PP Anywhere.
+MT Biological
+CF C2 H4
+BL Suppressed:C1H2
+DR Unimod; 36.
+//
+################################## Simple Biological
+ID Methylation
+TG A or C or G or U or T or Y
+PP Anywhere.
+MT Simple Biological
+CF C1 H2
+//
ID Dimethylation
TG A or C or G or U or T or Y
PP Anywhere.
-MT Biological
+MT Simple Biological
CF C2 H4
-DR Unimod; 34.
//
ID MethoxyEthoxylation
TG A or C or G or U
PP Anywhere.
-MT Biological
+MT Simple Biological
CF C3 H6 O1
-DR Unimod; 34.
//
+################################## Base Conversion
ID Deoxylnosine
TG T
PP Anywhere.
-MT Biological
+MT Base Conversion
CF N-1H-1
-DR Unimod; 34.
//
ID Deoxylnosine
TG G
PP Anywhere.
-MT Biological
+MT Base Conversion
CF N-1H-1O-1
-DR Unimod; 34.
//
################################## Common Artificial
ID DeoxyFluoronation
@@ -55,7 +144,6 @@ TG A or C or G or U
PP Anywhere.
MT Common Artificial
CF O-1 H-1 F1
-DR Unimod; 34.
//
################################## Terminal Shifts
ID Cyclic Phosphate
@@ -63,34 +151,55 @@ TG X
PP Oligo 3'-terminal.
MT Digestion Termini
CF H-2 O-1
-DR Unimod; 280.
//
ID Terminal Phosphorylation
TG X
PP Oligo 5'-terminal.
MT Digestion Termini
CF H1 O3 P1
-DR Unimod; 280.
//
ID Terminal Dephosphorylation
TG X
PP Oligo 5'-terminal.
MT Digestion Termini
CF P-1 O-3 H-1
-DR Unimod; 280.
//
ID Pfizer 5'-Cap
TG X
PP 5'-terminal.
MT Standard
CF C13H22N5O14P3
-DR Unimod; 280.
//
################################## Backbone Shift
ID Phosphorothioate
TG X
+FT Backbone
PP Anywhere.
MT Common Variable
CF SO-1
-DR Unimod; 280.
+BM w,x,c,d
+//
+ID Boranophosphate
+TG X
+PP Anywhere.
+FT Backbone
+MT Therapeutic
+CF B1 H3 O-1
+BM w,x,c,d
+//
+ID N3'-Phosphoramidate
+TG X
+PP Anywhere.
+FT Backbone
+MT Therapeutic
+CF N1 H1 O-1 # Replace 3'-O with NH
+BM w,b,c,d
+//
+ID Methylphosphonate
+TG X
+PP Anywhere.
+FT Backbone
+MT Therapeutic
+CF C1 H2 O-1
+BM w,x,c,d
//
\ No newline at end of file
diff --git a/MetaMorpheus/EngineLayer/NonSpecificEnzymeSearch/NonSpecificEnzymeSearchEngine.cs b/MetaMorpheus/EngineLayer/NonSpecificEnzymeSearch/NonSpecificEnzymeSearchEngine.cs
index 85b530b504..ed363e2ab8 100644
--- a/MetaMorpheus/EngineLayer/NonSpecificEnzymeSearch/NonSpecificEnzymeSearchEngine.cs
+++ b/MetaMorpheus/EngineLayer/NonSpecificEnzymeSearch/NonSpecificEnzymeSearchEngine.cs
@@ -124,13 +124,13 @@ protected override MetaMorpheusEngineResults RunSpecific()
foreach (int id in idsOfPeptidesPossiblyObserved.Where(id => scoringTable[id] == maxInitialScore))
{
PeptideWithSetModifications peptide = PeptideIndex[id];
- peptide.Fragment(CommonParameters.DissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts);
+ peptide.Fragment(CommonParameters.DissociationType, CommonParameters.DigestionParams.FragmentationTerminus, peptideTheorProducts, CommonParameters.FragmentationParameters);
Tuple notchAndUpdatedPeptide = Accepts(peptideTheorProducts, scan.PrecursorMass, peptide, CommonParameters.DigestionParams.FragmentationTerminus, MassDiffAcceptor, semiSpecificSearch);
int notch = notchAndUpdatedPeptide.Item1;
if (notch >= 0)
{
peptide = notchAndUpdatedPeptide.Item2;
- peptide.Fragment(CommonParameters.DissociationType, FragmentationTerminus.Both, peptideTheorProducts);
+ peptide.Fragment(CommonParameters.DissociationType, FragmentationTerminus.Both, peptideTheorProducts, CommonParameters.FragmentationParameters);
List matchedIons = MatchFragmentIons(scan, peptideTheorProducts, ModifiedParametersNoComp);
double thisScore = CalculatePeptideScore(scan.TheScan, matchedIons);
diff --git a/MetaMorpheus/EngineLayer/SpectralLibrarySearch/SpectralLibrarySearchFunction.cs b/MetaMorpheus/EngineLayer/SpectralLibrarySearch/SpectralLibrarySearchFunction.cs
index c32832c180..a8630ae29b 100644
--- a/MetaMorpheus/EngineLayer/SpectralLibrarySearch/SpectralLibrarySearchFunction.cs
+++ b/MetaMorpheus/EngineLayer/SpectralLibrarySearch/SpectralLibrarySearchFunction.cs
@@ -60,7 +60,7 @@ public static void CalculateSpectralAngles(SpectralLibrary spectralLibrary, Spec
else if (bestMatch.IsDecoy && spectralLibrary.TryGetSpectrum(bestMatch.SpecificBioPolymer.Description, scan.PrecursorCharge, out var targetlibrarySpectrum))
{
var decoyPeptideTheorProducts = new List();
- bestMatch.SpecificBioPolymer.Fragment(commonParameters.DissociationType, commonParameters.DigestionParams.FragmentationTerminus, decoyPeptideTheorProducts);
+ bestMatch.SpecificBioPolymer.Fragment(commonParameters.DissociationType, commonParameters.DigestionParams.FragmentationTerminus, decoyPeptideTheorProducts, commonParameters.FragmentationParameters);
var decoylibrarySpectrum = LibrarySpectrum.GetDecoyLibrarySpectrumFromTargetByReverse(targetlibrarySpectrum, decoyPeptideTheorProducts);
SpectralSimilarity s = new SpectralSimilarity(scan.TheScan.MassSpectrum, decoylibrarySpectrum.Select(x => x.Mz).ToArray(),
decoylibrarySpectrum.Select(x => x.Intensity).ToArray(), SpectralSimilarity.SpectrumNormalizationScheme.SquareRootSpectrumSum,
diff --git a/MetaMorpheus/GUI/App.xaml b/MetaMorpheus/GUI/App.xaml
index 3b560cfcf2..31a78fda89 100644
--- a/MetaMorpheus/GUI/App.xaml
+++ b/MetaMorpheus/GUI/App.xaml
@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MetaMorpheusGUI"
+ xmlns:guiFunctions="clr-namespace:GuiFunctions;assembly=GuiFunctions"
StartupUri="MainWindow.xaml">
@@ -105,7 +106,21 @@
-
+
+
+
+
@@ -113,5 +128,7 @@
+
+
\ No newline at end of file
diff --git a/MetaMorpheus/GUI/GUI.csproj b/MetaMorpheus/GUI/GUI.csproj
index a264ec25cb..9449ab0152 100644
--- a/MetaMorpheus/GUI/GUI.csproj
+++ b/MetaMorpheus/GUI/GUI.csproj
@@ -63,7 +63,7 @@
-
+
diff --git a/MetaMorpheus/GUI/GUI_h05aw1sq_wpftmp.csproj b/MetaMorpheus/GUI/GUI_h05aw1sq_wpftmp.csproj
new file mode 100644
index 0000000000..4f03a4b587
--- /dev/null
+++ b/MetaMorpheus/GUI/GUI_h05aw1sq_wpftmp.csproj
@@ -0,0 +1,448 @@
+
+
+ MetaMorpheusGUI
+ obj\Debug\
+ obj\
+ C:\Users\Nic\source\repos\MetaMorpheus\MetaMorpheus\GUI\obj\
+ <_TargetAssemblyProjectName>GUI
+
+
+
+ WinExe
+ net8.0-windows
+ true
+ false
+ false
+ false
+ false
+ false
+ false
+ MetaMorpheusGUI
+ MetaMorpheusGUI
+ Debug;Release
+ full
+ true
+ Icons\MMnice.ico
+ true
+
+
+ true
+ false
+ false
+ false
+
+
+ x64
+
+
+ x64
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MetaMorpheus/GUI/MainWindow.xaml b/MetaMorpheus/GUI/MainWindow.xaml
index f0848612ad..ebab7d7fab 100644
--- a/MetaMorpheus/GUI/MainWindow.xaml
+++ b/MetaMorpheus/GUI/MainWindow.xaml
@@ -154,20 +154,6 @@
-
-
-
-
-
+
+
@@ -380,6 +380,15 @@
+
+
+
+
+ Write intermediate search result files during calibration.
+
+
+
+
@@ -435,7 +444,6 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Basic search designed for standard search spaces (tight precursor mass tolerance, strict enzyme specificity)
+
+ Ideal for low RAM machines.
+
+
+
+
+
+
+
+
+ Indexed search designed for massive search spaces (wide/open precursor mass tolerance).
+
+ This search generates an indexed database that is saved to the folder of the original database.
+
+ Subsequent "Modern Searches" are much faster after the creation of the indexed database.
+
+ This search has potentially high RAM requirements, requiring database partitions.
+
+
+
+
+
+
+
+
+
+ Splits indexed search modes (Modern) into separate, smaller searches to reduce RAM requirements. More partitions means less RAM.
+
+ There is no correlation between the number of partitions and the resulting search speed.
+
+
+
+
+
+
+
@@ -519,4 +581,4 @@
-
\ No newline at end of file
+
diff --git a/MetaMorpheus/GUI/TaskWindows/CalibrateTaskWindow.xaml.cs b/MetaMorpheus/GUI/TaskWindows/CalibrateTaskWindow.xaml.cs
index 2906a3b917..5a06d67ced 100644
--- a/MetaMorpheus/GUI/TaskWindows/CalibrateTaskWindow.xaml.cs
+++ b/MetaMorpheus/GUI/TaskWindows/CalibrateTaskWindow.xaml.cs
@@ -1,4 +1,4 @@
-using EngineLayer;
+using EngineLayer;
using GuiFunctions;
using MassSpectrometry;
using MzLibUtil;
@@ -99,8 +99,29 @@ private void UpdateFieldsFromTask(CalibrationTask task)
PrecursorMassToleranceComboBox.SelectedIndex = task.CommonParameters.PrecursorMassTolerance is AbsoluteTolerance ? 0 : 1;
CustomFragmentationWindow = new CustomFragmentationWindow(task.CommonParameters.CustomIons);
writeIndexMzmlCheckbox.IsChecked = task.CalibrationParameters.WriteIndexedMzml;
-
- //writeIntermediateFilesCheckBox.IsChecked = task.CalibrationParameters.WriteIntermediateFiles;
+ NumberOfDatabaseSearchesTextBox.Text = task.CommonParameters.TotalPartitions.ToString(CultureInfo.InvariantCulture);
+
+ //// Set Search Type radio buttons
+ switch (task.CalibrationParameters.SearchType)
+ {
+ case SearchType.Classic:
+ ClassicSearchRadioButton.IsChecked = true;
+ ModernSearchRadioButton.IsChecked = false;
+ break;
+ case SearchType.Modern:
+ ClassicSearchRadioButton.IsChecked = false;
+ ModernSearchRadioButton.IsChecked = true;
+ break;
+ default:
+ MessageBox.Show(
+ $"SearchType '{task.CalibrationParameters.SearchType}' is not supported by the Calibration Task window.",
+ "Unsupported Search Type",
+ MessageBoxButton.OK,
+ MessageBoxImage.Warning);
+ break;
+ }
+
+ writeIntermediateFilesCheckBox.IsChecked = task.CalibrationParameters.WriteIntermediateFiles;
MinScoreAllowed.Text = task.CommonParameters.ScoreCutoff.ToString(CultureInfo.InvariantCulture);
@@ -159,7 +180,7 @@ private void UpdateFieldsFromTask(CalibrationTask task)
private void PopulateChoices()
{
- bool isRnaMode = GuiGlobalParamsViewModel.Instance.IsRnaMode;
+ bool isRnaMode = GuiGlobalParamsViewModel.Instance.IsRnaMode;
List modsToUse = isRnaMode ? GlobalVariables.AllRnaModsKnown.ToList() : GlobalVariables.AllModsKnown.ToList();
foreach (string dissassociationType in GlobalVariables.AllSupportedDissociationTypes.Keys)
@@ -236,7 +257,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
if (!TaskValidator.CheckTaskSettingsValidity(PrecursorMassToleranceTextBox.Text, ProductMassToleranceTextBox.Text, MissedCleavagesTextBox.Text,
MaxModificationIsoformsTextBox.Text, MinPeptideLengthTextBox.Text, MaxPeptideLengthTextBox.Text, MaxThreadsTextBox.Text, MinScoreAllowed.Text,
- fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, null, null, fieldNotUsed, MaxModsPerPeptideTextBox.Text, fieldNotUsed,
+ fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, fieldNotUsed, null, null, fieldNotUsed, MaxModsPerPeptideTextBox.Text, fieldNotUsed,
null, null, null))
{
return;
@@ -332,11 +353,12 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
assumeOrphanPeaksAreZ1Fragments: protease.Name != "top-down" && !isRnaMode,
minVariantDepth: minVariantDepth,
maxHeterozygousVariants: maxHeterozygousVariants,
+ totalPartitions: int.Parse(NumberOfDatabaseSearchesTextBox.Text, CultureInfo.InvariantCulture),
trimMsMsPeaks: false,
doPrecursorDeconvolution: doPrecursorDeconvolution,
precursorDeconParams: precursorDeconvolutionParameters,
productDeconParams: productDeconvolutionParameters,
- useProvidedPrecursorInfo: useProvidedPrecursorInfo);
+ useProvidedPrecursorInfo: useProvidedPrecursorInfo);
TheTask.CommonParameters = commonParamsToSave;
}
else //bottom-up
@@ -354,6 +376,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
assumeOrphanPeaksAreZ1Fragments: protease.Name != "top-down",
minVariantDepth: minVariantDepth,
maxHeterozygousVariants: maxHeterozygousVariants,
+ totalPartitions: int.Parse(NumberOfDatabaseSearchesTextBox.Text, CultureInfo.InvariantCulture),
useProvidedPrecursorInfo: useProvidedPrecursorInfo,
doPrecursorDeconvolution: doPrecursorDeconvolution,
precursorDeconParams: precursorDeconvolutionParameters,
@@ -362,6 +385,24 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
}
TheTask.CalibrationParameters.WriteIndexedMzml = writeIndexMzmlCheckbox.IsChecked.Value;
+ TheTask.CalibrationParameters.WriteIntermediateFiles = writeIntermediateFilesCheckBox.IsChecked.Value;
+ if (ModernSearchRadioButton.IsChecked == true)
+ {
+ TheTask.CalibrationParameters.SearchType = SearchType.Modern;
+ }
+ else if (ClassicSearchRadioButton.IsChecked == true)
+ {
+ TheTask.CalibrationParameters.SearchType = SearchType.Classic;
+ }
+ else
+ {
+ MessageBox.Show(
+ "No search type is selected. Please select Classic or Modern search.",
+ "No Search Type Selected",
+ MessageBoxButton.OK,
+ MessageBoxImage.Warning);
+ return;
+ }
DialogResult = true;
}
diff --git a/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml b/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml
index 7ff8028446..5e48599319 100644
--- a/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml
+++ b/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml
@@ -238,23 +238,41 @@
-
-
-
+
+
-
+
-
+
+
+
+
+
+
-
+
diff --git a/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml.cs b/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml.cs
index b9a212e0bd..00d6eee99c 100644
--- a/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml.cs
+++ b/MetaMorpheus/GUI/TaskWindows/GlycoSearchTaskWindow.xaml.cs
@@ -90,6 +90,8 @@ private void PopulateChoices()
productMassToleranceComboBox.Items.Add("Da");
productMassToleranceComboBox.Items.Add("ppm");
+ productMassTolerance_LowResComboBox.Items.Add("Da");
+ productMassTolerance_LowResComboBox.Items.Add("ppm");
foreach (var hm in GlobalVariables.AllModsKnown.Where(b => b.ValidModification == true).GroupBy(b => b.ModificationType))
{
@@ -184,6 +186,8 @@ private void UpdateFieldsFromTask(GlycoSearchTask task)
TxtBoxMaxModPerPep.Text = task.CommonParameters.DigestionParams.MaxMods.ToString(CultureInfo.InvariantCulture);
productMassToleranceTextBox.Text = task.CommonParameters.ProductMassTolerance.Value.ToString(CultureInfo.InvariantCulture);
productMassToleranceComboBox.SelectedIndex = task.CommonParameters.ProductMassTolerance is AbsoluteTolerance ? 0 : 1;
+ productMassTolerance_LowResTextBox.Text = task.CommonParameters.ProductMassTolerance_LowRes?.Value.ToString(CultureInfo.InvariantCulture);
+ productMassTolerance_LowResComboBox.SelectedIndex = task.CommonParameters.ProductMassTolerance_LowRes == null || task.CommonParameters.ProductMassTolerance_LowRes is AbsoluteTolerance ? 0 : 1;
minScoreAllowed.Text = task.CommonParameters.ScoreCutoff.ToString(CultureInfo.InvariantCulture);
numberOfDatabaseSearchesTextBox.Text = task.CommonParameters.TotalPartitions.ToString(CultureInfo.InvariantCulture);
maxThreadsTextBox.Text = task.CommonParameters.MaxThreadsToUsePerFile.ToString(CultureInfo.InvariantCulture);
@@ -259,8 +263,9 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
if (!TaskValidator.CheckTaskSettingsValidity(PrecusorMsTlTextBox.Text, productMassToleranceTextBox.Text, missedCleavagesTextBox.Text,
maxModificationIsoformsTextBox.Text, MinPeptideLengthTextBox.Text, MaxPeptideLengthTextBox.Text, maxThreadsTextBox.Text, minScoreAllowed.Text,
- fieldNotUsed, fieldNotUsed, fieldNotUsed, DeconHostViewModel.PrecursorDeconvolutionParameters.MaxAssumedChargeState.ToString(), TopNPeaksTextBox.Text, MinRatioTextBox.Text, null, null, numberOfDatabaseSearchesTextBox.Text, TxtBoxMaxModPerPep.Text,
- fieldNotUsed, null, null, null))
+ fieldNotUsed, fieldNotUsed, fieldNotUsed, DeconHostViewModel.PrecursorDeconvolutionParameters.MaxAssumedChargeState.ToString(), TopNPeaksTextBox.Text, MinRatioTextBox.Text, null, null, numberOfDatabaseSearchesTextBox.Text, TxtBoxMaxModPerPep.Text,
+ fieldNotUsed, null, null, null,
+ productMassTolerance_LowRes: productMassTolerance_LowResTextBox.Text))
{
return;
}
@@ -362,7 +367,32 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
ProductMassTolerance = new PpmTolerance(double.Parse(productMassToleranceTextBox.Text, CultureInfo.InvariantCulture));
}
- Tolerance PrecursorMassTolerance;
+ Tolerance ProductMassTolerance_lowRes;
+ var productMassTolerance_LowResToleranceText = productMassTolerance_LowResTextBox.Text;
+ if (string.IsNullOrWhiteSpace(productMassTolerance_LowResToleranceText))
+ {
+ // If no child scan mass tolerance is specified, fall back to product mass tolerance
+ ProductMassTolerance_lowRes = ProductMassTolerance;
+ }
+ else
+ {
+ // we already validate via non-positive TaskValidator, but this local guard is still useful as defensive programming in case validation flow changes later.
+ if (!double.TryParse(productMassTolerance_LowResToleranceText, NumberStyles.Any, CultureInfo.InvariantCulture, out double parsedChildTolerance) || parsedChildTolerance <= 0)
+ {
+ MessageBox.Show("The low-resolution product mass tolerance is invalid. Please enter a positive number.");
+ return;
+ }
+ if (productMassTolerance_LowResComboBox.SelectedIndex == 0)
+ {
+ ProductMassTolerance_lowRes = new AbsoluteTolerance(parsedChildTolerance);
+ }
+ else
+ {
+ ProductMassTolerance_lowRes = new PpmTolerance(parsedChildTolerance);
+ }
+ }
+
+ Tolerance PrecursorMassTolerance;
if (cbbPrecusorMsTl.SelectedIndex == 0)
{
PrecursorMassTolerance = new AbsoluteTolerance(double.Parse(PrecusorMsTlTextBox.Text, CultureInfo.InvariantCulture));
@@ -394,6 +424,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
precursorMassTolerance: PrecursorMassTolerance,
taskDescriptor: OutputFileNameTextBox.Text != "" ? OutputFileNameTextBox.Text : "GlycoSearchTask",
productMassTolerance: ProductMassTolerance,
+ productMassTolerance_LowRes: ProductMassTolerance_lowRes,
doPrecursorDeconvolution: doPrecursorDeconvolution,
useProvidedPrecursorInfo: useProvidedPrecursorInfo,
digestionParams: digestionParamsToSave,
diff --git a/MetaMorpheus/GUI/TaskWindows/SearchTaskWindow.xaml b/MetaMorpheus/GUI/TaskWindows/SearchTaskWindow.xaml
index f4a3f17d5d..1abe6f249e 100644
--- a/MetaMorpheus/GUI/TaskWindows/SearchTaskWindow.xaml
+++ b/MetaMorpheus/GUI/TaskWindows/SearchTaskWindow.xaml
@@ -13,7 +13,6 @@
-
@@ -101,6 +100,7 @@
+
@@ -264,7 +264,7 @@
-
+
-
+
+ IsChecked="False" Margin="5,5,2,0" />
@@ -297,6 +297,39 @@
+
+
+
+
+
+
+ By default, MetaMorpheus uses this machine's number of threads minus one to maximize search speed while letting you use your computer for other purposes.
+
+ Enter a lower number to limit CPU usage.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -642,10 +675,7 @@
-
-
-
-
+
@@ -684,36 +714,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1005,92 +1005,8 @@
-
-
-
-
-
-
- Generates experimental (not theoretical) complementary ions for each MS2.
-
- Useful for localization of modifications.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Search for theoretical internal fragment ions AFTER scoring. These ions are useful for localizing PTMs, but are not used during scoring.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Indexed Search Only: Limits the maximum MS2 fragment mass to avoid rare crashes.
-
- A very high value (e.g. 30000) does not impact performance.
-
-
-
-
-
-
-
-
-
- By default, MetaMorpheus uses this machine's number of threads minus one to maximize search speed while letting you use your computer for other purposes.
-
- Enter a lower number to limit CPU usage.
-
-
-
-
-
-
+
+
diff --git a/MetaMorpheus/GUI/TaskWindows/SearchTaskWindow.xaml.cs b/MetaMorpheus/GUI/TaskWindows/SearchTaskWindow.xaml.cs
index efe82b1789..df2f2e58ea 100644
--- a/MetaMorpheus/GUI/TaskWindows/SearchTaskWindow.xaml.cs
+++ b/MetaMorpheus/GUI/TaskWindows/SearchTaskWindow.xaml.cs
@@ -1,8 +1,11 @@
using EngineLayer;
+using GuiFunctions;
using MassSpectrometry;
using MzLibUtil;
using Nett;
+using Omics.Digestion;
using Omics.Fragmentation;
+using Omics.Modifications;
using Proteomics.ProteolyticDigestion;
using System;
using System.Collections.Generic;
@@ -15,11 +18,9 @@
using System.Windows.Controls;
using System.Windows.Input;
using TaskLayer;
-using UsefulProteomicsDatabases;
-using GuiFunctions;
-using Omics.Digestion;
-using Omics.Modifications;
+using Transcriptomics;
using Transcriptomics.Digestion;
+using UsefulProteomicsDatabases;
namespace MetaMorpheusGUI
{
@@ -38,6 +39,7 @@ public partial class SearchTaskWindow : Window
private bool AutomaticallyAskAndOrUpdateParametersBasedOnProtease = true;
private CustomFragmentationWindow CustomFragmentationWindow;
private MassDifferenceAcceptorSelectionViewModel _massDifferenceAcceptorViewModel;
+ private FragmentationParamsViewModel _fragmentationParamsViewModel;
private string _defaultMultiplexType = "TMT10";
private DeconHostViewModel DeconHostViewModel;
@@ -53,7 +55,7 @@ public SearchTaskWindow(SearchTask task)
{
Title = "RNA Search Task";
TheTask.SearchParameters = new RnaSearchParameters();
- TheTask.CommonParameters = new CommonParameters("RnaSearchTask", digestionParams: new RnaDigestionParams("RNase T1", 3), dissociationType: DissociationType.CID, deconvolutionMaxAssumedChargeState: -20, precursorMassTolerance: new PpmTolerance(15));
+ TheTask.CommonParameters = new CommonParameters("RnaSearchTask", digestionParams: new RnaDigestionParams("RNase T1", 3), dissociationType: DissociationType.CID, deconvolutionMaxAssumedChargeState: -20, precursorMassTolerance: new PpmTolerance(15), fragmentationParams: new RnaFragmentationParams());
}
else
{
@@ -307,20 +309,14 @@ private void UpdateFieldsFromTask(SearchTask task)
MissedCleavagesTextBox.Text = task.CommonParameters.DigestionParams.MaxMissedCleavages == int.MaxValue ? "" : task.CommonParameters.DigestionParams.MaxMissedCleavages.ToString(CultureInfo.InvariantCulture);
MinPeptideLengthTextBox.Text = task.CommonParameters.DigestionParams.MinLength.ToString(CultureInfo.InvariantCulture);
MaxPeptideLengthTextBox.Text = task.CommonParameters.DigestionParams.MaxLength == int.MaxValue ? "" : task.CommonParameters.DigestionParams.MaxLength.ToString(CultureInfo.InvariantCulture);
- MaxFragmentMassTextBox.Text = task.SearchParameters.MaxFragmentSize.ToString(CultureInfo.InvariantCulture); //put after max peptide length to allow for override of auto
maxModificationIsoformsTextBox.Text = task.CommonParameters.DigestionParams.MaxModificationIsoforms.ToString(CultureInfo.InvariantCulture);
MaxModNumTextBox.Text = task.CommonParameters.DigestionParams.MaxMods.ToString(CultureInfo.InvariantCulture);
DissociationTypeComboBox.SelectedItem = task.CommonParameters.DissociationType.ToString();
SeparationTypeComboBox.SelectedItem = task.CommonParameters.SeparationType.ToString();
- NTerminalIons.IsChecked = task.CommonParameters.DigestionParams.FragmentationTerminus == FragmentationTerminus.Both || task.CommonParameters.DigestionParams.FragmentationTerminus == FragmentationTerminus.N;
- CTerminalIons.IsChecked = task.CommonParameters.DigestionParams.FragmentationTerminus == FragmentationTerminus.Both || task.CommonParameters.DigestionParams.FragmentationTerminus == FragmentationTerminus.C;
- InternalIonsCheckBox.IsChecked = task.SearchParameters.MinAllowedInternalFragmentLength != 0;
- MinInternalFragmentLengthTextBox.Text = task.SearchParameters.MinAllowedInternalFragmentLength.ToString();
ProductMassToleranceTextBox.Text = task.CommonParameters.ProductMassTolerance.Value.ToString(CultureInfo.InvariantCulture);
ProductMassToleranceComboBox.SelectedIndex = task.CommonParameters.ProductMassTolerance is AbsoluteTolerance ? 0 : 1;
PrecursorMassToleranceTextBox.Text = task.CommonParameters.PrecursorMassTolerance.Value.ToString(CultureInfo.InvariantCulture);
PrecursorMassToleranceComboBox.SelectedIndex = task.CommonParameters.PrecursorMassTolerance is AbsoluteTolerance ? 0 : 1;
- AddCompIonCheckBox.IsChecked = task.CommonParameters.AddCompIons;
NumberOfDatabaseSearchesTextBox.Text = task.CommonParameters.TotalPartitions.ToString(CultureInfo.InvariantCulture);
RemoveContaminantRadioBox.IsChecked = task.SearchParameters.TCAmbiguity == TargetContaminantAmbiguity.RemoveContaminant;
RemoveTargetRadioBox.IsChecked = task.SearchParameters.TCAmbiguity == TargetContaminantAmbiguity.RemoveTarget;
@@ -441,6 +437,9 @@ private void UpdateFieldsFromTask(SearchTask task)
_massDifferenceAcceptorViewModel = new(task.SearchParameters.MassDiffAcceptorType, task.SearchParameters.CustomMdac, task.CommonParameters.PrecursorMassTolerance.Value);
WritePrunedDBCheckBox.IsChecked = task.SearchParameters.WritePrunedDatabase;
UpdateModSelectionGrid();
+
+ _fragmentationParamsViewModel = new FragmentationParamsViewModel(task.CommonParameters, task.SearchParameters);
+ FragmentationParametersControl.DataContext = _fragmentationParamsViewModel;
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
@@ -472,11 +471,11 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
WindowWidthThomsonsTextBox.Text,
NumberOfWindowsTextBox.Text,
NumberOfDatabaseSearchesTextBox.Text,
- MaxModNumTextBox.Text,
- MaxFragmentMassTextBox.Text,
+ MaxModNumTextBox.Text,
+ _fragmentationParamsViewModel.MaxFragmentMassDa.ToString(),
QValueThresholdTextBox.Text,
- PepQValueThresholdTextBox.Text,
- InternalIonsCheckBox.IsChecked.Value ? MinInternalFragmentLengthTextBox.Text : null))
+ PepQValueThresholdTextBox.Text,
+ _fragmentationParamsViewModel.GenerateInternalIons ? _fragmentationParamsViewModel.MinInternalIonLength.ToString() : null))
{
return;
}
@@ -560,7 +559,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
{
PrecursorMassTolerance = new PpmTolerance(double.Parse(PrecursorMassToleranceTextBox.Text, CultureInfo.InvariantCulture));
}
- TheTask.SearchParameters.MaxFragmentSize = double.Parse(MaxFragmentMassTextBox.Text, CultureInfo.InvariantCulture);
+ TheTask.SearchParameters.MaxFragmentSize = _fragmentationParamsViewModel.MaxFragmentMassDa;
var listOfModsVariable = new List<(string, string)>();
foreach (var heh in VariableModTypeForTreeViewObservableCollection)
@@ -642,12 +641,13 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
windowWidthThomsons: windowWidthThompsons,
numberOfWindows: numberOfWindows,//maybe change this some day
normalizePeaksAccrossAllWindows: normalizePeaksAccrossAllWindows,//maybe change this some day
- addCompIons: AddCompIonCheckBox.IsChecked.Value,
+ addCompIons: _fragmentationParamsViewModel.GenerateComplementaryIons,
assumeOrphanPeaksAreZ1Fragments: protease.Name != "top-down",
minVariantDepth: MinVariantDepth,
maxHeterozygousVariants: MaxHeterozygousVariants,
precursorDeconParams: precursorDeconvolutionParameters,
- productDeconParams: productDeconvolutionParameters);
+ productDeconParams: productDeconvolutionParameters,
+ fragmentationParams: _fragmentationParamsViewModel.ToFragmentationParams() );
if (ClassicSearchRadioButton.IsChecked.Value)
{
@@ -668,7 +668,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
return;
}
- TheTask.SearchParameters.MinAllowedInternalFragmentLength = InternalIonsCheckBox.IsChecked.Value ? Convert.ToInt32(MinInternalFragmentLengthTextBox.Text) : 0;
+ TheTask.SearchParameters.MinAllowedInternalFragmentLength = _fragmentationParamsViewModel.GenerateInternalIons ? _fragmentationParamsViewModel.MinInternalIonLength : 0;
TheTask.SearchParameters.DoParsimony = CheckBoxParsimony.IsChecked.Value;
TheTask.SearchParameters.NoOneHitWonders = CheckBoxNoOneHitWonders.IsChecked.Value;
TheTask.SearchParameters.DoLabelFreeQuantification = !CheckBoxNoQuant.IsChecked.Value;
@@ -858,13 +858,13 @@ private void NonSpecificUsingNonSpecific(object sender, RoutedEventArgs e)
if (NonSpecificSearchRadioButton.IsChecked.Value)
{
ProteaseComboBox.SelectedItem = ProteaseDictionary.Dictionary["non-specific"];
- AddCompIonCheckBox.IsChecked = true;
+ _fragmentationParamsViewModel.GenerateComplementaryIons = true;
}
else
{
- AddCompIonCheckBox.IsChecked = false;
- NTerminalIons.IsChecked = true;
- CTerminalIons.IsChecked = true;
+ _fragmentationParamsViewModel.GenerateComplementaryIons = false;
+ _fragmentationParamsViewModel.RightSideFragmentIons = true;
+ _fragmentationParamsViewModel.LeftSideFragmentIons = true;
}
}
@@ -920,8 +920,8 @@ private void ProteaseSpecificUpdate(object sender, SelectionChangedEventArgs e)
{
DeconHostViewModel.SetAllPrecursorMaxChargeState(60);
DeconHostViewModel.SetAllProductMaxChargeState(20);
- InternalIonsCheckBox.IsChecked = true;
- MinInternalFragmentLengthTextBox.Text = "10";
+ _fragmentationParamsViewModel.GenerateInternalIons = true;
+ _fragmentationParamsViewModel.MinInternalIonLength = 10;
CheckBoxNoQuant.IsChecked = true;
_massDifferenceAcceptorViewModel.SelectedType =
_massDifferenceAcceptorViewModel.MassDiffAcceptorTypes.First(p => p.Type == MassDiffAcceptorType.PlusOrMinusThreeMM);
@@ -1012,14 +1012,14 @@ private void ProteaseSpecificUpdate(object sender, TextChangedEventArgs e)
int maxLength = Convert.ToInt32(MaxPeptideLengthTextBox.Text);
if (maxLength > 0 && maxLength < 100) //default is 30000; 30000/300=100
{
- MaxFragmentMassTextBox.Text = (maxLength * 300).ToString(); //assume the average residue doesn't have a mass over 300 Da (largest is W @ 204, but mods exist)
+ _fragmentationParamsViewModel.MaxFragmentMassDa = maxLength * 300; //assume the average residue doesn't have a mass over 300 Da (largest is W @ 204, but mods exist)
}
}
}
private void SemiSpecificUpdate(object sender, RoutedEventArgs e)
{
- AddCompIonCheckBox.IsChecked = SemiSpecificSearchRadioButton.IsChecked.Value;
+ _fragmentationParamsViewModel.GenerateComplementaryIons = SemiSpecificSearchRadioButton.IsChecked.Value;
if (SemiSpecificSearchRadioButton.IsChecked.Value)
{
MissedCleavagesTextBox.Text = "2";
@@ -1027,8 +1027,8 @@ private void SemiSpecificUpdate(object sender, RoutedEventArgs e)
}
else
{
- NTerminalIons.IsChecked = true;
- CTerminalIons.IsChecked = true;
+ _fragmentationParamsViewModel.RightSideFragmentIons = true;
+ _fragmentationParamsViewModel.LeftSideFragmentIons = true;
}
}
@@ -1171,7 +1171,7 @@ private void SnesUpdates(CleavageSpecificity searchModeType)
{
searchModeType = CleavageSpecificity.None; //prevents an accidental semi attempt of a non-specific protease
- if (CTerminalIons.IsChecked.Value)
+ if (_fragmentationParamsViewModel.LeftSideFragmentIons)
{
Protease singleC = ProteaseDictionary.Dictionary["singleC"];
ProteaseComboBox.SelectedItem = singleC;
@@ -1182,41 +1182,48 @@ private void SnesUpdates(CleavageSpecificity searchModeType)
ProteaseComboBox.SelectedItem = singleN;
}
}
- if (!AddCompIonCheckBox.IsChecked.Value)
+ if (!_fragmentationParamsViewModel.GenerateComplementaryIons)
{
MessageBox.Show("Warning: Complementary ions are strongly recommended when using this algorithm.");
}
//only use N or C termini, not both
- if (CTerminalIons.IsChecked.Value)
+ if (_fragmentationParamsViewModel.LeftSideFragmentIons)
{
- NTerminalIons.IsChecked = false;
+ _fragmentationParamsViewModel.RightSideFragmentIons = false;
}
else
{
- NTerminalIons.IsChecked = true;
+ _fragmentationParamsViewModel.LeftSideFragmentIons = true;
}
}
}
private FragmentationTerminus GetFragmentationTerminus()
{
- if (NTerminalIons.IsChecked.Value && !CTerminalIons.IsChecked.Value)
- {
- return FragmentationTerminus.N;
- }
- else if (!NTerminalIons.IsChecked.Value && CTerminalIons.IsChecked.Value)
- {
- return FragmentationTerminus.C;
- }
- else if (!NTerminalIons.IsChecked.Value && !CTerminalIons.IsChecked.Value) //why would you want this
- {
- MessageBox.Show("Warning: No ion types were selected. MetaMorpheus will be unable to search MS/MS spectra.");
- return FragmentationTerminus.None;
- }
- else
- {
- return FragmentationTerminus.Both;
- }
+ FragmentationTerminus newTerm;
+ switch (_fragmentationParamsViewModel.LeftSideFragmentIons, _fragmentationParamsViewModel.RightSideFragmentIons)
+ {
+ case (true, false) when !GuiGlobalParamsViewModel.Instance.IsRnaMode:
+ newTerm = FragmentationTerminus.N;
+ break;
+ case (false, true) when !GuiGlobalParamsViewModel.Instance.IsRnaMode:
+ newTerm = FragmentationTerminus.C;
+ break;
+ case (true, false) when GuiGlobalParamsViewModel.Instance.IsRnaMode:
+ newTerm = FragmentationTerminus.FivePrime;
+ break;
+ case (false, true) when GuiGlobalParamsViewModel.Instance.IsRnaMode:
+ newTerm = FragmentationTerminus.ThreePrime;
+ break;
+ case (false, false):
+ MessageBox.Show("Warning: No ion types were selected. MetaMorpheus will be unable to search MS/MS spectra.");
+ newTerm = FragmentationTerminus.None;
+ break;
+ default:
+ newTerm = FragmentationTerminus.Both;
+ break;
+ }
+ return newTerm;
}
//string out is for error messages
@@ -1372,16 +1379,6 @@ private void AddTruncationsCheckBox_Checked(object sender, RoutedEventArgs e)
InitiatorMethionineBehaviorComboBox.SelectedIndex = (int)InitiatorMethionineBehavior.Retain;
}
}
-
- ///
- /// Sets the value of the Internal Ions TextBox upon being checked
- ///
- ///
- ///
- private void InternalIonsCheckBox_Checked(object sender, RoutedEventArgs e)
- {
- MinInternalFragmentLengthTextBox.Text = "4";
- }
}
public class DataContextForSearchTaskWindow : INotifyPropertyChanged
diff --git a/MetaMorpheus/GUI/Util/Converters/BoolToFontWeightConverter.cs b/MetaMorpheus/GUI/Util/Converters/BoolToFontWeightConverter.cs
new file mode 100644
index 0000000000..f7f860d004
--- /dev/null
+++ b/MetaMorpheus/GUI/Util/Converters/BoolToFontWeightConverter.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Globalization;
+using System.Windows;
+
+namespace MetaMorpheusGUI;
+
+public class BoolToFontWeightConverter : BaseValueConverter
+{
+ public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool boolValue && boolValue)
+ {
+ return FontWeights.Bold;
+ }
+ return FontWeights.Normal;
+ }
+
+ public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/MetaMorpheus/GUI/Util/Converters/CollapseOnRnaModeConverter.cs b/MetaMorpheus/GUI/Util/Converters/CollapseOnRnaModeConverter.cs
index f8a8f66250..860df3d9cb 100644
--- a/MetaMorpheus/GUI/Util/Converters/CollapseOnRnaModeConverter.cs
+++ b/MetaMorpheus/GUI/Util/Converters/CollapseOnRnaModeConverter.cs
@@ -18,4 +18,19 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu
{
throw new NotImplementedException();
}
+}
+
+public class CollapseOnProteinModeConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ // Collapse the element if RNA mode is enabled
+ return GuiGlobalParamsViewModel.Instance == null || !GuiGlobalParamsViewModel.Instance.IsRnaMode
+ ? System.Windows.Visibility.Collapsed
+ : System.Windows.Visibility.Visible;
+ }
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
}
\ No newline at end of file
diff --git a/MetaMorpheus/GUI/Util/TaskValidator.cs b/MetaMorpheus/GUI/Util/TaskValidator.cs
index cb37dd7b86..c15690777c 100644
--- a/MetaMorpheus/GUI/Util/TaskValidator.cs
+++ b/MetaMorpheus/GUI/Util/TaskValidator.cs
@@ -35,7 +35,8 @@ public static bool CheckTaskSettingsValidity(string precursorMassTolerance,
string maxFragmentMass,
string qValueFilter,
string pepqValueFilter,
- string minInternalIonLength
+ string minInternalIonLength,
+ string productMassTolerance_LowRes = null
)
{
maxMissedCleavages = MaxValueConversion(maxMissedCleavages);
@@ -45,6 +46,7 @@ string minInternalIonLength
{
(CheckPrecursorMassTolerance(precursorMassTolerance)),
(CheckProductMassTolerance(productMassTolerance)),
+ (string.IsNullOrWhiteSpace(productMassTolerance_LowRes) || CheckProductMassTolerance_LowRes(productMassTolerance_LowRes)),
(CheckMaxMissedCleavages(maxMissedCleavages)),
(CheckMaxModificationIsoForms(maxModificationIsoforms)),
(CheckPeptideLength(minPeptideLength, maxPeptideLength)),
@@ -171,6 +173,16 @@ public static bool CheckProductMassTolerance(string text)
return true;
}
+ public static bool CheckProductMassTolerance_LowRes(string text)
+ {
+ if (!double.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out double childScanMassTolerance) || childScanMassTolerance <= 0)
+ {
+ MessageBox.Show("The Low-Res product mass tolerance is invalid. \n You entered " + '"' + text + '"' + "\n Please enter a positive number.");
+ return false;
+ }
+ return true;
+ }
+
public static bool CheckNumberOfDatabasePartitions(string text)
{
if (!int.TryParse(text, out int numberOfDatabaseSearches) || numberOfDatabaseSearches <= 0)
diff --git a/MetaMorpheus/GUI/Views/CustomMIonLossWindow.xaml b/MetaMorpheus/GUI/Views/CustomMIonLossWindow.xaml
new file mode 100644
index 0000000000..5c01e21c5e
--- /dev/null
+++ b/MetaMorpheus/GUI/Views/CustomMIonLossWindow.xaml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chemical formula (required), example: 'H2O1'
+
+
+ Note: Use positive values as the formula will be subtracted from the precursor mass.
+
+ Example: For water loss (-18.01 Da), enter 'H2O1' not '-H2O1'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ * Required fields. Select at least one applicable mode.
+
+
+
+
+
+
+
+
+
diff --git a/MetaMorpheus/GUI/Views/CustomMIonLossWindow.xaml.cs b/MetaMorpheus/GUI/Views/CustomMIonLossWindow.xaml.cs
new file mode 100644
index 0000000000..d79caf532f
--- /dev/null
+++ b/MetaMorpheus/GUI/Views/CustomMIonLossWindow.xaml.cs
@@ -0,0 +1,126 @@
+using Chemistry;
+using EngineLayer;
+using GuiFunctions.Models;
+using GuiFunctions.Util;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Windows;
+
+namespace MetaMorpheusGUI
+{
+ ///
+ /// Interaction logic for CustomMIonLossWindow.xaml
+ ///
+ public partial class CustomMIonLossWindow : Window
+ {
+ public List CreatedLosses { get; private set; }
+
+ public CustomMIonLossWindow()
+ {
+ InitializeComponent();
+
+ // Set default selection based on current mode
+ if (GuiFunctions.GuiGlobalParamsViewModel.Instance.IsRnaMode)
+ {
+ rnaModeCheckBox.IsChecked = true;
+ }
+ else
+ {
+ proteinModeCheckBox.IsChecked = true;
+ }
+ }
+
+ private void SaveCustomLoss_Click(object sender, RoutedEventArgs e)
+ {
+ // Validate inputs
+ string name = nameTextBox.Text?.Trim();
+ string annotation = annotationTextBox.Text?.Trim();
+ string formulaText = chemicalFormulaTextBox.Text?.Trim();
+
+ if (string.IsNullOrEmpty(name))
+ {
+ MessageBox.Show("Please enter a name for the M-Ion loss.", "Validation Error", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ if (string.IsNullOrEmpty(annotation))
+ {
+ MessageBox.Show("Please enter an annotation for the M-Ion loss.", "Validation Error", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ if (string.IsNullOrEmpty(formulaText))
+ {
+ MessageBox.Show("Please enter a chemical formula.", "Validation Error", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ // Check that at least one mode is selected
+ if (proteinModeCheckBox.IsChecked != true && rnaModeCheckBox.IsChecked != true)
+ {
+ MessageBox.Show("Please select at least one applicable mode.", "Validation Error", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ // Parse chemical formula
+ ChemicalFormula formula;
+ try
+ {
+ formula = ChemicalFormula.ParseFormula(formulaText);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Invalid chemical formula: {ex.Message}", "Validation Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ return;
+ }
+
+ // Create list of analyte types based on selections
+ var analyteTypes = new List();
+ if (proteinModeCheckBox.IsChecked == true)
+ {
+ analyteTypes.Add(AnalyteType.Peptide);
+ }
+ if (rnaModeCheckBox.IsChecked == true)
+ {
+ analyteTypes.Add(AnalyteType.Oligo);
+ }
+
+ // Create custom M-Ion losses (one per selected mode)
+ CreatedLosses = new List();
+ try
+ {
+ foreach (var analyteType in analyteTypes)
+ {
+ var customLoss = new CustomMIonLoss(name, annotation, formula, analyteType);
+ CustomMIonLossManager.AddCustomMIonLoss(customLoss);
+ CreatedLosses.Add(customLoss);
+ }
+
+ string modesMessage = analyteTypes.Count > 1
+ ? $"Protein and RNA modes"
+ : analyteTypes[0] == AnalyteType.Peptide ? "Protein mode" : "RNA mode";
+
+ MessageBox.Show($"Successfully added custom M-Ion loss '{name}' for {modesMessage}.",
+ "Success", MessageBoxButton.OK, MessageBoxImage.Information);
+ DialogResult = true;
+ Close();
+ }
+ catch (InvalidOperationException ex)
+ {
+ MessageBox.Show(ex.Message, "Duplicate Loss", MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Error saving custom M-Ion loss: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void CancelCustomLoss_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+ }
+}
diff --git a/MetaMorpheus/GUI/Views/FragmentReanalysisControl.xaml b/MetaMorpheus/GUI/Views/FragmentReanalysisControl.xaml
index 7ab0cda5b0..05efc263e5 100644
--- a/MetaMorpheus/GUI/Views/FragmentReanalysisControl.xaml
+++ b/MetaMorpheus/GUI/Views/FragmentReanalysisControl.xaml
@@ -6,35 +6,26 @@
xmlns:local="clr-namespace:MetaMorpheusGUI"
xmlns:guiFunctions="clr-namespace:GuiFunctions;assembly=GuiFunctions"
mc:Ignorable="d"
- d:DesignHeight="450" d:DesignWidth="800">
+ d:DesignHeight="200" d:DesignWidth="800">
+
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
-
+
+ BorderThickness="1" Width="60" />
-
-
+
-
+ BorderThickness="1" Width="40" />
-
+
-
+
@@ -78,5 +69,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MetaMorpheus/GUI/Views/FragmentationParamsControl.xaml b/MetaMorpheus/GUI/Views/FragmentationParamsControl.xaml
new file mode 100644
index 0000000000..c797e6526a
--- /dev/null
+++ b/MetaMorpheus/GUI/Views/FragmentationParamsControl.xaml
@@ -0,0 +1,259 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Indexed Search Only: Limits the maximum MS2 fragment mass to avoid rare crashes.
+
+ A very high value (e.g. 30000) does not impact performance.
+
+
+
+
+
+
+
+
+
+
+
+
+ Generates experimental (not theoretical) complementary ions for each MS2.
+
+ Useful for localization of modifications.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Search for theoretical internal fragment ions AFTER scoring. These ions are useful for localizing PTMs, but are not used during scoring.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MetaMorpheus/GUI/Views/FragmentationParamsControl.xaml.cs b/MetaMorpheus/GUI/Views/FragmentationParamsControl.xaml.cs
new file mode 100644
index 0000000000..c3ac42bb5d
--- /dev/null
+++ b/MetaMorpheus/GUI/Views/FragmentationParamsControl.xaml.cs
@@ -0,0 +1,39 @@
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using GuiFunctions;
+
+namespace MetaMorpheusGUI
+{
+ ///
+ /// Interaction logic for FragmentationParamsControl.xaml
+ ///
+ public partial class FragmentationParamsControl : UserControl
+ {
+ public FragmentationParamsControl()
+ {
+ InitializeComponent();
+ }
+
+ private void AddCustomMIonLoss_Click(object sender, RoutedEventArgs e)
+ {
+ var window = new CustomMIonLossWindow();
+ if (window.ShowDialog() == true)
+ {
+ // Refresh the data context to reload loss
+ if (DataContext is FragmentationParamsViewModel viewModel)
+ {
+ // Trigger a reload of the M-Ion loss
+ viewModel.ReloadMIonLosses();
+
+ foreach (var loss in window.CreatedLosses)
+ {
+ var vm = viewModel.AvailableMIonLosses.FirstOrDefault(p => p.Name == loss.Name);
+ if (vm is not null)
+ vm.IsSelected = true;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/MetaMorpheus/GuiFunctions/GuiFunctions.csproj b/MetaMorpheus/GuiFunctions/GuiFunctions.csproj
index 8b95147e5d..df20fc330b 100644
--- a/MetaMorpheus/GuiFunctions/GuiFunctions.csproj
+++ b/MetaMorpheus/GuiFunctions/GuiFunctions.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/MetaMorpheus/GuiFunctions/GuiGlobalParamsViewModel.cs b/MetaMorpheus/GuiFunctions/GuiGlobalParamsViewModel.cs
index 96323d3661..8bea60b5f3 100644
--- a/MetaMorpheus/GuiFunctions/GuiGlobalParamsViewModel.cs
+++ b/MetaMorpheus/GuiFunctions/GuiGlobalParamsViewModel.cs
@@ -172,7 +172,10 @@ public bool IsRnaMode
{
get => _current.IsRnaMode;
set
- {
+ {
+ if (value == _current.IsRnaMode)
+ return;
+
// Invoke the event to check if the user wants to switch modes
var args = new ModeSwitchRequestEventArgs();
diff --git a/MetaMorpheus/GuiFunctions/MetaDraw/DeconExploration/DeconExplorationTabViewModel.cs b/MetaMorpheus/GuiFunctions/MetaDraw/DeconExploration/DeconExplorationTabViewModel.cs
index d3af599004..ce5653d981 100644
--- a/MetaMorpheus/GuiFunctions/MetaDraw/DeconExploration/DeconExplorationTabViewModel.cs
+++ b/MetaMorpheus/GuiFunctions/MetaDraw/DeconExploration/DeconExplorationTabViewModel.cs
@@ -189,8 +189,8 @@ private void RunDeconvolution(PlotView plotView)
}
else
{
- min = SelectedMsDataScan.IsolationRange.Minimum - 1;
- max = SelectedMsDataScan.IsolationRange.Maximum + 1;
+ min = MinMzToPlot > 0 ? MinMzToPlot : SelectedMsDataScan.IsolationRange.Minimum - 1;
+ max = MaxMzToPlot > 0 ? MaxMzToPlot : SelectedMsDataScan.IsolationRange.Maximum + 1;
isolationRange = SelectedMsDataScan.IsolationRange;
scanToPlot = SelectedMsDataFile!.GetOneBasedScan(SelectedMsDataScan.OneBasedPrecursorScanNumber!.Value);
results = DeconvoluteIsolationRegion(SelectedMsDataScan, scanToPlot);
diff --git a/MetaMorpheus/GuiFunctions/MetaDraw/DeconExploration/DeconvolutionPlot.cs b/MetaMorpheus/GuiFunctions/MetaDraw/DeconExploration/DeconvolutionPlot.cs
index fad5018ee6..d3ef265345 100644
--- a/MetaMorpheus/GuiFunctions/MetaDraw/DeconExploration/DeconvolutionPlot.cs
+++ b/MetaMorpheus/GuiFunctions/MetaDraw/DeconExploration/DeconvolutionPlot.cs
@@ -36,7 +36,10 @@ public DeconvolutionPlot(PlotView plotView, MsDataScan scan, List p.Intensity);
+ {
+ var peaksInRange = scan.MassSpectrum.Extract(isolationRange);
+ maxIntensity = peaksInRange.Max(p => p.Intensity);
+ }
else
maxIntensity = scan.MassSpectrum.YofPeakWithHighestY ?? 0;
diff --git a/MetaMorpheus/GuiFunctions/MetaDraw/FragmentResearching/FragmentationReanalysisViewModel.cs b/MetaMorpheus/GuiFunctions/MetaDraw/FragmentResearching/FragmentationReanalysisViewModel.cs
index 1621a99986..86df08db6b 100644
--- a/MetaMorpheus/GuiFunctions/MetaDraw/FragmentResearching/FragmentationReanalysisViewModel.cs
+++ b/MetaMorpheus/GuiFunctions/MetaDraw/FragmentResearching/FragmentationReanalysisViewModel.cs
@@ -13,6 +13,7 @@
using Omics.Fragmentation;
using Proteomics.ProteolyticDigestion;
using Readers;
+using TaskLayer;
using Transcriptomics;
using Transcriptomics.Digestion;
@@ -24,16 +25,17 @@ namespace GuiFunctions
public class FragmentationReanalysisViewModel : BaseViewModel
{
private readonly bool _isProtein;
+ private static readonly object _fragmentationLock = new();
public FragmentationReanalysisViewModel(bool isProtein = true)
{
_isProtein = isProtein;
- UseInternalIons = false;
- MinInternalIonLength = 10;
ProductIonMassTolerance = 20;
PossibleProducts = [.. GetPossibleProducts()];
IEnumerable values;
+ CommonParameters common;
+ SearchParameters search;
if (isProtein)
{
values = Enum.GetValues()
@@ -41,6 +43,12 @@ public FragmentationReanalysisViewModel(bool isProtein = true)
&& Omics.Fragmentation.Peptide.DissociationTypeCollection.ProductsFromDissociationType.TryGetValue(p, out var prod)
&& prod.Count != 0);
SelectedDissociationType = DissociationType.HCD;
+ common = new CommonParameters(digestionParams: new DigestionParams(), fragmentationParams: new FragmentationParams());
+ search = new SearchParameters()
+ {
+ MinAllowedInternalFragmentLength = 0,
+ MaxFragmentSize = 30000
+ };
}
else
{
@@ -49,9 +57,24 @@ public FragmentationReanalysisViewModel(bool isProtein = true)
&& Omics.Fragmentation.Oligo.DissociationTypeCollection.ProductsFromDissociationType.TryGetValue(p, out var prod)
&& prod.Count != 0);
SelectedDissociationType = DissociationType.CID;
+ common = new CommonParameters(digestionParams: new RnaDigestionParams(), fragmentationParams: new RnaFragmentationParams());
+ search = new RnaSearchParameters()
+ {
+ MinAllowedInternalFragmentLength = 0,
+ MaxFragmentSize = 30000
+ };
}
DissociationTypes = [.. values];
+ LoadFragmentationParameters(common, search);
+ }
+
+ ///
+ /// Updates the FragmentationParamsViewModel with parameters loaded from a search TOML
+ ///
+ public void LoadFragmentationParameters(CommonParameters common, SearchParameters search)
+ {
+ FragmentationParamsViewModel = new(common, search);
}
private ObservableCollection _possibleProducts;
@@ -89,22 +112,7 @@ public DissociationType SelectedDissociationType
}
}
- private int _minInternalIonLength;
- public int MinInternalIonLength
- {
- get => _minInternalIonLength;
- set { _minInternalIonLength = value; OnPropertyChanged(nameof(MinInternalIonLength)); }
- }
-
- private bool _useInternalIons;
- public bool UseInternalIons
- {
- get => _useInternalIons;
- set { _useInternalIons = value; OnPropertyChanged(nameof(UseInternalIons)); }
- }
-
private double _productIonMassTolerance;
-
public double ProductIonMassTolerance
{
get => _productIonMassTolerance;
@@ -118,6 +126,20 @@ public bool MatchAllCharges
set { _matchAllCharges = value; OnPropertyChanged(nameof(MatchAllCharges)); }
}
+ private FragmentationParamsViewModel _fragmentationParamsViewModel;
+ ///
+ /// View model containing fragmentation parameters including M-Ion losses
+ ///
+ public FragmentationParamsViewModel FragmentationParamsViewModel
+ {
+ get => _fragmentationParamsViewModel;
+ set
+ {
+ _fragmentationParamsViewModel = value;
+ OnPropertyChanged(nameof(FragmentationParamsViewModel));
+ }
+ }
+
private IEnumerable GetPossibleProducts()
{
foreach (var product in Enum.GetValues())
@@ -215,22 +237,30 @@ private void SetUseForFragmentsBasedUponDissociationType(DissociationType dissoc
PossibleProducts.ForEach(product => product.Use = dissociationTypeProducts.Contains(product.ProductType));
}
- public List MatchIonsWithNewTypes(MsDataScan ms2Scan, SpectrumMatchFromTsv smToRematch, bool concatOldIonsOfType)
+ public List MatchIonsWithNewTypes(MsDataScan ms2Scan, SpectrumMatchFromTsv smToRematch, bool concatOldIonsOfType = true)
{
if (smToRematch.FullSequence.Contains('|'))
return smToRematch.MatchedIons;
IBioPolymerWithSetMods bioPolymer = smToRematch.ToBioPolymerWithSetMods();
+ IFragmentationParams fragmentationParams = FragmentationParamsViewModel.ToFragmentationParams();
List terminalProducts = new List();
- smToRematch.ProductsFromDissociationType()[DissociationType.Custom] = _productsToUse.ToList();
- bioPolymer.Fragment(DissociationType.Custom, FragmentationTerminus.Both, terminalProducts);
-
List internalProducts = new List();
- if (UseInternalIons && bioPolymer is PeptideWithSetModifications) // internal ions are not currently implemented for RNA
+
+ // Snapshot products before acquiring lock to avoid enumerating collection while it may be modified by UI thread
+ var productsSnapshot = _productsToUse.ToList();
+ // Lock to ensure thread-safe mutation of static DissociationTypeCollection dictionary
+ lock (_fragmentationLock)
{
- Omics.Fragmentation.Peptide.DissociationTypeCollection.ProductsFromDissociationType[DissociationType.Custom] = _productsToUse.ToList();
- bioPolymer.FragmentInternally(DissociationType.Custom, MinInternalIonLength, internalProducts);
+ smToRematch.ProductsFromDissociationType()[DissociationType.Custom] = productsSnapshot;
+ bioPolymer.Fragment(DissociationType.Custom, FragmentationTerminus.Both, terminalProducts, fragmentationParams);
+
+ if (FragmentationParamsViewModel.GenerateInternalIons && bioPolymer is PeptideWithSetModifications) // internal ions are not currently implemented for RNA
+ {
+ Omics.Fragmentation.Peptide.DissociationTypeCollection.ProductsFromDissociationType[DissociationType.Custom] = productsSnapshot;
+ bioPolymer.FragmentInternally(DissociationType.Custom, FragmentationParamsViewModel.MinInternalIonLength, internalProducts, fragmentationParams);
+ }
}
var allProducts = terminalProducts.Concat(internalProducts).ToList();
@@ -239,7 +269,8 @@ public List MatchIonsWithNewTypes(MsDataScan ms2Scan, Spectr
precursorDeconParams: MetaDrawSettingsViewModel.Instance.DeconHostViewModel.PrecursorDeconvolutionParameters.Parameters,
productDeconParams: MetaDrawSettingsViewModel.Instance.DeconHostViewModel.ProductDeconvolutionParameters.Parameters,
deconvolutionMaxAssumedChargeState: _isProtein ? 60 : -60,
- digestionParams: _isProtein ? new DigestionParams() : new RnaDigestionParams() // no digestion occurs, just used to set values.
+ digestionParams: _isProtein ? new DigestionParams() : new RnaDigestionParams(), // no digestion occurs, just used to set values.
+ fragmentationParams: fragmentationParams
);
@@ -254,16 +285,15 @@ public List MatchIonsWithNewTypes(MsDataScan ms2Scan, Spectr
? newMatches.Concat(smToRematch.MatchedIons)
: newMatches;
- uniqueMatches = uniqueMatches.Where(p => _productsToUse.Contains(p.NeutralTheoreticalProduct.ProductType))
+ uniqueMatches = uniqueMatches.Where(p => productsSnapshot.Contains(p.NeutralTheoreticalProduct.ProductType))
.Where(p => Math.Abs(p.MassErrorPpm) <= ProductIonMassTolerance);
// retain only internal ions
- if (!UseInternalIons)
+ if (!FragmentationParamsViewModel.GenerateInternalIons)
uniqueMatches = uniqueMatches.Where(p => !p.IsInternalFragment);
// retain terminal and internals greater than min length
else
- uniqueMatches = uniqueMatches.Where(p => !p.IsInternalFragment || Math.Abs(p.NeutralTheoreticalProduct.FragmentNumber - p.NeutralTheoreticalProduct.SecondaryFragmentNumber) >= MinInternalIonLength);
-
+ uniqueMatches = uniqueMatches.Where(p => !p.IsInternalFragment || Math.Abs(p.NeutralTheoreticalProduct.FragmentNumber - p.NeutralTheoreticalProduct.SecondaryFragmentNumber) >= FragmentationParamsViewModel.MinInternalIonLength);
return uniqueMatches.Distinct(MatchedFragmentIonComparer)
.ToList();
}
@@ -283,7 +313,8 @@ public bool Equals(MatchedFragmentIon x, MatchedFragmentIon y)
&& x.NeutralTheoreticalProduct.FragmentNumber == y.NeutralTheoreticalProduct.FragmentNumber
&& x.NeutralTheoreticalProduct.ProductType == y.NeutralTheoreticalProduct.ProductType
&& x.NeutralTheoreticalProduct.SecondaryProductType == y.NeutralTheoreticalProduct.SecondaryProductType
- && x.NeutralTheoreticalProduct.SecondaryFragmentNumber == y.NeutralTheoreticalProduct.SecondaryFragmentNumber;
+ && x.NeutralTheoreticalProduct.SecondaryFragmentNumber == y.NeutralTheoreticalProduct.SecondaryFragmentNumber
+ && x.NeutralTheoreticalProduct.NeutralLoss.Equals(y.NeutralTheoreticalProduct.NeutralLoss);
}
public int GetHashCode(MatchedFragmentIon obj)
@@ -298,6 +329,7 @@ public int GetHashCode(MatchedFragmentIon obj)
hash = hash * 23 + (obj.NeutralTheoreticalProduct.SecondaryProductType?.GetHashCode() ?? 0);
if (obj.NeutralTheoreticalProduct.SecondaryFragmentNumber != null)
hash = hash * 23 + obj.NeutralTheoreticalProduct.SecondaryFragmentNumber.GetHashCode();
+ hash = hash * 23 + obj.NeutralTheoreticalProduct.NeutralLoss.GetHashCode();
return hash;
}
diff --git a/MetaMorpheus/GuiFunctions/MetaDraw/MetaDrawDataLoader.cs b/MetaMorpheus/GuiFunctions/MetaDraw/MetaDrawDataLoader.cs
index 9f8bdccf52..3d4cab1ec3 100644
--- a/MetaMorpheus/GuiFunctions/MetaDraw/MetaDrawDataLoader.cs
+++ b/MetaMorpheus/GuiFunctions/MetaDraw/MetaDrawDataLoader.cs
@@ -1,5 +1,6 @@
#nullable enable
using EngineLayer;
+using Omics.Fragmentation;
using Readers.SpectralLibrary;
using Readers;
using System;
@@ -35,7 +36,8 @@ public async Task> LoadAllAsync(
bool loadLibraries,
ChimeraAnalysisTabViewModel? chimeraTabViewModel = null,
BioPolymerTabViewModel? bioPolymerTabViewModel = null,
- DeconExplorationTabViewModel? deconExplorationTabViewModel = null)
+ DeconExplorationTabViewModel? deconExplorationTabViewModel = null,
+ FragmentationReanalysisViewModel? fragmentationReanalysisViewModel = null)
{
// Cancel any previous run
_cancellationTokenSource?.Cancel();
@@ -63,8 +65,8 @@ public async Task> LoadAllAsync(
? LoadLibrariesAsync(token)
: Task.FromResult(new List());
var proseAndTomlTask = _logic.SpectralMatchResultFilePaths.Any()
- ? TryLoadProseAndSearchToml(bioPolymerTabViewModel, deconExplorationTabViewModel)
- : Task.FromResult((false, false));
+ ? TryLoadProseAndSearchToml(bioPolymerTabViewModel, deconExplorationTabViewModel, fragmentationReanalysisViewModel)
+ : Task.FromResult((false, false, false));
List[] results;
try
@@ -306,10 +308,11 @@ await Task.Run(() =>
return errors;
}
- public async Task<(bool Database, bool SearchParams)> TryLoadProseAndSearchToml(BioPolymerTabViewModel? bpTabVm, DeconExplorationTabViewModel? deconTabVm)
+ public async Task<(bool Database, bool SearchParams, bool FragmentationParams)> TryLoadProseAndSearchToml(BioPolymerTabViewModel? bpTabVm, DeconExplorationTabViewModel? deconTabVm, FragmentationReanalysisViewModel? fragmentationReanalysisVm)
{
bool loadedDb = false;
bool loadedSearchParams = false;
+ bool loadedFragmentationParams = false;
var searchDirectories = _logic.SpectralMatchResultFilePaths
.Select(Path.GetDirectoryName)
@@ -327,7 +330,7 @@ await Task.Run(() =>
.Distinct()
.ToArray();
- // Try to find search tomls to load decon parameters.
+ // Try to find search tomls to load decon and fragmentation parameters.
try
{
HashSet distinctSearchTomls = new();
@@ -353,12 +356,16 @@ await Task.Run(() =>
List precursorParameters = new();
List productParameters = new();
+ List commonParameters = new();
+ List searchParameters = new();
foreach (var searchToml in distinctSearchTomls.Where(p => p != null))
{
var task = Toml.ReadFile(searchToml, MetaMorpheusTask.tomlConfig);
precursorParameters.Add(task.CommonParameters.PrecursorDeconvolutionParameters);
productParameters.Add(task.CommonParameters.ProductDeconvolutionParameters);
+ commonParameters.Add(task.CommonParameters);
+ searchParameters.Add(task.SearchParameters);
}
// IF we find params, and have multiple, take the last.
@@ -375,11 +382,24 @@ await Task.Run(() =>
deconTabVm.DeconHostViewModel = new(precursor, product);
}
}
+
+ // Load fragmentation parameters if found
+ if (commonParameters.Any() && searchParameters.Any() && fragmentationReanalysisVm != null)
+ {
+ var commonParam = commonParameters.Last();
+ var searchParam = searchParameters.Last();
+ fragmentationReanalysisVm.LoadFragmentationParameters(commonParam, searchParam);
+ loadedFragmentationParams = true;
+ }
+ }
+ catch (Exception)
+ {
+ loadedSearchParams = false;
+ loadedFragmentationParams = false;
}
- catch (Exception) { loadedSearchParams = false; }
if (bpTabVm is null)
- return (loadedDb, loadedSearchParams);
+ return (loadedDb, loadedSearchParams, loadedFragmentationParams);
string[] acceptableDbTypes = new[] { ".fasta", ".fa", ".xml" };
@@ -421,7 +441,7 @@ await Task.Run(() =>
catch (Exception) { loadedDb = false; }
- return (loadedDb, loadedSearchParams);
+ return (loadedDb, loadedSearchParams, loadedFragmentationParams);
}
#endregion
diff --git a/MetaMorpheus/GuiFunctions/MetaDraw/MetaDrawSettings.cs b/MetaMorpheus/GuiFunctions/MetaDraw/MetaDrawSettings.cs
index deee26cd10..abfe33f2fc 100644
--- a/MetaMorpheus/GuiFunctions/MetaDraw/MetaDrawSettings.cs
+++ b/MetaMorpheus/GuiFunctions/MetaDraw/MetaDrawSettings.cs
@@ -115,6 +115,7 @@ public static class MetaDrawSettings
public static string[] CoverageTypes { get; set; } = { "N-Terminal Color", "C-Terminal Color", "Internal Color" };
public static string[] ExportTypes { get; set; } = { "Pdf", "Png", "Jpeg", "Tiff", "Wmf", "Bmp" };
public static string[] AmbiguityTypes { get; set; } = { "No Filter", "1", "2A", "2B", "2C", "2D", "3", "4", "5" };
+ public static string[] GroupingProperties { get; set; } = { "None", "Notch", "Precursor Charge", "File Name", "Ambiguity Level", "Missed Cleavages", "OrganismName", "DecoyContamTarget" };
#endregion
@@ -293,6 +294,10 @@ public static void ResetSettings()
AxisLabelTextSize = 12;
StrokeThicknessUnannotated = 0.7;
StrokeThicknessAnnotated = 1.0;
+
+ // Reset the new ViewModel structure
+ PlotModelStatParametersViewModel.Instance.LoadFromSnapshot(new PlotModelStatParameters());
+
SetDefaultColors();
}
@@ -547,7 +552,14 @@ public static MetaDrawSettingsSnapshot MakeSnapShot()
DisplayFilteredOnly = DisplayFilteredOnly,
DataVisualizationColorOrder = DataVisualizationColorOrder?.Select(c => c.GetColorName()).ToList(),
BioPolymerCoverageFontSize = BioPolymerCoverageFontSize,
- BioPolymerCoverageColors = BioPolymerCoverageColors.Select(p => $"{p.Key},{p.Value.ToOxyColor().GetColorName()}").ToList()
+ BioPolymerCoverageColors = BioPolymerCoverageColors.Select(p => $"{p.Key},{p.Value.ToOxyColor().GetColorName()}").ToList(),
+
+ // Save from the new ViewModel structure
+ UseLogScaleYAxis = PlotModelStatParametersViewModel.Instance.UseLogScaleYAxis,
+ GroupingProperty = PlotModelStatParametersViewModel.Instance.GroupingProperty,
+ MinRelativeCutoff = PlotModelStatParametersViewModel.Instance.MinRelativeCutoff,
+ MaxRelativeCutoff = PlotModelStatParametersViewModel.Instance.MaxRelativeCutoff,
+ AllowAmbiguousGroups = PlotModelStatParametersViewModel.Instance.AllowAmbiguousGroups
};
}
@@ -591,6 +603,19 @@ public static void LoadSettings(MetaDrawSettingsSnapshot settings, out bool flag
NormalizeHistogramToFile = settings.NormalizeHistogramToFile;
DisplayFilteredOnly = settings.DisplayFilteredOnly;
BioPolymerCoverageFontSize = settings.BioPolymerCoverageFontSize;
+
+ // Load into the new ViewModel structure
+ var plotParams = new PlotModelStatParameters
+ {
+ UseLogScaleYAxis = settings.UseLogScaleYAxis,
+ GroupingProperty = settings.GroupingProperty ?? "None",
+ MinRelativeCutoff = settings.MinRelativeCutoff,
+ MaxRelativeCutoff = settings.MaxRelativeCutoff,
+ NormalizeHistogramToFile = settings.NormalizeHistogramToFile,
+ DisplayFilteredOnly = settings.DisplayFilteredOnly,
+ AllowAmbiguousGroups = settings.AllowAmbiguousGroups
+ };
+ PlotModelStatParametersViewModel.Instance.LoadFromSnapshot(plotParams);
try // Product Type Colors
{
diff --git a/MetaMorpheus/GuiFunctions/MetaDraw/MetaDrawSettingsSnapshot.cs b/MetaMorpheus/GuiFunctions/MetaDraw/MetaDrawSettingsSnapshot.cs
index 9779033484..1eb643df82 100644
--- a/MetaMorpheus/GuiFunctions/MetaDraw/MetaDrawSettingsSnapshot.cs
+++ b/MetaMorpheus/GuiFunctions/MetaDraw/MetaDrawSettingsSnapshot.cs
@@ -46,6 +46,11 @@ public class MetaDrawSettingsSnapshot
public bool DisplayFilteredOnly { get; set; } = true;
public bool NormalizeHistogramToFile { get; set; } = false;
public List DataVisualizationColorOrder { get; set; }
+ public bool UseLogScaleYAxis { get; set; } = false;
+ public string GroupingProperty { get; set; } = "None";
+ public double MinRelativeCutoff { get; set; } = 0.0;
+ public double MaxRelativeCutoff { get; set; } = 100.0;
+ public bool AllowAmbiguousGroups { get; set; } = false;
// filter settings
diff --git a/MetaMorpheus/GuiFunctions/MetaDraw/PlotModelStat.cs b/MetaMorpheus/GuiFunctions/MetaDraw/PlotModelStat.cs
index 4ed0e13c81..e11db3c820 100644
--- a/MetaMorpheus/GuiFunctions/MetaDraw/PlotModelStat.cs
+++ b/MetaMorpheus/GuiFunctions/MetaDraw/PlotModelStat.cs
@@ -1,15 +1,19 @@
-using EngineLayer;
+using EngineLayer;
+using GuiFunctions.MetaDraw;
+using Omics.Fragmentation;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
using Proteomics.ProteolyticDigestion;
using Proteomics.RetentionTimePrediction;
+using Readers;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Linq;
using System.Globalization;
-using Readers;
+using System.IO;
+using System.Linq;
+using System.Text;
namespace GuiFunctions
{
@@ -18,6 +22,12 @@ public class PlotModelStat
private PlotModel privateModel;
private readonly ObservableCollection allSpectralMatches;
private readonly Dictionary> psmsBySourceFile;
+ private readonly PlotModelStatParameters parameters;
+
+ ///
+ /// Stores the tabular data for text export. Each row is a dictionary of column name to value.
+ ///
+ public List> PlotData { get; private set; } = new();
public readonly static List PlotNames = new List {
"Histogram of Precursor PPM Errors (around 0 Da mass-difference notch only)",
@@ -38,14 +48,36 @@ public class PlotModelStat
public PlotModel Model => privateModel;
- public PlotModelStat(string plotName, ObservableCollection sms, Dictionary> smsBySourceFile)
+ public PlotModelStat(string plotName, ObservableCollection sms,
+ Dictionary> smsBySourceFile,
+ PlotModelStatParameters parameters = null)
{
privateModel = new PlotModel { Title = plotName, DefaultFontSize = 14 };
allSpectralMatches = sms;
this.psmsBySourceFile = smsBySourceFile;
+ this.parameters = parameters ?? PlotModelStatParametersViewModel.Instance.GetParameters();
createPlot(plotName);
privateModel.DefaultColors = MetaDrawSettings.DataVisualizationColorOrder;
}
+
+ ///
+ /// Exports the plot data to a tab-separated text file.
+ ///
+ public void ExportToText(string filePath)
+ {
+ if (PlotData == null || PlotData.Count == 0)
+ return;
+
+ var columns = PlotData.SelectMany(r => r.Keys).Distinct().ToList();
+ var sb = new StringBuilder();
+ sb.AppendLine(string.Join("\t", columns));
+ foreach (var row in PlotData)
+ {
+ sb.AppendLine(string.Join("\t", columns.Select(c => row.ContainsKey(c) ? row[c] : "")));
+ }
+ File.WriteAllText(filePath, sb.ToString());
+ }
+
private void createPlot(string plotType)
{
switch (plotType)
@@ -53,7 +85,7 @@ private void createPlot(string plotType)
case "Histogram of Precursor PPM Errors (around 0 Da mass-difference notch only)":
histogramPlot(1);
break;
- case "Histogram of Fragment PPM Errors":
+ case "Histogram of Fragment PPM Errors":
histogramPlot(2);
break;
case "Histogram of Precursor Charges":
@@ -94,16 +126,66 @@ private void createPlot(string plotType)
break;
}
}
+
private void histogramPlot(int plotType)
{
privateModel.LegendTitle = "Source file(s)";
- string yAxisTitle = "Count";
+
+ bool isGroupingEnabled = parameters.GroupingProperty != "None";
+ Dictionary>> groupedPsmsBySourceFile = null;
+
+ if (isGroupingEnabled)
+ {
+ groupedPsmsBySourceFile = GroupPsmsByProperty(psmsBySourceFile, parameters.GroupingProperty);
+ }
+
+ // Gather histogram data from PSMs
+ var histData = GetHistogramData(plotType);
+ string xAxisTitle = histData.XAxisTitle;
+ string yAxisTitle = histData.YAxisTitle;
+ double binSize = histData.BinSize;
+ double labelAngle = histData.LabelAngle;
+ var numbersBySourceFile = histData.NumbersBySourceFile;
+ var dictsBySourceFile = histData.DictsBySourceFile;
+
+ // Build series and categories
+ string[] category;
+ int[] totalCounts;
+ int categoriesPerGroup = 0;
+ List allGroupKeys = null;
+ bool isCategoryHistogram = plotType == 5 || plotType == 10 || plotType == 11;
+
+ if (isCategoryHistogram)
+ {
+ (category, totalCounts, categoriesPerGroup, allGroupKeys) = BuildCategoryHistogramSeries(
+ plotType, dictsBySourceFile, groupedPsmsBySourceFile, isGroupingEnabled);
+ }
+ else
+ {
+ (category, totalCounts, categoriesPerGroup, allGroupKeys) = BuildNumericalHistogramSeries(
+ plotType, binSize, dictsBySourceFile, numbersBySourceFile, groupedPsmsBySourceFile, isGroupingEnabled);
+ }
+
+ // Configure axes
+ ConfigureHistogramAxes(xAxisTitle, yAxisTitle, labelAngle, category,
+ isGroupingEnabled, allGroupKeys, categoriesPerGroup);
+ }
+
+ #region Histogram Data Gathering
+
+ private record HistogramRawData(
+ string XAxisTitle, string YAxisTitle, double BinSize, double LabelAngle,
+ Dictionary> NumbersBySourceFile,
+ Dictionary> DictsBySourceFile);
+
+ private HistogramRawData GetHistogramData(int plotType)
+ {
string xAxisTitle = "";
+ string yAxisTitle = "Count";
double binSize = -1;
double labelAngle = 0;
- SortedList numCategory = new SortedList();
- Dictionary> numbersBySourceFile = new Dictionary>(); // key is file name, value is data from that file
- Dictionary> dictsBySourceFile = new Dictionary>(); // key is file name, value is dictionary of bins and their counts
+ var numbersBySourceFile = new Dictionary>();
+ var dictsBySourceFile = new Dictionary>();
switch (plotType)
{
@@ -119,7 +201,7 @@ private void histogramPlot(int plotType)
break;
case 2: // Histogram of Fragment PPM Errors
xAxisTitle = "Fragment error (ppm)";
- binSize = 1;
+ binSize = 2;
foreach (string key in psmsBySourceFile.Keys)
{
numbersBySourceFile.Add(key, psmsBySourceFile[key].SelectMany(p => p.MatchedIons.Select(v => v.MassErrorPpm)));
@@ -191,7 +273,7 @@ private void histogramPlot(int plotType)
foreach (var psm in psmsBySourceFile[key].Where(p => p is not OsmFromTsv))
{
values.Add(sSRCalc3.ScoreSequence(new PeptideWithSetModifications(psm.BaseSeq.Split("|")[0], null)));
-
+
}
numbersBySourceFile.Add(key, values);
var results = numbersBySourceFile[key].GroupBy(p => roundToBin(p, binSize)).OrderBy(p => p.Key).Select(p => p);
@@ -217,11 +299,15 @@ private void histogramPlot(int plotType)
foreach (var fileName in psmsBySourceFile.Keys)
{
- var result = psmsBySourceFile[fileName].SelectMany(p => p.MatchedIons)
- .GroupBy(p => p.NeutralTheoreticalProduct.ProductType)
+ var allMatchedIons = psmsBySourceFile[fileName].SelectMany(p => p.MatchedIons).ToList();
+
+ var result = allMatchedIons
+ .GroupBy(p => p.NeutralTheoreticalProduct is CustomMProduct cmp ? cmp.Annotation : p.NeutralTheoreticalProduct.ProductType.ToString())
.ToDictionary(p => p.Key.ToString(), p => p.Count());
dictsBySourceFile.Add(fileName, result);
}
+ if (dictsBySourceFile.Sum(p => p.Value.Keys.Count) >= 40)
+ labelAngle = -50;
break;
case 11: // Histogram of Fragment Ion Types by intensity
xAxisTitle = "Fragment Types";
@@ -229,17 +315,19 @@ private void histogramPlot(int plotType)
yAxisTitle = "Summed Intensity";
foreach (var fileName in psmsBySourceFile.Keys)
{
- var result = psmsBySourceFile[fileName].SelectMany(p => p.MatchedIons)
- .GroupBy(p => p.NeutralTheoreticalProduct.ProductType)
+ var allMatchedIons = psmsBySourceFile[fileName].SelectMany(p => p.MatchedIons).ToList();
+ var result = allMatchedIons
+ .GroupBy(p => p.NeutralTheoreticalProduct is CustomMProduct cmp ? cmp.Annotation : p.NeutralTheoreticalProduct.ProductType.ToString())
.ToDictionary(p => p.Key.ToString(), p => (int)p.Sum(m => m.Intensity));
dictsBySourceFile.Add(fileName, result);
}
+ if (dictsBySourceFile.Sum(p => p.Value.Keys.Count) >= 40)
+ labelAngle = -50;
break;
- case 12: // Histogram of Fragment Ion Types
+ case 12: // Histogram of Ids by Retention Time
xAxisTitle = "Retention Time";
binSize = 1;
labelAngle = 0;
-
foreach (var fileName in psmsBySourceFile.Keys)
{
var result = psmsBySourceFile[fileName]
@@ -250,109 +338,491 @@ private void histogramPlot(int plotType)
break;
}
- String[] category; // for labeling bottom axis
- int[] totalCounts; // for having the tracker show total count across all files
- if (plotType == 5 || plotType == 10 || plotType == 11) // category histogram
+ return new HistogramRawData(xAxisTitle, yAxisTitle, binSize, labelAngle, numbersBySourceFile, dictsBySourceFile);
+ }
+
+ #endregion
+
+ #region Category Histogram Series
+
+ private (string[] category, int[] totalCounts, int categoriesPerGroup, List allGroupKeys)
+ BuildCategoryHistogramSeries(int plotType,
+ Dictionary> dictsBySourceFile,
+ Dictionary>> groupedPsmsBySourceFile,
+ bool isGroupingEnabled)
+ {
+ IEnumerable allCategories = dictsBySourceFile.Values.Select(p => p.Keys).SelectMany(p => p);
+ Dictionary categoryIDs = new Dictionary();
+ int counter = 0;
+ foreach (string s in allCategories)
+ {
+ if (!categoryIDs.ContainsKey(s))
+ categoryIDs.Add(s, counter++);
+ }
+ var totalCounts = new int[counter];
+
+ foreach (string cat in categoryIDs.Keys)
{
- // assign all categories their index on the x axis
- IEnumerable allCategories = dictsBySourceFile.Values.Select(p => p.Keys).SelectMany(p => p);
- Dictionary categoryIDs = new Dictionary();
- int counter = 0;
- foreach (string s in allCategories)
+ foreach (Dictionary dict in dictsBySourceFile.Values)
+ totalCounts[categoryIDs[cat]] += dict.ContainsKey(cat) ? dict[cat] : 0;
+ }
+
+ int maxCount = totalCounts.Max();
+ double minThreshold = maxCount * (parameters.MinRelativeCutoff / 100.0);
+ double maxThreshold = maxCount * (parameters.MaxRelativeCutoff / 100.0);
+
+ var filteredCategoryIDs = new Dictionary();
+ var filteredCategory = new List();
+ var filteredTotalCounts = new List();
+ int newIndex = 0;
+
+ foreach (var cat in categoryIDs.Keys.OrderBy(k => k))
+ {
+ int oldId = categoryIDs[cat];
+ if (totalCounts[oldId] >= minThreshold && totalCounts[oldId] <= maxThreshold)
{
- if (!categoryIDs.ContainsKey(s))
+ filteredCategoryIDs[cat] = newIndex;
+ filteredCategory.Add(cat);
+ filteredTotalCounts.Add(totalCounts[oldId]);
+ newIndex++;
+ }
+ }
+
+ var category = filteredCategory.ToArray();
+ var filteredCounts = filteredTotalCounts.ToArray();
+ int categoriesPerGroup = filteredCategoryIDs.Count;
+ List allGroupKeys = null;
+
+ if (isGroupingEnabled)
+ {
+ allGroupKeys = OrderByNaturalKey(
+ groupedPsmsBySourceFile.Values
+ .SelectMany(d => d.Keys)
+ .Distinct())
+ .ToList();
+
+ var nestedCategories = new List();
+ var categoryToIndex = new Dictionary();
+ int categoryIndex = 0;
+
+ foreach (var groupKey in allGroupKeys)
+ {
+ foreach (var cat in filteredCategoryIDs.Keys.OrderBy(k => k))
{
- categoryIDs.Add(s, counter++);
+ nestedCategories.Add(cat);
+ categoryToIndex[cat + "|" + groupKey] = categoryIndex++;
}
}
- category = new string[counter];
- totalCounts = new int[counter];
- // calculate category totals across all files
- foreach (string cat in categoryIDs.Keys)
+ foreach (string sourceFile in psmsBySourceFile.Keys)
{
- foreach (Dictionary dict in dictsBySourceFile.Values)
+ ColumnSeries column = new ColumnSeries
{
- totalCounts[categoryIDs[cat]] += dict.ContainsKey(cat) ? dict[cat] : 0;
+ ColumnWidth = 200,
+ IsStacked = true,
+ Title = sourceFile,
+ TrackerFormatString = "Category: {bin}\n{0}: {2}\nGroup: {group}\nTotal: {total}",
+ BaseValue = parameters.UseLogScaleYAxis ? 0.1 : 0
+ };
+
+ foreach (string groupKey in allGroupKeys)
+ {
+ if (!groupedPsmsBySourceFile[sourceFile].ContainsKey(groupKey))
+ {
+ foreach (var cat in filteredCategoryIDs.Keys.OrderBy(k => k))
+ {
+ string lookupKey = cat + "|" + groupKey;
+ if (categoryToIndex.ContainsKey(lookupKey))
+ column.Items.Add(new HistItem(0, categoryToIndex[lookupKey], cat, filteredCounts[filteredCategoryIDs[cat]], groupKey));
+ }
+ continue;
+ }
+
+ var groupDict = GetCategoryDictForGroup(plotType, groupedPsmsBySourceFile[sourceFile][groupKey]);
+
+ foreach (var cat in filteredCategoryIDs.Keys.OrderBy(k => k))
+ {
+ string lookupKey = cat + "|" + groupKey;
+ if (!categoryToIndex.ContainsKey(lookupKey))
+ continue;
+
+ double total = parameters.NormalizeHistogramToFile ? groupDict.Values.Sum() : 1.0;
+ int catId = filteredCategoryIDs[cat];
+ double value = groupDict.ContainsKey(cat) ? groupDict[cat] / total : 0;
+ if (parameters.UseLogScaleYAxis && value > 0 && value < 0.1)
+ value = 0.1;
+
+ column.Items.Add(new HistItem(value, categoryToIndex[lookupKey], cat, filteredCounts[catId], groupKey));
+
+ PlotData.Add(new Dictionary
+ {
+ { "Source File", sourceFile }, { parameters.GroupingProperty, groupKey }, { "Category", cat },
+ { "Value", (groupDict.ContainsKey(cat) ? groupDict[cat] : 0).ToString() },
+ { "Total", filteredCounts[catId].ToString() }
+ });
+ }
}
+ privateModel.Series.Add(column);
}
- // add a column series for each file
+ category = nestedCategories.ToArray();
+ }
+ else
+ {
foreach (string key in dictsBySourceFile.Keys)
{
- ColumnSeries column = new ColumnSeries { ColumnWidth = 200, IsStacked = true, Title = key, TrackerFormatString = "Bin: {bin}\n{0}: {2}\nTotal: {total}" };
+ ColumnSeries column = new ColumnSeries
+ {
+ ColumnWidth = 200,
+ IsStacked = true,
+ Title = key,
+ TrackerFormatString = "Bin: {bin}\n{0}: {2}\nTotal: {total}",
+ BaseValue = parameters.UseLogScaleYAxis ? 0.1 : 0
+ };
+
foreach (var d in dictsBySourceFile[key])
{
- int id = categoryIDs[d.Key];
- double total = 1.0;
- if (MetaDrawSettings.NormalizeHistogramToFile)
- {
- total = dictsBySourceFile[key].Values.Sum();
- }
- column.Items.Add(new HistItem(d.Value / total, id, d.Key, totalCounts[id]));
+ if (!filteredCategoryIDs.ContainsKey(d.Key))
+ continue;
+
+ int id = filteredCategoryIDs[d.Key];
+ double total = parameters.NormalizeHistogramToFile ? dictsBySourceFile[key].Values.Sum() : 1.0;
+ double value = d.Value / total;
+ if (parameters.UseLogScaleYAxis && value < 0.1)
+ value = 0.1;
+
+ column.Items.Add(new HistItem(value, id, d.Key, filteredCounts[id]));
- category[categoryIDs[d.Key]] = d.Key;
+ PlotData.Add(new Dictionary
+ {
+ { "Source File", key }, { "Category", d.Key },
+ { "Value", d.Value.ToString() }, { "Total", filteredCounts[id].ToString() }
+ });
}
privateModel.Series.Add(column);
}
}
- else // numerical histogram
+
+ return (category, filteredCounts, categoriesPerGroup, allGroupKeys);
+ }
+
+ private Dictionary GetCategoryDictForGroup(int plotType, ObservableCollection groupPsms)
+ {
+ if (plotType == 5)
+ {
+ var psmsWithMods = groupPsms.Where(p => !p.FullSequence.Contains("|") && p.FullSequence.Contains("["));
+ var mods = psmsWithMods.Select(p => p.ToBioPolymerWithSetMods()).Select(p => p.AllModsOneIsNterminus).SelectMany(p => p.Values);
+ return mods.GroupBy(p => p.IdWithMotif).ToDictionary(p => p.Key, v => v.Count());
+ }
+
+ var allMatchedIons = groupPsms.SelectMany(p => p.MatchedIons).ToList();
+ if (plotType == 10)
+ {
+ return allMatchedIons
+ .GroupBy(p => p.NeutralTheoreticalProduct is CustomMProduct cmp ? cmp.Annotation : p.NeutralTheoreticalProduct.ProductType.ToString())
+ .ToDictionary(p => p.Key.ToString(), p => p.Count());
+ }
+
+ return allMatchedIons
+ .GroupBy(p => p.NeutralTheoreticalProduct is CustomMProduct cmp ? cmp.Annotation : p.NeutralTheoreticalProduct.ProductType.ToString())
+ .ToDictionary(p => p.Key.ToString(), p => (int)p.Sum(m => m.Intensity));
+ }
+
+ #endregion
+
+ #region Numerical Histogram Series
+
+ private (string[] category, int[] totalCounts, int categoriesPerGroup, List allGroupKeys)
+ BuildNumericalHistogramSeries(int plotType, double binSize,
+ Dictionary> dictsBySourceFile,
+ Dictionary> numbersBySourceFile,
+ Dictionary>> groupedPsmsBySourceFile,
+ bool isGroupingEnabled)
+ {
+ int end = dictsBySourceFile.Values.Max(p => p.Max(v => int.Parse(v.Key)));
+ int start = dictsBySourceFile.Values.Min(p => p.Min(v => int.Parse(v.Key)));
+ int numBins = end - start + 1;
+ int minBinLabels = 22;
+ int skipBinLabel = numBins < minBinLabels ? 1 : numBins / minBinLabels;
+
+ var totalCounts = new int[numBins];
+ int maxCount = 0;
+ foreach (Dictionary dict in dictsBySourceFile.Values)
+ {
+ foreach (var kvp in dict)
+ {
+ int idx = int.Parse(kvp.Key) - start;
+ if (idx >= 0 && idx < totalCounts.Length)
+ totalCounts[idx] += kvp.Value;
+ if (totalCounts[idx] > maxCount)
+ maxCount = totalCounts[idx];
+ }
+ }
+
+ double minThreshold = maxCount * (parameters.MinRelativeCutoff / 100.0);
+ double maxThreshold = maxCount * (parameters.MaxRelativeCutoff / 100.0);
+
+ var filteredCategories = new List();
+ var filteredTotalCounts = new List();
+ var binToFilteredIndex = new Dictionary();
+ int filteredIndex = 0;
+
+ for (int i = start; i <= end; i++)
+ {
+ int binIdx = i - start;
+ if (totalCounts[binIdx] >= minThreshold && totalCounts[binIdx] <= maxThreshold)
+ {
+ binToFilteredIndex[i] = filteredIndex;
+ filteredCategories.Add(i % skipBinLabel == 0
+ ? Math.Round((i * binSize), 2).ToString(CultureInfo.InvariantCulture)
+ : "");
+ filteredTotalCounts.Add(totalCounts[binIdx]);
+ filteredIndex++;
+ }
+ }
+
+ var category = filteredCategories.ToArray();
+ var filteredCounts = filteredTotalCounts.ToArray();
+ int categoriesPerGroup = binToFilteredIndex.Count;
+ List allGroupKeys = null;
+
+ if (isGroupingEnabled)
{
- IEnumerable allNumbers = numbersBySourceFile.Values.SelectMany(x => x);
-
- int end = dictsBySourceFile.Values.Max(p => p.Max(v => int.Parse(v.Key)));
- int start = dictsBySourceFile.Values.Min(p => p.Min(v => int.Parse(v.Key)));
- int numBins = end - start + 1;
- int minBinLabels = 22; // the number of labeled bins will be between minBinLabels and 2 * minBinLabels
- int skipBinLabel = numBins < minBinLabels ? 1 : numBins / minBinLabels;
-
- // assign axis labels, skip labels based on skipBinLabel, calculate bin totals across all files
- category = new string[numBins];
- totalCounts = new int[numBins];
- for (int i = start; i <= end; i++)
+ allGroupKeys = OrderByNaturalKey(
+ groupedPsmsBySourceFile.Values
+ .SelectMany(d => d.Keys)
+ .Distinct())
+ .ToList();
+
+ var nestedCategories = new List();
+ var categoryToIndex = new Dictionary();
+ int categoryIndex = 0;
+
+ foreach (var groupKey in allGroupKeys)
{
- if (i % skipBinLabel == 0)
+ foreach (var binKey in binToFilteredIndex.Keys.OrderBy(k => k))
{
- category[i - start] = Math.Round((i * binSize), 2).ToString(CultureInfo.InvariantCulture);
+ string binLabel = Math.Round((binKey * binSize), 2).ToString(CultureInfo.InvariantCulture);
+ nestedCategories.Add(binLabel);
+ categoryToIndex[binKey.ToString() + "|" + groupKey] = categoryIndex++;
}
- foreach (Dictionary dict in dictsBySourceFile.Values)
+ }
+
+ foreach (string sourceFile in psmsBySourceFile.Keys)
+ {
+ var column = new ColumnSeries
+ {
+ ColumnWidth = 200,
+ IsStacked = true,
+ Title = sourceFile,
+ TrackerFormatString = "Bin: {bin}\n{0}: {2}\nGroup: {group}\nTotal: {total}",
+ BaseValue = parameters.UseLogScaleYAxis ? 0.1 : 0
+ };
+
+ foreach (string groupKey in allGroupKeys)
{
- totalCounts[i - start] += dict.ContainsKey(i.ToString(CultureInfo.InvariantCulture)) ? dict[i.ToString(CultureInfo.InvariantCulture)] : 0;
+ if (!groupedPsmsBySourceFile[sourceFile].ContainsKey(groupKey))
+ {
+ foreach (var binKey in binToFilteredIndex.Keys.OrderBy(k => k))
+ {
+ string lookupKey = binKey.ToString() + "|" + groupKey;
+ if (categoryToIndex.ContainsKey(lookupKey))
+ column.Items.Add(new HistItem(0, categoryToIndex[lookupKey],
+ (binKey * binSize).ToString(CultureInfo.InvariantCulture), filteredCounts[binToFilteredIndex[binKey]], groupKey));
+ }
+ continue;
+ }
+
+ var groupPsms = groupedPsmsBySourceFile[sourceFile][groupKey];
+ var groupNumbers = GetNumbersFromPsms(groupPsms, plotType);
+ var groupDict = groupNumbers.GroupBy(p => roundToBin(p, binSize)).OrderBy(p => p.Key)
+ .ToDictionary(p => p.Key.ToString(), v => v.Count());
+
+ foreach (var binKey in binToFilteredIndex.Keys.OrderBy(k => k))
+ {
+ string lookupKey = binKey.ToString() + "|" + groupKey;
+ if (!categoryToIndex.ContainsKey(lookupKey))
+ continue;
+
+ double total = parameters.NormalizeHistogramToFile ? groupDict.Values.Sum() : 1.0;
+ int rawValue = groupDict.ContainsKey(binKey.ToString()) ? groupDict[binKey.ToString()] : 0;
+ double value = rawValue / total;
+ if (parameters.UseLogScaleYAxis && value > 0 && value < 0.1)
+ value = 0.1;
+
+ column.Items.Add(new HistItem(value, categoryToIndex[lookupKey],
+ (binKey * binSize).ToString(CultureInfo.InvariantCulture), filteredCounts[binToFilteredIndex[binKey]], groupKey));
+
+ PlotData.Add(new Dictionary
+ {
+ { "Source File", sourceFile }, { parameters.GroupingProperty, groupKey },
+ { "Bin", (binKey * binSize).ToString(CultureInfo.InvariantCulture) },
+ { "Value", rawValue.ToString() }, { "Total", filteredCounts[binToFilteredIndex[binKey]].ToString() }
+ });
+ }
}
+ privateModel.Series.Add(column);
}
- // add a column series for each file
+ category = nestedCategories.ToArray();
+ }
+ else
+ {
foreach (string key in dictsBySourceFile.Keys)
{
- var column = new ColumnSeries { ColumnWidth = 200, IsStacked = true, Title = key, TrackerFormatString = "Bin: {bin}\n{0}: {2}\nTotal: {total}" };
+ var column = new ColumnSeries
+ {
+ ColumnWidth = 200,
+ IsStacked = true,
+ Title = key,
+ TrackerFormatString = "Bin: {bin}\n{0}: {2}\nTotal: {total}",
+ BaseValue = parameters.UseLogScaleYAxis ? 0.1 : 0
+ };
+
foreach (var d in dictsBySourceFile[key])
{
int bin = int.Parse(d.Key);
- double total = 1.0;
- if (MetaDrawSettings.NormalizeHistogramToFile)
+ if (!binToFilteredIndex.ContainsKey(bin))
+ continue;
+
+ int filteredIdx = binToFilteredIndex[bin];
+ double total = parameters.NormalizeHistogramToFile ? dictsBySourceFile[key].Values.Sum() : 1.0;
+ double value = d.Value / total;
+ if (parameters.UseLogScaleYAxis && value < 0.1)
+ value = 0.1;
+
+ column.Items.Add(new HistItem(value, filteredIdx,
+ (bin * binSize).ToString(CultureInfo.InvariantCulture), filteredCounts[filteredIdx]));
+
+ PlotData.Add(new Dictionary
{
- total = dictsBySourceFile[key].Values.Sum(m => m);
- }
- column.Items.Add(new HistItem(d.Value / total, bin - start, (bin * binSize).ToString(CultureInfo.InvariantCulture), totalCounts[bin - start]));
+ { "Source File", key },
+ { "Bin", (bin * binSize).ToString(CultureInfo.InvariantCulture) },
+ { "Value", d.Value.ToString() }, { "Total", filteredCounts[filteredIdx].ToString() }
+ });
}
privateModel.Series.Add(column);
}
}
- // add axes
- if (MetaDrawSettings.NormalizeHistogramToFile)
+ return (category, filteredCounts, categoriesPerGroup, allGroupKeys);
+ }
+
+ #endregion
+
+ #region Axis Configuration
+
+ private void ConfigureHistogramAxes(string xAxisTitle, string yAxisTitle, double labelAngle,
+ string[] category, bool isGroupingEnabled, List allGroupKeys, int categoriesPerGroup)
+ {
+ if (parameters.NormalizeHistogramToFile)
xAxisTitle = $"File Normalized {xAxisTitle}";
- privateModel.Axes.Add(new CategoryAxis
+
+ double bottomPadding = isGroupingEnabled ? 20 : 0;
+ privateModel.Padding = new OxyThickness(privateModel.Padding.Left, privateModel.Padding.Top,
+ privateModel.Padding.Right, privateModel.Padding.Bottom + bottomPadding);
+
+ var mainAxis = new CategoryAxis
{
Position = AxisPosition.Bottom,
ItemsSource = category,
- Title = xAxisTitle,
+ Title = isGroupingEnabled ? null : xAxisTitle,
GapWidth = 0.3,
Angle = labelAngle,
- });
- privateModel.Axes.Add(new LinearAxis { Title = yAxisTitle, Position = AxisPosition.Left, AbsoluteMinimum = 0 });
+ };
+
+ if (isGroupingEnabled && allGroupKeys != null && allGroupKeys.Count > 0 && categoriesPerGroup > 0)
+ {
+ mainAxis.GapWidth = 0.1;
+
+ if (allGroupKeys.Count > 1)
+ {
+ mainAxis.ExtraGridlines = new double[allGroupKeys.Count - 1];
+ for (int i = 1; i < allGroupKeys.Count; i++)
+ mainAxis.ExtraGridlines[i - 1] = i * categoriesPerGroup - 0.5;
+ mainAxis.ExtraGridlineStyle = LineStyle.Solid;
+ mainAxis.ExtraGridlineColor = OxyColors.LightGray;
+ mainAxis.ExtraGridlineThickness = 2;
+ }
+
+ var labelledCategories = new string[category.Length];
+ for (int g = 0; g < allGroupKeys.Count; g++)
+ {
+ int startIdx = g * categoriesPerGroup;
+ for (int j = 0; j < categoriesPerGroup; j++)
+ {
+ int idx = startIdx + j;
+ if (idx >= category.Length) break;
+ labelledCategories[idx] = category[idx];
+ }
+ }
+ mainAxis.ItemsSource = labelledCategories;
+ mainAxis.Title = xAxisTitle;
+
+ var groupLabels = new string[category.Length];
+ for (int g = 0; g < allGroupKeys.Count; g++)
+ {
+ int startIdx = g * categoriesPerGroup;
+ int midIdx = startIdx + categoriesPerGroup / 2;
+ for (int j = 0; j < categoriesPerGroup; j++)
+ {
+ int idx = startIdx + j;
+ if (idx >= groupLabels.Length) break;
+ groupLabels[idx] = idx == midIdx ? allGroupKeys[g] : "";
+ }
+ }
+
+ var groupAxis = new CategoryAxis
+ {
+ Position = AxisPosition.Bottom,
+ Key = "GroupAxis",
+ ItemsSource = groupLabels,
+ GapWidth = 0.1,
+ Angle = 0,
+ Title = parameters.GroupingProperty,
+ TitleFontWeight = FontWeights.Normal,
+ TitleFontSize = privateModel.DefaultFontSize + 2,
+ FontSize = privateModel.DefaultFontSize + 2,
+ FontWeight = FontWeights.Bold,
+ TickStyle = TickStyle.None,
+ IsAxisVisible = true,
+ AxislineStyle = LineStyle.None,
+ MajorGridlineStyle = LineStyle.None,
+ MinorGridlineStyle = LineStyle.None,
+ PositionTier = 1,
+ };
+ privateModel.Axes.Add(groupAxis);
+ }
+
+ privateModel.Axes.Add(mainAxis);
+
+ if (parameters.UseLogScaleYAxis)
+ {
+ privateModel.Axes.Add(new LogarithmicAxis
+ {
+ Title = yAxisTitle,
+ Position = AxisPosition.Left,
+ AbsoluteMinimum = 0.1,
+ Minimum = 0.1,
+ Base = 10,
+ Key = "Primary"
+ });
+ }
+ else
+ {
+ privateModel.Axes.Add(new LinearAxis
+ {
+ Title = yAxisTitle,
+ Position = AxisPosition.Left,
+ AbsoluteMinimum = 0,
+ Minimum = 0,
+ Key = "Primary"
+ });
+ }
}
+ #endregion
+
private void linePlot(int plotType)
{
string yAxisTitle = "";
@@ -389,6 +859,14 @@ private void linePlot(int plotType)
{
variantxy.Add(new Tuple(double.Parse(psm.MassDiffPpm, CultureInfo.InvariantCulture), (double)psm.RetentionTime, psm.FullSequence));
}
+
+ PlotData.Add(new Dictionary
+ {
+ { "Retention Time", psm.RetentionTime.ToString(CultureInfo.InvariantCulture) },
+ { "Precursor Error Ppm", psm.MassDiffPpm },
+ { "Full Sequence", psm.FullSequence },
+ { "Is Variant", (psm.IdentifiedSequenceVariations != null && !psm.IdentifiedSequenceVariations.Equals("")).ToString() }
+ });
}
break;
case 3: // Predicted RT vs. Observed RT
@@ -397,44 +875,45 @@ private void linePlot(int plotType)
SSRCalc3 sSRCalc3 = new SSRCalc3("A100", SSRCalc3.Column.A100);
foreach (var psm in allSpectralMatches)
{
+ double predicted = sSRCalc3.ScoreSequence(new PeptideWithSetModifications(psm.BaseSeq.Split('|')[0], null));
if (psm.IdentifiedSequenceVariations == null || psm.IdentifiedSequenceVariations.Equals(""))
{
- xy.Add(new Tuple(sSRCalc3.ScoreSequence(new PeptideWithSetModifications(psm.BaseSeq.Split('|')[0], null)),
- (double)psm.RetentionTime, psm.FullSequence));
+ xy.Add(new Tuple(predicted, (double)psm.RetentionTime, psm.FullSequence));
}
else
{
- variantxy.Add(new Tuple(sSRCalc3.ScoreSequence(new PeptideWithSetModifications(psm.BaseSeq.Split('|')[0], null)),
- (double)psm.RetentionTime, psm.FullSequence));
+ variantxy.Add(new Tuple(predicted, (double)psm.RetentionTime, psm.FullSequence));
}
+
+ PlotData.Add(new Dictionary
+ {
+ { "Retention Time", psm.RetentionTime.ToString(CultureInfo.InvariantCulture) },
+ { "Predicted Hydrophobicity", predicted.ToString(CultureInfo.InvariantCulture) },
+ { "Full Sequence", psm.FullSequence },
+ { "Is Variant", (psm.IdentifiedSequenceVariations != null && !psm.IdentifiedSequenceVariations.Equals("")).ToString() }
+ });
}
break;
}
if (xy.Count != 0)
{
- // plot each peptide
IOrderedEnumerable> sorted = xy.OrderBy(x => x.Item1);
foreach (var val in sorted)
{
series.Points.Add(new ScatterPoint(val.Item2, val.Item1, tag: val.Item3));
}
privateModel.Series.Add(series);
-
- // add series displayed in legend, the real series will show up with a tiny dot for the symbol
privateModel.Series.Add(new ScatterSeries { Title = "non-variant PSMs", MarkerFill = OxyColors.Blue });
}
if (variantxy.Count != 0)
{
- // plot each variant peptide
IOrderedEnumerable> variantSorted = variantxy.OrderBy(x => x.Item1);
foreach (var val in variantSorted)
{
variantSeries.Points.Add(new ScatterPoint(val.Item2, val.Item1, tag: val.Item3));
}
privateModel.Series.Add(variantSeries);
-
- // add series displayed in legend, the real series will show up with a tiny dot for the symbol
privateModel.Series.Add(new ScatterSeries { Title = "variant PSMs", MarkerFill = OxyColors.DarkRed });
}
privateModel.Axes.Add(new LinearAxis { Title = xAxisTitle, Position = AxisPosition.Bottom });
@@ -451,16 +930,126 @@ private static int roundToBin(double number, double binSize)
return i * sign;
}
- // used by histogram plots, gives additional properies for the tracker to display
+ ///
+ /// Groups PSMs by the specified property for side-by-side plotting.
+ /// Excludes ambiguous PSMs (those with "|" in FullSequence) since they cannot be
+ /// cleanly assigned to a single group.
+ ///
+ private Dictionary>> GroupPsmsByProperty(
+ Dictionary> psmsByFile, string propertyName)
+ {
+ var result = new Dictionary>>();
+
+ foreach (var sourceFile in psmsByFile.Keys)
+ {
+ var groupedPsms = new Dictionary>();
+
+ foreach (var psm in psmsByFile[sourceFile])
+ {
+ string groupKey = GetGroupKeyFromPsm(psm, propertyName);
+
+ if (groupKey.Contains('|'))
+ {
+ if (MetaDrawSettings.AmbiguityFilter == "1")
+ continue;
+
+ if (!parameters.AllowAmbiguousGroups)
+ continue;
+
+ groupKey = NormalizeAmbiguousGroupKey(groupKey);
+ }
+
+ if (!groupedPsms.ContainsKey(groupKey))
+ groupedPsms[groupKey] = new ObservableCollection();
+
+ groupedPsms[groupKey].Add(psm);
+ }
+
+ result[sourceFile] = groupedPsms;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Normalizes an ambiguous group key by sorting its pipe-delimited parts into a canonical order.
+ /// This ensures that keys like "0|2|1" and "1|0|2" map to the same group ("0|1|2").
+ /// Uses natural (numeric) sorting when all parts are numeric.
+ ///
+ private static string NormalizeAmbiguousGroupKey(string groupKey)
+ {
+ var parts = groupKey.Split('|');
+ return string.Join("|", OrderByNaturalKey(parts));
+ }
+
+ private string GetGroupKeyFromPsm(SpectrumMatchFromTsv psm, string propertyName)
+ {
+ return propertyName switch
+ {
+ "Notch" => psm.Notch ?? "0",
+ "Precursor Charge" => psm.PrecursorCharge.ToString(),
+ "File Name" => psm.FileNameWithoutExtension,
+ "Ambiguity Level" => psm.AmbiguityLevel ?? "1",
+ "Missed Cleavages" => psm.MissedCleavage ?? "0",
+ "OrganismName" => psm.OrganismName,
+ "DecoyContamTarget" => psm.DecoyContamTarget,
+
+ _ => "All"
+ };
+ }
+
+ private IEnumerable GetNumbersFromPsms(ObservableCollection psms, int plotType)
+ {
+ return plotType switch
+ {
+ 1 => psms.Where(p => !p.MassDiffDa.Contains("|") && Math.Round(double.Parse(p.MassDiffDa, CultureInfo.InvariantCulture), 0) == 0)
+ .Select(p => double.Parse(p.MassDiffPpm, CultureInfo.InvariantCulture)),
+ 2 => psms.SelectMany(p => p.MatchedIons.Select(v => v.MassErrorPpm)),
+ 3 => psms.Select(p => (double)p.PrecursorCharge),
+ 4 => psms.SelectMany(p => p.MatchedIons.Select(v => (double)v.Charge)),
+ 6 => psms.Select(p => (double)p.PrecursorMass),
+ 7 => psms.Select(p => (double)p.PrecursorMz),
+ 8 => GetHydrophobicityScores(psms),
+ 9 => psms.Where(p => !p.MissedCleavage.Contains("|")).Select(p => double.Parse(p.MissedCleavage)),
+ 12 => psms.Select(p => (double)(int)Math.Round(p.RetentionTime, 0)),
+ _ => Enumerable.Empty()
+ };
+ }
+
+ private IEnumerable GetHydrophobicityScores(ObservableCollection psms)
+ {
+ SSRCalc3 sSRCalc3 = new SSRCalc3("A100", SSRCalc3.Column.A100);
+ var values = new List();
+ foreach (var psm in psms.Where(p => p is not OsmFromTsv))
+ {
+ values.Add(sSRCalc3.ScoreSequence(new PeptideWithSetModifications(psm.BaseSeq.Split("|")[0], null)));
+ }
+ return values;
+ }
+
private class HistItem : ColumnItem
{
public int total { get; set; }
public string bin { get; set; }
- public HistItem(double value, int categoryIndex, string bin, int total) : base(value, categoryIndex)
+ public string group { get; set; }
+
+ public HistItem(double value, int categoryIndex, string bin, int total, string group = null) : base(value, categoryIndex)
{
this.total = total;
this.bin = bin;
+ this.group = group;
}
}
+
+ ///
+ /// Orders strings numerically if all values are numeric, otherwise alphabetically.
+ ///
+ private static IOrderedEnumerable OrderByNaturalKey(IEnumerable keys)
+ {
+ bool allNumeric = keys.All(k => double.TryParse(k, NumberStyles.Any, CultureInfo.InvariantCulture, out _));
+ return allNumeric
+ ? keys.OrderBy(k => double.Parse(k, CultureInfo.InvariantCulture))
+ : keys.OrderBy(k => k);
+ }
}
}
\ No newline at end of file
diff --git a/MetaMorpheus/GuiFunctions/MetaDraw/PlotModelStatParameters.cs b/MetaMorpheus/GuiFunctions/MetaDraw/PlotModelStatParameters.cs
new file mode 100644
index 0000000000..3d1d183e61
--- /dev/null
+++ b/MetaMorpheus/GuiFunctions/MetaDraw/PlotModelStatParameters.cs
@@ -0,0 +1,68 @@
+namespace GuiFunctions.MetaDraw
+{
+ ///
+ /// Model class for Data Visualization plot parameters
+ ///
+ public class PlotModelStatParameters
+ {
+ ///
+ /// When true, uses logarithmic scale for Y-axis in histograms
+ ///
+ public bool UseLogScaleYAxis { get; set; } = false;
+
+ ///
+ /// Property to group PSMs by for nested X-axis plotting
+ /// Options: "None", "Notch", "Precursor Charge", "File Name", "Ambiguity Level", "Missed Cleavages"
+ ///
+ public string GroupingProperty { get; set; } = "None";
+ public bool AllowAmbiguousGroups { get; set; } = false;
+
+ ///
+ /// Minimum percentage of total for data points to be displayed (0-100)
+ /// Applied to all histogram plots
+ ///
+ public double MinRelativeCutoff { get; set; } = 0.0;
+
+ ///
+ /// Maximum percentage of total for data points to be displayed (0-100)
+ /// Applied to all histogram plots
+ ///
+ public double MaxRelativeCutoff { get; set; } = 100.0;
+
+ ///
+ /// When true, normalizes histogram counts to file totals
+ ///
+ public bool NormalizeHistogramToFile { get; set; } = false;
+
+ ///
+ /// When true, only displays PSMs that pass the current filters
+ ///
+ public bool DisplayFilteredOnly { get; set; } = true;
+
+ public PlotModelStatParameters Clone()
+ {
+ return new PlotModelStatParameters
+ {
+ UseLogScaleYAxis = this.UseLogScaleYAxis,
+ GroupingProperty = this.GroupingProperty,
+ MinRelativeCutoff = this.MinRelativeCutoff,
+ MaxRelativeCutoff = this.MaxRelativeCutoff,
+ NormalizeHistogramToFile = this.NormalizeHistogramToFile,
+ DisplayFilteredOnly = this.DisplayFilteredOnly,
+ AllowAmbiguousGroups = this.AllowAmbiguousGroups
+ };
+ }
+
+ public bool Equals(PlotModelStatParameters other)
+ {
+ if (other == null) return false;
+ return UseLogScaleYAxis == other.UseLogScaleYAxis
+ && GroupingProperty == other.GroupingProperty
+ && System.Math.Abs(MinRelativeCutoff - other.MinRelativeCutoff) < 0.0001
+ && System.Math.Abs(MaxRelativeCutoff - other.MaxRelativeCutoff) < 0.0001
+ && NormalizeHistogramToFile == other.NormalizeHistogramToFile
+ && DisplayFilteredOnly == other.DisplayFilteredOnly
+ && AllowAmbiguousGroups == other.AllowAmbiguousGroups;
+ }
+ }
+}
diff --git a/MetaMorpheus/GuiFunctions/MetaDraw/PlotModelStatParametersViewModel.cs b/MetaMorpheus/GuiFunctions/MetaDraw/PlotModelStatParametersViewModel.cs
new file mode 100644
index 0000000000..23c8e55b7e
--- /dev/null
+++ b/MetaMorpheus/GuiFunctions/MetaDraw/PlotModelStatParametersViewModel.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.ObjectModel;
+
+namespace GuiFunctions.MetaDraw
+{
+ ///
+ /// Singleton ViewModel for Data Visualization plot parameters
+ ///
+ public class PlotModelStatParametersViewModel : BaseViewModel
+ {
+ private static readonly Lazy _instance =
+ new(() => new PlotModelStatParametersViewModel());
+
+ public static PlotModelStatParametersViewModel Instance => _instance.Value;
+
+ private PlotModelStatParameters _current;
+
+ private PlotModelStatParametersViewModel()
+ {
+ _current = new PlotModelStatParameters();
+ }
+
+ public ObservableCollection GroupingProperties { get; } = new(MetaDrawSettings.GroupingProperties);
+
+ public bool UseLogScaleYAxis
+ {
+ get => _current.UseLogScaleYAxis;
+ set
+ {
+ _current.UseLogScaleYAxis = value;
+ OnPropertyChanged(nameof(UseLogScaleYAxis));
+ }
+ }
+
+ public string GroupingProperty
+ {
+ get => _current.GroupingProperty;
+ set
+ {
+ _current.GroupingProperty = value ?? "None";
+ OnPropertyChanged(nameof(GroupingProperty));
+ }
+ }
+
+ public bool AllowAmbiguousGroups
+ {
+ get => _current.AllowAmbiguousGroups;
+ set
+ {
+ _current.AllowAmbiguousGroups = value;
+ OnPropertyChanged(nameof(AllowAmbiguousGroups));
+ }
+ }
+
+ public double MinRelativeCutoff
+ {
+ get => _current.MinRelativeCutoff;
+ set
+ {
+ if (value < 0) value = 0;
+ if (value > 100) value = 100;
+ if (value > MaxRelativeCutoff) value = MaxRelativeCutoff;
+ _current.MinRelativeCutoff = value;
+ OnPropertyChanged(nameof(MinRelativeCutoff));
+ }
+ }
+
+ public double MaxRelativeCutoff
+ {
+ get => _current.MaxRelativeCutoff;
+ set
+ {
+ if (value < 0) value = 0;
+ if (value > 100) value = 100;
+ if (value < MinRelativeCutoff) value = MinRelativeCutoff;
+ _current.MaxRelativeCutoff = value;
+ OnPropertyChanged(nameof(MaxRelativeCutoff));
+ }
+ }
+
+ public bool NormalizeHistogramToFile
+ {
+ get => _current.NormalizeHistogramToFile;
+ set
+ {
+ _current.NormalizeHistogramToFile = value;
+ OnPropertyChanged(nameof(NormalizeHistogramToFile));
+ }
+ }
+
+ public bool DisplayFilteredOnly
+ {
+ get => _current.DisplayFilteredOnly;
+ set
+ {
+ _current.DisplayFilteredOnly = value;
+ OnPropertyChanged(nameof(DisplayFilteredOnly));
+ }
+ }
+
+ ///
+ /// Gets a clone of the current parameters for passing to PlotModelStat
+ ///
+ public PlotModelStatParameters GetParameters()
+ {
+ return _current.Clone();
+ }
+
+ ///
+ /// Loads parameters from a snapshot
+ ///
+ public void LoadFromSnapshot(PlotModelStatParameters parameters)
+ {
+ _current = parameters.Clone();
+ OnPropertyChanged(nameof(UseLogScaleYAxis));
+ OnPropertyChanged(nameof(GroupingProperty));
+ OnPropertyChanged(nameof(MinRelativeCutoff));
+ OnPropertyChanged(nameof(MaxRelativeCutoff));
+ OnPropertyChanged(nameof(NormalizeHistogramToFile));
+ OnPropertyChanged(nameof(DisplayFilteredOnly));
+ OnPropertyChanged(nameof(AllowAmbiguousGroups));
+ }
+ }
+}
diff --git a/MetaMorpheus/GuiFunctions/MetaDraw/SpectrumMatch/SpectrumMatchPlot.cs b/MetaMorpheus/GuiFunctions/MetaDraw/SpectrumMatch/SpectrumMatchPlot.cs
index 8e6ffc7288..7862857153 100644
--- a/MetaMorpheus/GuiFunctions/MetaDraw/SpectrumMatch/SpectrumMatchPlot.cs
+++ b/MetaMorpheus/GuiFunctions/MetaDraw/SpectrumMatch/SpectrumMatchPlot.cs
@@ -166,23 +166,28 @@ protected void AnnotatePeak(MatchedFragmentIon matchedIon, bool isBetaPeptide,
// Fragment Number annotation
if (MetaDrawSettings.SubAndSuperScriptIons)
+ {
+ char? previousCharacter = null;
foreach (var character in matchedIon.NeutralTheoreticalProduct.Annotation)
{
if (char.IsDigit(character))
peakAnnotationText += MetaDrawSettings.SubScriptNumbers[character - '0'];
- else switch (character)
- {
- case '-':
- peakAnnotationText += "\u208B"; // sub scripted Hyphen
- break;
- case '[':
- case ']':
- continue;
- default:
- peakAnnotationText += character;
- break;
- }
+ else
+ switch (character)
+ {
+ case '-' when previousCharacter is not null && char.IsNumber(previousCharacter.Value):
+ peakAnnotationText += "\u208B"; // sub scripted Hyphen
+ break;
+ case '[':
+ case ']':
+ continue;
+ default:
+ peakAnnotationText += character;
+ break;
+ }
+ previousCharacter = character;
}
+ }
else
peakAnnotationText += matchedIon.NeutralTheoreticalProduct.Annotation;
diff --git a/MetaMorpheus/GuiFunctions/Models/CustomMIonLoss.cs b/MetaMorpheus/GuiFunctions/Models/CustomMIonLoss.cs
new file mode 100644
index 0000000000..01a689a265
--- /dev/null
+++ b/MetaMorpheus/GuiFunctions/Models/CustomMIonLoss.cs
@@ -0,0 +1,102 @@
+using Chemistry;
+using EngineLayer;
+using Omics.Fragmentation;
+using System;
+
+namespace GuiFunctions.Models;
+
+///
+/// Represents a custom M-Ion loss that can be defined by the user.
+/// Extends MIonLoss with AnalyteType information for filtering between Peptide and Oligo modes.
+///
+public class CustomMIonLoss : MIonLoss
+{
+ ///
+ /// Indicates which analyte type(s) this loss applies to
+ ///
+ public AnalyteType ApplicableAnalyteType { get; set; }
+
+ public CustomMIonLoss(string name, string annotation, ChemicalFormula chemicalFormula, AnalyteType applicableAnalyteType)
+ : base(name, annotation, chemicalFormula)
+ {
+ ApplicableAnalyteType = applicableAnalyteType;
+ }
+
+ ///
+ /// Converts a CustomMIonLoss to a regular MIonLoss for use in fragmentation
+ ///
+ public MIonLoss ToMIonLoss()
+ {
+ return new MIonLoss(Name, Annotation, ThisChemicalFormula);
+ }
+
+ ///
+ /// Creates a CustomMIonLoss from a MIonLoss and AnalyteType
+ ///
+ public static CustomMIonLoss FromMIonLoss(MIonLoss mIonLoss, AnalyteType analyteType)
+ {
+ return new CustomMIonLoss(mIonLoss.Name, mIonLoss.Annotation, mIonLoss.ThisChemicalFormula, analyteType);
+ }
+
+ ///
+ /// Serializes the custom M-Ion loss to a string format for file storage
+ /// Format: Name|Annotation|ChemicalFormula|AnalyteType
+ ///
+ public string Serialize()
+ {
+ return $"{Name}|{Annotation}|{ThisChemicalFormula.Formula}|{ApplicableAnalyteType}";
+ }
+
+ ///
+ /// Deserializes a custom M-Ion loss from a string
+ ///
+ public static CustomMIonLoss Deserialize(string line)
+ {
+ var parts = line.Split('|');
+ if (parts.Length != 4)
+ {
+ throw new FormatException($"Invalid custom M-Ion loss format: {line}");
+ }
+
+ string name = parts[0];
+ string annotation = parts[1];
+ string formulaString = parts[2];
+
+ if (!Enum.TryParse(parts[3], out var analyteType))
+ {
+ throw new FormatException($"Invalid AnalyteType in custom M-Ion loss: {parts[3]}");
+ }
+
+ ChemicalFormula formula;
+ try
+ {
+ formula = ChemicalFormula.ParseFormula(formulaString);
+ }
+ catch (Exception ex)
+ {
+ throw new FormatException($"Invalid chemical formula '{formulaString}' in custom M-Ion loss", ex);
+ }
+
+ return new CustomMIonLoss(name, annotation, formula, analyteType);
+ }
+
+ ///
+ /// Checks if this loss is applicable to the current analyte type
+ ///
+ public bool IsApplicableToCurrentMode(AnalyteType? currentAnalyteType = null)
+ {
+ switch (ApplicableAnalyteType)
+ {
+ // If the loss is for both types, it's always applicable
+ case AnalyteType.Peptide when currentAnalyteType == AnalyteType.Peptide:
+ case AnalyteType.Peptide when currentAnalyteType == null && !GuiGlobalParamsViewModel.Instance.IsRnaMode:
+ case AnalyteType.Oligo when currentAnalyteType == AnalyteType.Oligo:
+ case AnalyteType.Oligo when currentAnalyteType == null && GuiGlobalParamsViewModel.Instance.IsRnaMode:
+ case AnalyteType.Proteoform when currentAnalyteType == AnalyteType.Proteoform:
+ case AnalyteType.Proteoform when currentAnalyteType == null && !GuiGlobalParamsViewModel.Instance.IsRnaMode:
+ return true;
+ default:
+ return false;
+ }
+ }
+}
diff --git a/MetaMorpheus/GuiFunctions/Util/CustomMIonLossManager.cs b/MetaMorpheus/GuiFunctions/Util/CustomMIonLossManager.cs
new file mode 100644
index 0000000000..c8852b409a
--- /dev/null
+++ b/MetaMorpheus/GuiFunctions/Util/CustomMIonLossManager.cs
@@ -0,0 +1,180 @@
+using EngineLayer;
+using GuiFunctions.Models;
+using Omics.Fragmentation;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace GuiFunctions.Util;
+
+///
+/// Manages loading and saving of custom M-Ion losses with caching
+///
+public static class CustomMIonLossManager
+{
+ private static readonly string CustomMIonLossFileName = "CustomMIonLosses.txt";
+ private static List _cachedLosses;
+ private static DateTime _lastFileWriteTime;
+
+ ///
+ /// Gets the full path to the custom M-Ion losses file
+ ///
+ public static string GetCustomMIonLossFilePath()
+ {
+ string modsDirectory = Path.Combine(GlobalVariables.DataDir, "Mods");
+ if (!Directory.Exists(modsDirectory))
+ {
+ Directory.CreateDirectory(modsDirectory);
+ }
+ return Path.Combine(modsDirectory, CustomMIonLossFileName);
+ }
+
+ ///
+ /// Clears the cache, forcing a reload on next access
+ ///
+ public static void ClearCache()
+ {
+ _cachedLosses = null;
+ }
+
+ ///
+ /// Loads all custom M-Ion losses from the file with caching
+ ///
+ public static List LoadCustomMIonLosses()
+ {
+ string filePath = GetCustomMIonLossFilePath();
+
+ // Check if cache is valid
+ var currentFileWriteTime = File.GetLastWriteTime(filePath);
+ if (_cachedLosses != null && _lastFileWriteTime == currentFileWriteTime)
+ {
+ return [.._cachedLosses]; // Return a copy to prevent modification
+ }
+
+ // Load from file
+ var customLosses = new List();
+ try
+ {
+ var lines = File.ReadAllLines(filePath);
+ foreach (var line in lines)
+ {
+ // Skip empty lines and comments
+ if (string.IsNullOrWhiteSpace(line) || line.TrimStart().StartsWith("#"))
+ {
+ continue;
+ }
+
+ try
+ {
+ var customLoss = CustomMIonLoss.Deserialize(line);
+ customLosses.Add(customLoss);
+ }
+ catch (Exception ex)
+ {
+ // Log the error but continue loading other losses
+ System.Diagnostics.Debug.WriteLine($"Error loading custom M-Ion loss: {ex.Message}");
+ }
+ }
+
+ // Update cache
+ _cachedLosses = customLosses;
+ _lastFileWriteTime = currentFileWriteTime;
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error reading custom M-Ion losses file: {ex.Message}");
+ }
+
+ return [..customLosses];
+ }
+
+ ///
+ /// Saves all custom M-Ion losses to the file and updates cache
+ ///
+ public static void SaveCustomMIonLosses(IEnumerable losses)
+ {
+ string filePath = GetCustomMIonLossFilePath();
+
+ try
+ {
+ var lines = new List
+ {
+ "# Custom M-Ion Losses for MetaMorpheus",
+ "# Format: Name|Annotation|ChemicalFormula|AnalyteType",
+ "# AnalyteType can be: Peptide, Oligo, or Proteoform",
+ "#"
+ };
+
+ lines.AddRange(losses.Select(loss => loss.Serialize()));
+
+ File.WriteAllLines(filePath, lines);
+
+ // Update cache
+ _cachedLosses = new List(losses);
+ _lastFileWriteTime = File.GetLastWriteTime(filePath);
+ }
+ catch (Exception ex)
+ {
+ throw new IOException($"Failed to save custom M-Ion losses: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Adds a new custom M-Ion loss to the file and updates cache
+ ///
+ public static void AddCustomMIonLoss(CustomMIonLoss loss)
+ {
+ var existingLosses = LoadCustomMIonLosses();
+
+ // Check for duplicates
+ if (existingLosses.Any(l => l.Name.Equals(loss.Name, StringComparison.OrdinalIgnoreCase)
+ && l.ApplicableAnalyteType == loss.ApplicableAnalyteType))
+ {
+ throw new InvalidOperationException($"A custom M-Ion loss with the name '{loss.Name}' already exists for {loss.ApplicableAnalyteType} mode.");
+ }
+
+ existingLosses.Add(loss);
+ SaveCustomMIonLosses(existingLosses);
+ }
+
+ ///
+ /// Gets all M-Ion losses (built-in + custom) for the specified analyte type
+ ///
+ public static List GetAllMIonLossesForAnalyteType(AnalyteType analyteType)
+ {
+ var allLosses = new List();
+
+ // Add built-in losses from MIonLoss.AllMIonLosses
+ if (MIonLoss.AllMIonLosses != null)
+ {
+ allLosses.AddRange(MIonLoss.AllMIonLosses.Values);
+ }
+
+ // Add applicable custom losses
+ var customLosses = LoadCustomMIonLosses()
+ .Where(l => l.IsApplicableToCurrentMode(analyteType))
+ .Select(l => l.ToMIonLoss());
+
+ allLosses.AddRange(customLosses);
+
+ return allLosses;
+ }
+
+ ///
+ /// Deletes a custom M-Ion loss from the file and updates cache
+ ///
+ public static void DeleteCustomMIonLoss(string name, AnalyteType analyteType)
+ {
+ var existingLosses = LoadCustomMIonLosses();
+ var lossToRemove = existingLosses.FirstOrDefault(l =>
+ l.Name.Equals(name, StringComparison.OrdinalIgnoreCase)
+ && l.ApplicableAnalyteType == analyteType);
+
+ if (lossToRemove != null)
+ {
+ existingLosses.Remove(lossToRemove);
+ SaveCustomMIonLosses(existingLosses);
+ }
+ }
+}
diff --git a/MetaMorpheus/GuiFunctions/ViewModels/FragmentationParamsViewModel.cs b/MetaMorpheus/GuiFunctions/ViewModels/FragmentationParamsViewModel.cs
new file mode 100644
index 0000000000..487a7f5651
--- /dev/null
+++ b/MetaMorpheus/GuiFunctions/ViewModels/FragmentationParamsViewModel.cs
@@ -0,0 +1,330 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Windows.Input;
+using Easy.Common.Extensions;
+using EngineLayer;
+using Omics.Digestion;
+using Omics.Fragmentation;
+using Proteomics.ProteolyticDigestion;
+using TaskLayer;
+using Transcriptomics;
+using Transcriptomics.Digestion;
+using GuiFunctions.Util;
+
+namespace GuiFunctions;
+
+///
+/// View model for managing fragmentation parameters in the GUI.
+/// Contains properties specific to RNA fragmentation when in RNA mode.
+///
+public class FragmentationParamsViewModel : BaseViewModel
+{
+ private bool _generateMIon;
+ private bool _generateComplementaryIons;
+ private bool _leftSideFragmentIons;
+ private bool _rightSideFragmentIons;
+ private double _maxFragmentMassDa;
+ private int _minInternalIonLength;
+ private bool _useInternalIons;
+ private bool _modsCanSuppressBaseLossFragments;
+
+ public FragmentationParamsViewModel(CommonParameters initialParams, SearchParameters searchParams)
+ {
+ // Initialize from provided parameters
+ _generateComplementaryIons = initialParams.AddCompIons;
+ if (initialParams.FragmentationParameters is not null)
+ {
+ _generateMIon = initialParams.FragmentationParameters.GenerateMIon;
+ _modsCanSuppressBaseLossFragments = initialParams.FragmentationParameters is RnaFragmentationParams
+ {
+ ModificationsCanSuppressBaseLossIons: true
+ };
+ }
+ else
+ {
+ _modsCanSuppressBaseLossFragments = false;
+ _generateMIon = GuiGlobalParamsViewModel.Instance.IsRnaMode;
+ }
+
+ _maxFragmentMassDa = searchParams.MaxFragmentSize;
+ _minInternalIonLength = searchParams.MinAllowedInternalFragmentLength;
+ _useInternalIons = searchParams.MinAllowedInternalFragmentLength > 0;
+
+ // Initialize fragment ion termini
+ var terminus = initialParams.DigestionParams.FragmentationTerminus;
+ _leftSideFragmentIons = terminus == FragmentationTerminus.FivePrime || terminus == FragmentationTerminus.N || terminus == FragmentationTerminus.Both;
+ _rightSideFragmentIons = terminus == FragmentationTerminus.ThreePrime || terminus == FragmentationTerminus.C || terminus == FragmentationTerminus.Both;
+
+ // Load available M-ion losses (built-in + custom)
+ var availableMIonLosses = LoadAvailableMIonLosses(initialParams);
+ AvailableMIonLosses = new ObservableCollection(availableMIonLosses);
+
+ AddAllLossesCommand = new RelayCommand(AddAllLosses);
+ RemoveAllLossesCommand = new RelayCommand(RemoveAllLosses);
+ }
+
+ private List LoadAvailableMIonLosses(CommonParameters initialParams)
+ {
+ var allLosses = new List();
+
+ // Determine which losses were previously selected
+ var selectedLosses = initialParams.FragmentationParameters?.MIonLosses ?? new List();
+
+ if (GuiGlobalParamsViewModel.Instance.IsRnaMode)
+ {
+ // Ensure static constructor has run to populate MIonLoss.AllMIonLosses
+ _ = new RnaFragmentationParams();
+
+ // Add built-in RNA losses
+ foreach (var loss in MIonLoss.AllMIonLosses.Values)
+ {
+ bool isSelected = selectedLosses.Any(l => l.Annotation == loss.Annotation);
+ allLosses.Add(new MIonLossViewModel(loss, isSelected, false));
+ }
+ }
+ else
+ {
+ // Add built-in peptide losses (if any exist)
+ if (MIonLoss.AllMIonLosses.TryGetValue("-H2O", out var waterLoss))
+ {
+ bool isSelected = selectedLosses.Any(l => l.Annotation == waterLoss.Annotation);
+ allLosses.Add(new MIonLossViewModel(waterLoss, isSelected, true));
+ }
+ if (MIonLoss.AllMIonLosses.TryGetValue("-NH3", out var ammoniaLoss))
+ {
+ bool isSelected = selectedLosses.Any(l => l.Annotation == ammoniaLoss.Annotation);
+ allLosses.Add(new MIonLossViewModel(ammoniaLoss, isSelected, true));
+ }
+ }
+
+ // Add custom losses
+ var customLosses = CustomMIonLossManager.LoadCustomMIonLosses()
+ .Where(l => l.IsApplicableToCurrentMode());
+
+ foreach (var customLoss in customLosses)
+ {
+ var mIonLoss = customLoss.ToMIonLoss();
+ bool isSelected = selectedLosses.Any(l => l.Annotation == customLoss.Annotation);
+ allLosses.Add(new MIonLossViewModel(mIonLoss, isSelected, true));
+ }
+
+ return allLosses;
+ }
+
+ public ICommand AddAllLossesCommand { get; set; }
+ public ICommand RemoveAllLossesCommand { get; set; }
+
+ public bool GenerateMIon
+ {
+ get => _generateMIon;
+ set
+ {
+ _generateMIon = value;
+ OnPropertyChanged(nameof(GenerateMIon));
+ }
+ }
+
+ public bool ModsCanSuppressBaseLossFragments
+ {
+ get => _modsCanSuppressBaseLossFragments;
+ set
+ {
+ _modsCanSuppressBaseLossFragments = value;
+ OnPropertyChanged(nameof(ModsCanSuppressBaseLossFragments));
+ }
+ }
+
+ public bool GenerateComplementaryIons
+ {
+ get => _generateComplementaryIons;
+ set
+ {
+ _generateComplementaryIons = value;
+ OnPropertyChanged(nameof(GenerateComplementaryIons));
+ }
+ }
+
+ public bool LeftSideFragmentIons
+ {
+ get => _leftSideFragmentIons;
+ set
+ {
+ _leftSideFragmentIons = value;
+ OnPropertyChanged(nameof(LeftSideFragmentIons));
+ }
+ }
+
+ public bool RightSideFragmentIons
+ {
+ get => _rightSideFragmentIons;
+ set
+ {
+ _rightSideFragmentIons = value;
+ OnPropertyChanged(nameof(RightSideFragmentIons));
+ }
+ }
+
+ public double MaxFragmentMassDa
+ {
+ get => _maxFragmentMassDa;
+ set
+ {
+ _maxFragmentMassDa = value;
+ OnPropertyChanged(nameof(MaxFragmentMassDa));
+ }
+ }
+
+ public int MinInternalIonLength
+ {
+ get => _minInternalIonLength;
+ set
+ {
+ _minInternalIonLength = value;
+ OnPropertyChanged(nameof(MinInternalIonLength));
+ }
+ }
+
+ public bool GenerateInternalIons
+ {
+ get => _useInternalIons;
+ set
+ {
+ _useInternalIons = value;
+ if (value && MinInternalIonLength <= 0)
+ {
+ // Set a default minimum length for internal ions if enabling them without a valid length
+ MinInternalIonLength = 4;
+ }
+ if (!value && MinInternalIonLength > 0)
+ {
+ // Reset minimum length if disabling internal ions
+ MinInternalIonLength = 0;
+ }
+ OnPropertyChanged(nameof(GenerateInternalIons));
+ }
+ }
+
+ public ObservableCollection AvailableMIonLosses { get; protected set; }
+
+ ///
+ /// Gets the selected M-ion losses as a list
+ ///
+ public List GetSelectedMIonLosses()
+ {
+ return AvailableMIonLosses.Where(m => m.IsSelected).Select(m => m.MIonLoss).ToList();
+ }
+
+ ///
+ /// Converts the view model to FragmentationParams object
+ ///
+ public IFragmentationParams ToFragmentationParams()
+ {
+ if (GuiGlobalParamsViewModel.Instance.IsRnaMode)
+ {
+ return new RnaFragmentationParams
+ {
+ GenerateMIon = GenerateMIon,
+ ModificationsCanSuppressBaseLossIons = ModsCanSuppressBaseLossFragments,
+ MIonLosses = GetSelectedMIonLosses()
+ };
+ }
+ else
+ {
+ return new FragmentationParams
+ {
+ GenerateMIon = GenerateMIon,
+ MIonLosses = GetSelectedMIonLosses()
+ };
+ }
+ }
+
+ ///
+ /// Reloads the M-Ion losses from the cache/file
+ ///
+ public void ReloadMIonLosses()
+ {
+ // Force reload by clearing cache
+ CustomMIonLossManager.ClearCache();
+
+ // Preserve current terminus state to avoid resetting user preferences
+ var terminus = LeftSideFragmentIons && RightSideFragmentIons ? FragmentationTerminus.Both
+ : LeftSideFragmentIons ? FragmentationTerminus.N
+ : RightSideFragmentIons ? FragmentationTerminus.C
+ : FragmentationTerminus.Both;
+
+ // Build CommonParameters with correct digeston type to avoid CommonParameters
+ // overriding the passed fragmentationParams with a protein default
+ IDigestionParams digParams = GuiGlobalParamsViewModel.Instance?.IsRnaMode == true
+ ? new RnaDigestionParams(fragmentationTerminus: terminus)
+ : new DigestionParams(fragmentationTerminus: terminus);
+
+ var commonParams = new CommonParameters(
+ digestionParams: digParams,
+ fragmentationParams: ToFragmentationParams()
+ );
+
+ // LoadAvailableMIonLosses already restores selections from FragmentationParameters.MIonLosses
+ // No need to manually restore selections here
+ AvailableMIonLosses.Clear();
+ foreach (var loss in LoadAvailableMIonLosses(commonParams))
+ AvailableMIonLosses.Add(loss);
+
+ OnPropertyChanged(nameof(AvailableMIonLosses));
+ }
+
+ private void AddAllLosses() => AvailableMIonLosses.ForEach(p => p.IsSelected = true);
+ private void RemoveAllLosses() => AvailableMIonLosses.ForEach(p => p.IsSelected = false);
+}
+
+///
+/// View model wrapper for MIonLoss to enable selection in the GUI
+///
+public class MIonLossViewModel : BaseViewModel
+{
+ public MIonLossViewModel(MIonLoss mIonLoss, bool isSelected = false, bool isCustom = false)
+ {
+ MIonLoss = mIonLoss;
+ IsSelected = isSelected;
+ IsCustom = isCustom;
+ }
+
+ public MIonLoss MIonLoss { get; }
+
+ public string Name => MIonLoss.Name;
+ public string Annotation => MIonLoss.Annotation;
+ public double MassShift => MIonLoss.MonoisotopicMass;
+ public string ChemicalFormula => MIonLoss.ThisChemicalFormula.Formula;
+ public bool IsCustom { get; }
+
+ private bool _isSelected;
+ public bool IsSelected
+ {
+ get => _isSelected;
+ set
+ {
+ _isSelected = value;
+ OnPropertyChanged(nameof(IsSelected));
+ }
+ }
+}
+
+[ExcludeFromCodeCoverage] // Design-time model for XAML designer
+public class FragmentationParamsDesignModel : FragmentationParamsViewModel
+{
+ public static FragmentationParamsDesignModel Instance => new();
+
+ public FragmentationParamsDesignModel() : base(new(), new())
+ {
+ GenerateMIon = true;
+
+ // Add some sample losses for design-time viewing
+ AvailableMIonLosses = new ObservableCollection
+ {
+ new MIonLossViewModel(new MIonLoss("Water Loss", "-H2O", Chemistry.ChemicalFormula.ParseFormula("H2O1")), true, true),
+ new MIonLossViewModel(new MIonLoss("Ammonia Loss", "-NH3", Chemistry.ChemicalFormula.ParseFormula("H3N1")), false, true),
+ new MIonLossViewModel(new MIonLoss("Phosphate Loss", "-P", Chemistry.ChemicalFormula.ParseFormula("H1P1O3")), true, false),
+ };
+ }
+}
diff --git a/MetaMorpheus/GuiFunctions/ViewModels/MetaDrawSettingsViewModel.cs b/MetaMorpheus/GuiFunctions/ViewModels/MetaDrawSettingsViewModel.cs
index 4ca71c732d..37de9ddcf6 100644
--- a/MetaMorpheus/GuiFunctions/ViewModels/MetaDrawSettingsViewModel.cs
+++ b/MetaMorpheus/GuiFunctions/ViewModels/MetaDrawSettingsViewModel.cs
@@ -80,6 +80,7 @@ public ObservableCollection CoverageColors
public ObservableCollection ChimericLegendDisplayProperties { get; } = [..Enum.GetValues()];
public ObservableCollection AmbiguityFilters { get; } = [..MetaDrawSettings.AmbiguityTypes];
public ObservableCollection GlycanLocalizationLevels { get; } = [.. Enum.GetValues()];
+ public ObservableCollection GroupingProperties { get; } = [..MetaDrawSettings.GroupingProperties];
public bool HasDefaultSaved { get { return File.Exists(SettingsPath); } }
public bool CanOpen { get { return (_LoadedIons && _LoadedPTMs && _LoadedSequenceCoverage); } }
@@ -248,6 +249,37 @@ public bool NormalizeHistogramToFile
set { MetaDrawSettings.NormalizeHistogramToFile = value; OnPropertyChanged(nameof(NormalizeHistogramToFile)); }
}
+ // These properties now forward to PlotModelStatParametersViewModel
+ public bool UseLogScaleYAxis
+ {
+ get => PlotModelStatParametersViewModel.Instance.UseLogScaleYAxis;
+ set { PlotModelStatParametersViewModel.Instance.UseLogScaleYAxis = value; OnPropertyChanged(nameof(UseLogScaleYAxis)); }
+ }
+
+ public string GroupingProperty
+ {
+ get => PlotModelStatParametersViewModel.Instance.GroupingProperty;
+ set { PlotModelStatParametersViewModel.Instance.GroupingProperty = value; OnPropertyChanged(nameof(GroupingProperty)); }
+ }
+
+ public double MinRelativeCutoff
+ {
+ get => PlotModelStatParametersViewModel.Instance.MinRelativeCutoff;
+ set { PlotModelStatParametersViewModel.Instance.MinRelativeCutoff = value; OnPropertyChanged(nameof(MinRelativeCutoff)); }
+ }
+
+ public double MaxRelativeCutoff
+ {
+ get => PlotModelStatParametersViewModel.Instance.MaxRelativeCutoff;
+ set { PlotModelStatParametersViewModel.Instance.MaxRelativeCutoff = value; OnPropertyChanged(nameof(MaxRelativeCutoff)); }
+ }
+
+ public double RelativeIntensityCutoff
+ {
+ get => PlotModelStatParametersViewModel.Instance.MinRelativeCutoff;
+ set { PlotModelStatParametersViewModel.Instance.MinRelativeCutoff = value; OnPropertyChanged(nameof(RelativeIntensityCutoff)); }
+ }
+
// BioPolymer Coverage Settings
public ObservableCollection BioPolymerCoverageColors
{
diff --git a/MetaMorpheus/MetaMorpheusSetup/Product.wxs b/MetaMorpheus/MetaMorpheusSetup/Product.wxs
index e5a3946624..90ca23627f 100644
--- a/MetaMorpheus/MetaMorpheusSetup/Product.wxs
+++ b/MetaMorpheus/MetaMorpheusSetup/Product.wxs
@@ -202,6 +202,9 @@
+
+
+
diff --git a/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationParameters.cs b/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationParameters.cs
index 7029b07da6..87aa591424 100644
--- a/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationParameters.cs
+++ b/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationParameters.cs
@@ -10,10 +10,12 @@ public CalibrationParameters()
NumFragmentsNeededForEveryIdentification = 10;
QValueCutoffForCalibratingPSMs = 0.01;
WriteIndexedMzml = true;
+ SearchType = SearchType.Classic;
}
public bool WriteIntermediateFiles { get; set; }
public bool WriteIndexedMzml { get; set; }
+ public SearchType SearchType { get; set; }
public int MinMS1IsotopicPeaksNeededForConfirmedIdentification { get; set; }
public int MinMS2IsotopicPeaksNeededForConfirmedIdentification { get; set; }
@@ -21,4 +23,4 @@ public CalibrationParameters()
public double QValueCutoffForCalibratingPSMs { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationTask.cs b/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationTask.cs
index aad3a64d6d..c8cd3de261 100644
--- a/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationTask.cs
+++ b/MetaMorpheus/TaskLayer/CalibrationTask/CalibrationTask.cs
@@ -2,12 +2,15 @@
using EngineLayer.Calibration;
using EngineLayer.ClassicSearch;
using EngineLayer.FdrAnalysis;
+using EngineLayer.Indexing;
+using EngineLayer.ModernSearch;
using EngineLayer.Util;
using MassSpectrometry;
using MzLibUtil;
using Nett;
using Omics;
using Omics.Modifications;
+using Proteomics;
using Proteomics.ProteolyticDigestion;
using Readers;
using System;
@@ -30,6 +33,7 @@ public CalibrationTask() : base(MyTask.Calibrate)
precursorMassTolerance: new PpmTolerance(InitialPrecursorTolerance)
);
+
CalibrationParameters = new CalibrationParameters();
}
@@ -45,12 +49,18 @@ public CalibrationTask() : base(MyTask.Calibrate)
public const string CalibSuffix = "-calib";
+
private List _unsuccessfullyCalibratedFilePaths;
private string _taskId;
private List _proteinList;
private List _variableModifications;
private List _fixedModifications;
private MyFileManager _myFileManager;
+ private List _dbFilenameList;
+
+ // Modern Search indexing fields
+ private List _peptideIndex;
+ private List[] _fragmentIndex;
protected override MyTaskResults RunSpecific(string outputFolder, List dbFilenameList, List currentRawFileList, string taskId, FileSpecificParameters[] fileSettingsList)
{
@@ -67,6 +77,10 @@ protected override MyTaskResults RunSpecific(string outputFolder, List { _taskId, "Individual Spectra Files", originalUncalibratedFilePath });
// carry over file-specific parameters from the uncalibrated file to the calibrated one and update combined params
- FileSpecificParameters fileSpecificParams = fileSettingsList[spectraFileIndex] == null
- ? new()
+ FileSpecificParameters fileSpecificParams = fileSettingsList[spectraFileIndex] == null
+ ? new()
: fileSettingsList[spectraFileIndex].Clone();
CommonParameters combinedParams = SetAllFileSpecificCommonParams(CommonParameters, fileSpecificParams);
-
+
// load the file
Status("Loading spectra file...", new List { _taskId, "Individual Spectra Files" });
MsDataFile myMsDataFile = _myFileManager.LoadFile(originalUncalibratedFilePath, combinedParams).LoadAllStaticData();
@@ -101,7 +115,7 @@ protected override MyTaskResults RunSpecific(string outputFolder, List { _taskId, "Individual Spectra Files", fileNameWithoutExtension });
+ Log("Searching with searchType: " + CalibrationParameters.SearchType, new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension });
Log("Searching with precursorMassTolerance: " + combinedParameters.PrecursorMassTolerance, new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension });
Log("Searching with productMassTolerance: " + combinedParameters.ProductMassTolerance, new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension });
- _ = new ClassicSearchEngine(allPsmsArray, listOfSortedms2Scans, _variableModifications, _fixedModifications, null, null, null, _proteinList, searchMode, combinedParameters,
- FileSpecificParameters, null, new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }, false).Run();
+ if (CalibrationParameters.SearchType == SearchType.Classic)
+ {
+ _ = new ClassicSearchEngine(allPsmsArray, listOfSortedms2Scans, _variableModifications, _fixedModifications, null, null, null, _proteinList, searchMode, combinedParameters,
+ FileSpecificParameters, null, new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }, false).Run();
+ }
+ else // Modern Search
+ {
+ // Regenerate indexes whenever tolerances change to keep fragment index in sync with current parameters
+ GenerateIndexes(combinedParameters, fileNameWithoutExtension);
+
+ _ = new ModernSearchEngine(allPsmsArray, listOfSortedms2Scans, _peptideIndex, _fragmentIndex, 0, combinedParameters,
+ FileSpecificParameters, searchMode, SearchParameters.DefaultMaxFragmentSize, new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension }).Run();
+ }
List allPsms = allPsmsArray.Where(b => b != null).OrderByDescending(b => b.Score)
.ThenBy(b => b.BioPolymerWithSetModsMonoisotopicMass.HasValue ? Math.Abs(b.ScanPrecursorMass - b.BioPolymerWithSetModsMonoisotopicMass.Value) : double.MaxValue)
@@ -232,6 +258,7 @@ private DataPointAquisitionResults GetDataAcquisitionResults(MsDataFile myMsData
return currentResult;
}
+
///
/// Writes prose settings and initializes the following private fields used by the calibration engine:
/// _taskId, _variableModifications, _fixedModifications, _proteinList, _myFileManager, _unsuccessfullyCalibratedFilePaths
@@ -249,14 +276,89 @@ private void Initialize(string taskId, List dbFilenameList)
var dbLoader = new DatabaseLoadingEngine(CommonParameters, this.FileSpecificParameters, [taskId], dbFilenameList, taskId, DecoyType.Reverse, true, localizeableModificationTypes);
var loadingResults = dbLoader.Run() as DatabaseLoadingEngineResults;
_proteinList = loadingResults!.BioPolymers;
-
+
_myFileManager = new MyFileManager(true);
_unsuccessfullyCalibratedFilePaths = new List();
+ _dbFilenameList = dbFilenameList;
+
+ // Reset indexes for Modern Search (will be generated on first use)
+ _peptideIndex = null;
+ _fragmentIndex = null;
// write prose settings
WriteProse(_fixedModifications, _variableModifications, _proteinList);
}
+ ///
+ /// Filters a mixed biopolymer list down to entries only, returning
+ /// the filtered list and the number of non-protein entries that were excluded.
+ /// Does not throw or warn — callers are responsible for acting on the returned counts.
+ ///
+ /// The full list of biopolymers loaded from the database.
+ ///
+ /// A tuple of the filtered list and the count of excluded non-protein entries.
+ ///
+ public static (List Proteins, int ExcludedCount) FilterBiopolymersForModernSearch(
+ List bioPolymers)
+ {
+ var proteins = bioPolymers.OfType().ToList();
+ int excludedCount = bioPolymers.Count - proteins.Count;
+ return (proteins, excludedCount);
+ }
+
+ ///
+ /// Generates peptide and fragment indexes for Modern Search.
+ /// Delegates biopolymer filtering to .
+ /// Throws if no Protein entries remain after filtering.
+ /// Warns if non-protein biopolymers were excluded.
+ ///
+ /// The combined common parameters for the current search.
+ /// The spectra file name (without extension) for status reporting.
+ /// Thrown when the database contains no Protein entries compatible with Modern Search.
+ private void GenerateIndexes(CommonParameters combinedParameters, string fileNameWithoutExtension)
+ {
+ Status("Generating indexes for Modern Search...", new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension });
+
+ var (proteinList, excludedCount) = FilterBiopolymersForModernSearch(_proteinList);
+
+ if (proteinList.Count == 0)
+ {
+ throw new MetaMorpheusException(
+ "Modern Search calibration requires protein sequences, but the loaded database contains no Protein entries " +
+ $"({excludedCount} non-protein biopolymer(s) were filtered out). " +
+ "Use Classic Search calibration for non-protein analytes, or provide a protein sequence database.");
+ }
+
+ if (excludedCount > 0)
+ {
+ Warn($"Modern Search calibration: {excludedCount} non-protein biopolymer(s) were excluded from the search index. " +
+ "Only protein sequences are supported by Modern Search.");
+ }
+
+ var indexingEngine = new IndexingEngine(
+ proteinList,
+ _variableModifications,
+ _fixedModifications,
+ null, // silacLabels
+ null, // startLabel
+ null, // endLabel
+ 0, // currentPartition (using single partition for calibration)
+ DecoyType.Reverse,
+ combinedParameters,
+ FileSpecificParameters,
+ SearchParameters.DefaultMaxFragmentSize,
+ false, // generatePrecursorIndex
+ _dbFilenameList.Select(p => new FileInfo(p.FilePath)).ToList(),
+ TargetContaminantAmbiguity.RemoveContaminant,
+ new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension });
+
+ var indexingResults = (IndexingResults)indexingEngine.Run();
+ _peptideIndex = indexingResults.PeptideIndex;
+ _fragmentIndex = indexingResults.FragmentIndex;
+
+ Status("Indexing complete.", new List { _taskId, "Individual Spectra Files", fileNameWithoutExtension });
+ }
+
public void WriteProse(List fixedModifications, List variableModifications, List bioPolymerList)
{
// write prose settings
@@ -319,7 +421,7 @@ private void WriteUncalibratedFile(string originalUncalibratedFilePath, string u
// if we didn't calibrate, write the uncalibrated file to the output folder as an mzML
File.Copy(originalUncalibratedFilePath, uncalibratedNewFullFilePath, true);
// and add it to the list of all unsuccessfully calibrated files
- unsuccessfullyCalibratedFilePaths.Add(uncalibratedNewFullFilePath);
+ unsuccessfullyCalibratedFilePaths.Add(uncalibratedNewFullFilePath);
// provide a message indicating why we couldn't calibrate
CalibrationWarnMessage(acquisitionResults);
@@ -339,7 +441,7 @@ private bool CalibrationHasValue(DataPointAquisitionResults acquisitionResultsFi
int peptidesCountFirst = acquisitionResultsFirst.Psms.Select(p => p.FullSequence).Distinct().Count();
int peptidesCountSecond = acquisitionResultsSecond.Psms.Select(p => p.FullSequence).Distinct().Count();
bool improvedCounts = psmsCountSecond > psmsCountFirst && peptidesCountSecond > peptidesCountFirst;
- if(improvedCounts)
+ if (improvedCounts)
{
return true;
}
@@ -350,11 +452,11 @@ private bool CalibrationHasValue(DataPointAquisitionResults acquisitionResultsFi
return (numPsmsIncreased && numPeptidesIncreased && psmPrecursorMedianPpmErrorDecreased && psmProductMedianPpmErrorDecreased);
}
-
+
private bool SufficientAcquisitionResults(DataPointAquisitionResults acquisitionResults)
{
- return acquisitionResults.Psms.Count >= NumRequiredPsms
- && acquisitionResults.Ms1List.Count >= NumRequiredMs1Datapoints
+ return acquisitionResults.Psms.Count >= NumRequiredPsms
+ && acquisitionResults.Ms1List.Count >= NumRequiredMs1Datapoints
&& acquisitionResults.Ms2List.Count >= NumRequiredMs2Datapoints;
}
@@ -428,4 +530,4 @@ private static void WriteNewExperimentalDesignFile(string pathToOldExperDesign,
_ = ExperimentalDesign.WriteExperimentalDesignToFile(newExperDesign);
}
}
-}
\ No newline at end of file
+}
diff --git a/MetaMorpheus/TaskLayer/FileSpecificParameters.cs b/MetaMorpheus/TaskLayer/FileSpecificParameters.cs
index a007ce1078..43a8d50bc2 100644
--- a/MetaMorpheus/TaskLayer/FileSpecificParameters.cs
+++ b/MetaMorpheus/TaskLayer/FileSpecificParameters.cs
@@ -24,6 +24,8 @@ public FileSpecificParameters(TomlTable tomlTable)
PrecursorMassTolerance = keyValuePair.Value.Get(); break;
case nameof(ProductMassTolerance):
ProductMassTolerance = keyValuePair.Value.Get(); break;
+ case nameof(ProductMassTolerance_LowRes):
+ ProductMassTolerance_LowRes = keyValuePair.Value.Get(); break;
case nameof(DigestionAgent): // Support new tomls that labeled by Digestion Agent Type instead of specific type
string valueString = keyValuePair.Value.Get();
@@ -82,6 +84,7 @@ public FileSpecificParameters()
public Tolerance PrecursorMassTolerance { get; set; }
public Tolerance ProductMassTolerance { get; set; }
+ public Tolerance ProductMassTolerance_LowRes { get; set; }
public DigestionAgent DigestionAgent { get; set; }
public int? MinPeptideLength { get; set; }
public int? MaxPeptideLength { get; set; }
diff --git a/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs b/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs
index 611fa84d8f..0e70b3f8eb 100644
--- a/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs
+++ b/MetaMorpheus/TaskLayer/MetaMorpheusTask.cs
@@ -1,4 +1,3 @@
-using Chemistry;
using EngineLayer;
using EngineLayer.Indexing;
using MassSpectrometry;
@@ -29,7 +28,7 @@
using Transcriptomics.Digestion;
using EngineLayer.Util;
using EngineLayer.DIA;
-using EngineLayer.SpectrumMatch;
+using Omics.Fragmentation;
namespace TaskLayer
{
@@ -153,6 +152,27 @@ public abstract class MetaMorpheusTask
)
)
)
+ .ConfigureType>(type => type
+ .WithConversionFor(convert => convert
+ .ToToml(custom => string.Join("\t", custom.Select(f => f.Annotation)))
+ .FromToml(tmlString => tmlString.Value
+ .Split('\t', StringSplitOptions.RemoveEmptyEntries)
+ .Select(typeName => MIonLoss.AllMIonLosses.GetValueOrDefault(typeName, null))
+ .Where(t => t != null)
+ .ToList()
+ )
+ )
+ )
+ .ConfigureType(type => type
+ .WithConversionFor(c => c
+ .FromToml(tmlTable =>
+ tmlTable.ContainsKey("ModificationsCanSuppressBaseLossIons")
+ ? tmlTable.Get()
+ : tmlTable.Get())))
+ .ConfigureType(type => type
+ .CreateInstance(() => RnaFragmentationParams.Default))
+ .ConfigureType(type => type
+ .CreateInstance(() => new()))
);
@@ -519,6 +539,7 @@ public static CommonParameters SetAllFileSpecificCommonParams(CommonParameters c
// set the rest of the file-specific parameters
Tolerance precursorMassTolerance = fileSpecificParams.PrecursorMassTolerance ?? commonParams.PrecursorMassTolerance;
Tolerance productMassTolerance = fileSpecificParams.ProductMassTolerance ?? commonParams.ProductMassTolerance;
+ Tolerance productMassTolerance_LowRes = fileSpecificParams.ProductMassTolerance_LowRes ?? commonParams.ProductMassTolerance_LowRes;
DissociationType dissociationType = fileSpecificParams.DissociationType ?? commonParams.DissociationType;
string separationType = fileSpecificParams.SeparationType ?? commonParams.SeparationType;
@@ -526,6 +547,7 @@ public static CommonParameters SetAllFileSpecificCommonParams(CommonParameters c
dissociationType: dissociationType,
precursorMassTolerance: precursorMassTolerance,
productMassTolerance: productMassTolerance,
+ productMassTolerance_LowRes: productMassTolerance_LowRes,
digestionParams: fileSpecificDigestionParams,
separationType: separationType,
@@ -559,7 +581,8 @@ public static CommonParameters SetAllFileSpecificCommonParams(CommonParameters c
addTruncations: commonParams.AddTruncations,
precursorDeconParams: commonParams.PrecursorDeconvolutionParameters,
productDeconParams: commonParams.ProductDeconvolutionParameters,
- useMostAbundantPrecursorIntensity: commonParams.UseMostAbundantPrecursorIntensity);
+ useMostAbundantPrecursorIntensity: commonParams.UseMostAbundantPrecursorIntensity,
+ fragmentationParams: commonParams.FragmentationParameters);
return returnParams;
}
diff --git a/MetaMorpheus/TaskLayer/SearchTask/SearchParameters.cs b/MetaMorpheus/TaskLayer/SearchTask/SearchParameters.cs
index 76d42eefcf..4b7d659447 100644
--- a/MetaMorpheus/TaskLayer/SearchTask/SearchParameters.cs
+++ b/MetaMorpheus/TaskLayer/SearchTask/SearchParameters.cs
@@ -7,6 +7,12 @@ namespace TaskLayer
{
public class SearchParameters
{
+ ///
+ /// Default maximum fragment size in Daltons used for indexing. This value is shared across
+ /// all task types that require fragment indexing (Search, Calibration, CrossLink, Glyco).
+ ///
+ public const double DefaultMaxFragmentSize = 30000.0;
+
public SearchParameters()
{
// default search task parameters
@@ -26,7 +32,7 @@ public SearchParameters()
WritePrunedDatabase = false;
KeepAllUniprotMods = true;
MassDiffAcceptorType = MassDiffAcceptorType.OneMM;
- MaxFragmentSize = 30000.0;
+ MaxFragmentSize = DefaultMaxFragmentSize;
MinAllowedInternalFragmentLength = 0;
WriteMzId = true;
WritePepXml = false;
diff --git a/MetaMorpheus/TaskLayer/SearchTask/SearchTask.cs b/MetaMorpheus/TaskLayer/SearchTask/SearchTask.cs
index a724e5399b..a8e34662d7 100644
--- a/MetaMorpheus/TaskLayer/SearchTask/SearchTask.cs
+++ b/MetaMorpheus/TaskLayer/SearchTask/SearchTask.cs
@@ -88,7 +88,7 @@ public static MassDiffAcceptor GetMassDiffAcceptor(Tolerance precursorMassTolera
}
}
- protected override MyTaskResults RunSpecific(string OutputFolder, List dbFilenameList, List currentRawFileList, string taskId,
+ protected override MyTaskResults RunSpecific(string OutputFolder, List dbFilenameList, List currentRawFileList, string taskId,
FileSpecificParameters[] fileSettingsList)
{
MyTaskResults = new(this);
@@ -199,7 +199,7 @@ protected override MyTaskResults RunSpecific(string OutputFolder, List { taskId } );
+ Status("Searching files...", new List { taskId });
Status("Searching files...", new List { taskId, "Individual Spectra Files" });
Dictionary numMs2SpectraPerFile = new Dictionary(); // key is filename, value is an int array of length 2, where the first element is the number of MS2 spectra in the file, and the second element is the number of different deconvoluted precursors assigned to those scans
@@ -236,7 +236,7 @@ protected override MyTaskResults RunSpecific(string OutputFolder, List s.MsnOrder ==3))
+ if (SearchParameters.DoMultiplexQuantification && myMsDataFile.Scans.Any(s => s.MsnOrder == 3))
{
// In most experiments with MS3 scans for reporter ion detection, MS2ChildScanDissociationType is LowCID.
// However, we do not set it here to allow for flexibility in dissociation type selection.
@@ -276,7 +276,7 @@ protected override MyTaskResults RunSpecific(string OutputFolder, List p.DigestionParams)],
+ ListOfDigestionParams = [.. fileSpecificCommonParams.Select(p => p.DigestionParams)],
CurrentRawFileList = currentRawFileList,
MyFileManager = myFileManager,
NumNotches = numNotches,
diff --git a/MetaMorpheus/TaskLayer/TaskLayer.csproj b/MetaMorpheus/TaskLayer/TaskLayer.csproj
index 013ae6bcfc..27034a6bb1 100644
--- a/MetaMorpheus/TaskLayer/TaskLayer.csproj
+++ b/MetaMorpheus/TaskLayer/TaskLayer.csproj
@@ -21,7 +21,7 @@
-
+
diff --git a/MetaMorpheus/Test/CalibrationTests.cs b/MetaMorpheus/Test/CalibrationTests.cs
index 61a35c152f..b4e0f06c31 100644
--- a/MetaMorpheus/Test/CalibrationTests.cs
+++ b/MetaMorpheus/Test/CalibrationTests.cs
@@ -1,17 +1,21 @@
using EngineLayer;
+using EngineLayer.DatabaseLoading;
using FlashLFQ;
using MassSpectrometry;
+using MzLibUtil;
using NUnit.Framework;
+using Omics;
+using Omics.Modifications;
+using Proteomics;
+using System;
+using System.Reflection;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using TaskLayer;
-using System;
-using EngineLayer.DatabaseLoading;
-using System.Diagnostics;
-using MzLibUtil;
-using Proteomics;
+using Transcriptomics;
namespace Test
@@ -49,7 +53,7 @@ public static void ExperimentalDesignCalibrationTest(string nonCalibratedFile)
string expectedCalibratedFileName = Path.GetFileNameWithoutExtension(nonCalibratedFilePath) + "-calib.mzML";
var expectedCalibratedFilePath = Path.Combine(outputFolder, expectedCalibratedFileName);
var newExperDesign = ExperimentalDesign.ReadExperimentalDesign(newExpDesignPath, new List { expectedCalibratedFilePath }, out var errors);
-
+
Assert.That(!errors.Any());
Assert.That(newExperDesign.Count == 1);
@@ -75,15 +79,17 @@ public static void ExperimentalDesignCalibrationTest(string nonCalibratedFile)
}
[Test]
- public static void TestToleranceExpansion()
+ [TestCase(SearchType.Classic)]
+ [TestCase(SearchType.Modern)]
+ public static void TestToleranceExpansion(SearchType searchType)
{
// capture warnings
- var originalOut = Console.Out;
- var originalErr = Console.Error;
- var sw = new StringWriter();
- var listener = new TextWriterTraceListener(sw) { TraceOutputOptions = TraceOptions.None };
- Trace.Listeners.Add(listener);
- Console.SetOut(sw);
+ var originalOut = Console.Out;
+ var originalErr = Console.Error;
+ var sw = new StringWriter();
+ var listener = new TextWriterTraceListener(sw) { TraceOutputOptions = TraceOptions.None };
+ Trace.Listeners.Add(listener);
+ Console.SetOut(sw);
Console.SetError(sw);
try
@@ -103,6 +109,7 @@ public static void TestToleranceExpansion()
// run calibration
CalibrationTask calibrationTask = new();
+ calibrationTask.CalibrationParameters.SearchType = searchType;
calibrationTask.CommonParameters.PrecursorMassTolerance = new PpmTolerance(2);
calibrationTask.CommonParameters.ProductMassTolerance = new PpmTolerance(2);
calibrationTask.RunTask(outputFolder, new List { new DbForTask(myDatabase, false) }, new List { nonCalibratedFilePath }, "test");
@@ -149,7 +156,9 @@ public static void TestToleranceExpansion()
}
[Test]
- public static void CalibrationTestNoPsms()
+ [TestCase(SearchType.Classic)]
+ [TestCase(SearchType.Modern)]
+ public static void CalibrationTestNoPsms(SearchType searchType)
{
// set up directories
string unitTestFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, @"ExperimentalDesignCalibrationTest");
@@ -170,6 +179,7 @@ public static void CalibrationTestNoPsms()
// run calibration
CalibrationTask calibrationTask = new();
+ calibrationTask.CalibrationParameters.SearchType = searchType;
calibrationTask.RunTask(outputFolder, new List { new DbForTask(myDatabase, false) }, new List { nonCalibratedFilePath }, "test");
// test new experimental design written by calibration
@@ -232,9 +242,12 @@ private static void CalibrationWarnHandler(object sender, StringEventArgs e, ref
}
[Test]
- public static void CalibrationTestLowRes()
+ [TestCase(SearchType.Classic)]
+ [TestCase(SearchType.Modern)]
+ public static void CalibrationTestLowRes(SearchType searchType)
{
CalibrationTask calibrationTask = new CalibrationTask();
+ calibrationTask.CalibrationParameters.SearchType = searchType;
string outputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestCalibrationLow");
string myFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\TaGe_SA_A549_3_snip.mzML");
@@ -309,8 +322,8 @@ public static void ExperimentalDesignCalibrationAndSearch(string nonCalibratedFi
// run the tasks
EverythingRunnerEngine a = new EverythingRunnerEngine(
- new List<(string, MetaMorpheusTask)> { ("", calibrationTask), ("", searchTask) },
- new List { nonCalibratedFilePath },
+ new List<(string, MetaMorpheusTask)> { ("", calibrationTask), ("", searchTask) },
+ new List { nonCalibratedFilePath },
new List { new DbForTask(myDatabase, false) },
outputFolder);
@@ -368,7 +381,7 @@ public static void ExperimentalDesignCalibrationAndSearchWithOneCalibratibleAndO
}
[Test]
- [TestCase("ExpDesFileNotFound","small.mzML", "Experimental design file not found!")]
+ [TestCase("ExpDesFileNotFound", "small.mzML", "Experimental design file not found!")]
[TestCase("WrongNumberOfCells", "small.mzML", "Error: The experimental design was not formatted correctly. Expected 5 cells, but found 4 on line 2")]
[TestCase("BioRepNotInteger", "small.mzML", "Error: The experimental design was not formatted correctly. The biorep on line 2 is not an integer")]
[TestCase("FractionNotInteger", "small.mzML", "Error: The experimental design was not formatted correctly. The fraction on line 2 is not an integer")]
@@ -392,7 +405,7 @@ public static void TestWriteNewExperimentalDesignFileDuringCalibration()
string outputFolder = Path.Combine(unitTestFolder, @"TaskOutput");
Directory.CreateDirectory(unitTestFolder);
Directory.CreateDirectory(outputFolder);
-
+
// set up original spectra file (input to calibration)
string nonCalibratedFilePath = Path.Combine(unitTestFolder, "filename1.mzML");
File.Copy(Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\SmallCalibratible_Yeast.mzML"), nonCalibratedFilePath, true);
@@ -400,7 +413,7 @@ public static void TestWriteNewExperimentalDesignFileDuringCalibration()
// set up original BAD experimental design (input to calibration)
string experimentalDesignPath = Path.Combine(unitTestFolder, "ExperimentalDesign.tsv");
File.Copy(badExperimentalDesignPath, experimentalDesignPath, true);
-
+
// protein db
string myDatabase = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\smalldb.fasta");
@@ -410,16 +423,302 @@ public static void TestWriteNewExperimentalDesignFileDuringCalibration()
bool wasCalled = false;
MetaMorpheusTask.WarnHandler += (o, e) => wasCalled = true;
-
+
calibrationTask.RunTask(outputFolder, new List { new DbForTask(myDatabase, false) }, new List { nonCalibratedFilePath }, "test");
-
+
//The original experimental design file is bad so we expect Warn event in "WriteNewExperimentalDesignFile"
Assert.That(wasCalled);
-
+
+
+ // clean up
+ Directory.Delete(unitTestFolder, true);
+ }
+
+ ///
+ /// Tests that calibration using Modern Search produces the expected output files.
+ /// This is critical because Modern Search uses an indexed database approach (IndexingEngine + ModernSearchEngine)
+ /// rather than the ClassicSearchEngine. We need to verify that the calibration workflow works correctly
+ /// when using this alternative search strategy.
+ ///
+ [Test]
+ public static void CalibrationWithModernSearchTest()
+ {
+ // set up directories
+ string unitTestFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, @"CalibrationModernSearchTest");
+ string outputFolder = Path.Combine(unitTestFolder, @"TaskOutput");
+ Directory.CreateDirectory(unitTestFolder);
+ Directory.CreateDirectory(outputFolder);
+
+ // set up original spectra file (input to calibration)
+ string nonCalibratedFilePath = Path.Combine(unitTestFolder, "testfile.mzML");
+ File.Copy(Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\SmallCalibratible_Yeast.mzML"), nonCalibratedFilePath, true);
+
+ // protein db
+ string myDatabase = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\smalldb.fasta");
+
+ // run calibration with Modern Search
+ CalibrationTask calibrationTask = new();
+ calibrationTask.CalibrationParameters.SearchType = SearchType.Modern;
+ calibrationTask.RunTask(outputFolder, new List { new DbForTask(myDatabase, false) }, new List { nonCalibratedFilePath }, "test");
+
+ // test file-specific toml written by calibration w/ suggested ppm tolerances
+ string expectedTomlName = Path.GetFileNameWithoutExtension(nonCalibratedFilePath) + "-calib.toml";
+ string expectedCalibratedFileName = Path.GetFileNameWithoutExtension(nonCalibratedFilePath) + "-calib.mzML";
+ var expectedCalibratedFilePath = Path.Combine(outputFolder, expectedCalibratedFileName);
+
+ Assert.That(File.Exists(Path.Combine(outputFolder, expectedTomlName)), "Calibration toml file should exist");
+ Assert.That(File.Exists(expectedCalibratedFilePath), "Calibrated mzML file should exist");
+
+ var lines = File.ReadAllLines(Path.Combine(outputFolder, expectedTomlName));
+ var tolerance = Regex.Match(lines[0], @"\d+\.\d*").Value;
+ var tolerance1 = Regex.Match(lines[1], @"\d+\.\d*").Value;
+
+ Assert.That(double.TryParse(tolerance, out double tol), "Precursor tolerance should be parseable");
+ Assert.That(double.TryParse(tolerance1, out double tol1), "Product tolerance should be parseable");
+ Assert.That(lines[0].Contains("PrecursorMassTolerance"), "First line should contain PrecursorMassTolerance");
+ Assert.That(lines[1].Contains("ProductMassTolerance"), "Second line should contain ProductMassTolerance");
+
+ // clean up
+ Directory.Delete(unitTestFolder, true);
+ }
+
+ ///
+ /// Compares calibration results between Classic Search and Modern Search to ensure they produce
+ /// comparable results. This is critical to verify that the Modern Search implementation doesn't
+ /// introduce significant differences in calibration quality. Both search types should find
+ /// similar numbers of PSMs and produce similar mass tolerance recommendations.
+ ///
+ [Test]
+ public static void CalibrationClassicVsModernSearchComparison()
+ {
+ // set up directories
+ string unitTestFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, @"CalibrationClassicVsModernTest");
+ string classicOutputFolder = Path.Combine(unitTestFolder, @"ClassicOutput");
+ string modernOutputFolder = Path.Combine(unitTestFolder, @"ModernOutput");
+ Directory.CreateDirectory(unitTestFolder);
+ Directory.CreateDirectory(classicOutputFolder);
+ Directory.CreateDirectory(modernOutputFolder);
+
+ // set up original spectra files (need separate copies to avoid file locking issues)
+ string classicFilePath = Path.Combine(unitTestFolder, "classic_test.mzML");
+ string modernFilePath = Path.Combine(unitTestFolder, "modern_test.mzML");
+ File.Copy(Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\SmallCalibratible_Yeast.mzML"), classicFilePath, true);
+ File.Copy(Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\SmallCalibratible_Yeast.mzML"), modernFilePath, true);
+
+ // protein db
+ string myDatabase = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\smalldb.fasta");
+
+ // run calibration with Classic Search
+ CalibrationTask classicCalibrationTask = new();
+ classicCalibrationTask.CalibrationParameters.SearchType = SearchType.Classic;
+ classicCalibrationTask.RunTask(classicOutputFolder, new List { new DbForTask(myDatabase, false) }, new List { classicFilePath }, "classic");
+
+ // run calibration with Modern Search
+ CalibrationTask modernCalibrationTask = new();
+ modernCalibrationTask.CalibrationParameters.SearchType = SearchType.Modern;
+ modernCalibrationTask.RunTask(modernOutputFolder, new List { new DbForTask(myDatabase, false) }, new List { modernFilePath }, "modern");
+
+ // verify both produced output files
+ string classicTomlPath = Path.Combine(classicOutputFolder, "classic_test-calib.toml");
+ string modernTomlPath = Path.Combine(modernOutputFolder, "modern_test-calib.toml");
+
+ Assert.That(File.Exists(classicTomlPath), "Classic calibration should produce toml file");
+ Assert.That(File.Exists(modernTomlPath), "Modern calibration should produce toml file");
+
+ // read tolerances from both
+ var classicLines = File.ReadAllLines(classicTomlPath);
+ var modernLines = File.ReadAllLines(modernTomlPath);
+
+ Assert.That(double.TryParse(Regex.Match(classicLines[0], @"\d+\.\d*").Value, out double classicPrecursorTol),
+ "Classic precursor tolerance should be parseable");
+ Assert.That(double.TryParse(Regex.Match(classicLines[1], @"\d+\.\d*").Value, out double classicProductTol),
+ "Classic product tolerance should be parseable");
+ Assert.That(double.TryParse(Regex.Match(modernLines[0], @"\d+\.\d*").Value, out double modernPrecursorTol),
+ "Modern precursor tolerance should be parseable");
+ Assert.That(double.TryParse(Regex.Match(modernLines[1], @"\d+\.\d*").Value, out double modernProductTol),
+ "Modern product tolerance should be parseable");
+
+ // tolerances should be very close — both engines search the same data
+ // and feed into the same calibration math
+ Assert.That(modernPrecursorTol, Is.InRange(classicPrecursorTol * 0.9, classicPrecursorTol * 1.1),
+ $"Modern precursor tolerance ({modernPrecursorTol}) should be within 10% of Classic ({classicPrecursorTol})");
+ Assert.That(modernProductTol, Is.InRange(classicProductTol * 0.9, classicProductTol * 1.1),
+ $"Modern product tolerance ({modernProductTol}) should be within 10% of Classic ({classicProductTol})");
// clean up
Directory.Delete(unitTestFolder, true);
}
+ ///
+ /// Verifies that returns an empty
+ /// protein list and the correct excluded count when the input contains only non-Protein biopolymers.
+ /// GenerateIndexes uses this result to throw an informative MetaMorpheusException directing
+ /// users to Classic Search for non-protein analytes.
+ ///
+ [Test]
+ public static void FilterBiopolymersForModernSearch_AllNonProtein_ReturnsEmptyWithCorrectCount()
+ {
+ var rnaList = new List
+ {
+ new RNA("GUACUG", "rna_1"),
+ new RNA("AACUGCUAG", "rna_2")
+ };
+
+ var (proteins, excludedCount) = CalibrationTask.FilterBiopolymersForModernSearch(rnaList);
+
+ Assert.That(proteins, Is.Empty,
+ "No proteins should be returned when the input contains only non-protein biopolymers");
+ Assert.That(excludedCount, Is.EqualTo(2),
+ "Excluded count should equal the total number of non-protein entries");
+ }
+
+ ///
+ /// Verifies that correctly separates
+ /// a mixed biopolymer list, keeping only Protein entries and reporting the right excluded count.
+ /// GenerateIndexes uses this result to emit a warning when non-protein entries are dropped.
+ ///
+ [Test]
+ public static void FilterBiopolymersForModernSearch_MixedList_RetainsProteinsAndCountsExcluded()
+ {
+ var protein = new Protein("MKWVTFISLLLLFSSAYSR", "P00001");
+ var rna = new RNA("GUACUG", "rna_1");
+ var mixedList = new List { protein, rna };
+
+ var (proteins, excludedCount) = CalibrationTask.FilterBiopolymersForModernSearch(mixedList);
+
+ Assert.That(proteins.Count, Is.EqualTo(1),
+ "Only the Protein entry should be retained");
+ Assert.That(proteins[0].Accession, Is.EqualTo("P00001"),
+ "The retained protein should be the one from the input list");
+ Assert.That(excludedCount, Is.EqualTo(1),
+ "Excluded count should equal the number of non-protein entries");
+ }
+
+ ///
+ /// Verifies that passes through
+ /// an all-protein list with zero excluded entries.
+ ///
+ [Test]
+ public static void FilterBiopolymersForModernSearch_AllProteins_ReturnsAllWithZeroExcluded()
+ {
+ var proteins = new List
+ {
+ new Protein("MKWVTFISLLLLFSSAYSR", "P00001"),
+ new Protein("ACDEFGHIKLMNPQRSTVWY", "P00002")
+ };
+
+ var (result, excludedCount) = CalibrationTask.FilterBiopolymersForModernSearch(proteins);
+
+ Assert.That(result.Count, Is.EqualTo(2),
+ "All proteins should be retained when input contains only Protein entries");
+ Assert.That(excludedCount, Is.EqualTo(0),
+ "No entries should be excluded when input contains only Protein entries");
+ }
+
+ ///
+ /// Verifies that throws a
+ /// when the loaded database contains only non-Protein biopolymers,
+ /// and that the message directs the user to Classic Search or a protein database.
+ /// The exception is thrown before any indexing work begins, so no spectra files are needed.
+ ///
+ [Test]
+ public static void GenerateIndexes_AllNonProteinDatabase_ThrowsMetaMorpheusException()
+ {
+ var calibrationTask = new CalibrationTask();
+ var type = typeof(CalibrationTask);
+
+ type.GetField("_proteinList", BindingFlags.NonPublic | BindingFlags.Instance)
+ .SetValue(calibrationTask, new List
+ {
+ new RNA("GUACUG", "rna_1"),
+ new RNA("AACUGCUAG", "rna_2")
+ });
+ type.GetField("_taskId", BindingFlags.NonPublic | BindingFlags.Instance)
+ .SetValue(calibrationTask, "testTask");
+
+ var generateIndexes = type.GetMethod("GenerateIndexes",
+ BindingFlags.NonPublic | BindingFlags.Instance,
+ null,
+ new[] { typeof(CommonParameters), typeof(string) },
+ null);
+
+ MetaMorpheusException caughtException = null;
+ try
+ {
+ generateIndexes.Invoke(calibrationTask, new object[] { new CommonParameters(), "testFile" });
+ }
+ catch (TargetInvocationException ex)
+ {
+ caughtException = ex.InnerException as MetaMorpheusException;
+ }
+
+ Assert.That(caughtException, Is.Not.Null,
+ "A MetaMorpheusException should be thrown when the database contains no Protein entries");
+ Assert.That(caughtException.Message, Does.Contain("Modern Search calibration requires protein sequences"),
+ "Exception message should describe the requirement for protein sequences");
+ Assert.That(caughtException.Message, Does.Contain("2 non-protein biopolymer(s) were filtered out"),
+ "Exception message should report the number of excluded non-protein biopolymers");
+ }
+
+ ///
+ /// Verifies that emits a
+ /// warning via when the loaded database contains a mix
+ /// of Protein and non-Protein biopolymers, informing the user that only protein sequences are indexed.
+ /// The warning is emitted before the runs, so any failure in the
+ /// subsequent indexing step does not affect whether the warning was captured.
+ ///
+ [Test]
+ public static void GenerateIndexes_MixedBiopolymers_EmitsWarnForExcludedNonProteinEntries()
+ {
+ var calibrationTask = new CalibrationTask();
+ var type = typeof(CalibrationTask);
+
+ type.GetField("_proteinList", BindingFlags.NonPublic | BindingFlags.Instance)
+ .SetValue(calibrationTask, new List
+ {
+ new Protein("MKWVTFISLLLLFSSAYSR", "P00001"),
+ new RNA("GUACUG", "rna_1")
+ });
+ type.GetField("_taskId", BindingFlags.NonPublic | BindingFlags.Instance)
+ .SetValue(calibrationTask, "testTask");
+ type.GetField("_variableModifications", BindingFlags.NonPublic | BindingFlags.Instance)
+ .SetValue(calibrationTask, new List());
+ type.GetField("_fixedModifications", BindingFlags.NonPublic | BindingFlags.Instance)
+ .SetValue(calibrationTask, new List());
+
+ string dbPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\smalldb.fasta");
+ type.GetField("_dbFilenameList", BindingFlags.NonPublic | BindingFlags.Instance)
+ .SetValue(calibrationTask, new List { new DbForTask(dbPath, false) });
+
+ var warnings = new List();
+ EventHandler warnHandler = (o, e) => warnings.Add(e.S);
+ MetaMorpheusTask.WarnHandler += warnHandler;
+
+ var generateIndexes = type.GetMethod("GenerateIndexes",
+ BindingFlags.NonPublic | BindingFlags.Instance,
+ null,
+ new[] { typeof(CommonParameters), typeof(string) },
+ null);
+
+ try
+ {
+ generateIndexes.Invoke(calibrationTask, new object[] { new CommonParameters(), "testFile" });
+ }
+ catch (TargetInvocationException)
+ {
+ // The warning is emitted before the IndexingEngine runs; any subsequent
+ // failure does not affect whether the warning was captured.
+ }
+ finally
+ {
+ MetaMorpheusTask.WarnHandler -= warnHandler;
+ }
+
+ Assert.That(warnings.Any(w => w.Contains("1 non-protein biopolymer(s) were excluded from the search index")),
+ "A warning should be emitted when non-protein entries are excluded from the search index");
+ Assert.That(warnings.Any(w => w.Contains("Only protein sequences are supported by Modern Search")),
+ "Warning should indicate that Modern Search only supports protein sequences");
+ }
+
}
-}
\ No newline at end of file
+}
diff --git a/MetaMorpheus/Test/GlycoSearchEngineTest.cs b/MetaMorpheus/Test/GlycoSearchEngineTest.cs
index cdcc9a73b2..27ff57f8f4 100644
--- a/MetaMorpheus/Test/GlycoSearchEngineTest.cs
+++ b/MetaMorpheus/Test/GlycoSearchEngineTest.cs
@@ -1,7 +1,11 @@
-using EngineLayer;
+using Chemistry;
+using Easy.Common.Extensions;
+using EngineLayer;
+using EngineLayer.DatabaseLoading;
using EngineLayer.GlycoSearch;
using MassSpectrometry;
using MzLibUtil;
+using Nett;
using NUnit.Framework;
using Omics.Modifications;
using Org.BouncyCastle.Asn1.Cmp;
@@ -76,5 +80,280 @@ public void CreateGsm_WithWideProductTolerance_ScanInfo_p_IsCappedToOne()
Assert.That(pEstimate, Is.GreaterThan(1d), "Test must drive p > 1 to validate the cap.");
Assert.That(result.ScanInfo_p, Is.EqualTo(1), "P value should be capped at 1 when product mass tolerance is very wide.");
}
+
+ [Test]
+ public static void TestLowResToleranceConstruction_Default()
+ {
+ var commonParameters = new CommonParameters();
+ Assert.That(commonParameters.ProductMassTolerance_LowRes, Is.Not.Null, "ChildScanMassTolerance should be initialized by default.");
+
+ // Default low-res tolerance inherits from ProductMassTolerance (PpmTolerance 20 ppm)
+ Assert.That(commonParameters.ProductMassTolerance_LowRes, Is.TypeOf());
+ Assert.That(commonParameters.ProductMassTolerance_LowRes.Value, Is.EqualTo(commonParameters.ProductMassTolerance.Value));
+ }
+
+ [Test]
+ public static void TestLowResToleranceConstruction_Default2()
+ {
+ // In this toml settng, we omit the productMassTolerance_LowRes. The test confirms that the default value is 0.35 Da.
+ string outputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "TESTGlycoData");
+ Directory.CreateDirectory(outputFolder);
+
+ try
+ {
+ var inputTask = Toml.ReadFile(
+ Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\GlycoSearchTaskconfigOGlycoTest_ToleranceTracking.toml"),
+ MetaMorpheusTask.tomlConfig);
+
+ // Confirm read-in behavior: ProductMassTolerance is set to 40 ppm from TOML.
+ // Low-res inherits from the default ProductMassTolerance (20 ppm) at construction time,
+ // since TOML deserialization sets ProductMassTolerance via the public setter after construction.
+ Assert.That(inputTask.CommonParameters.ProductMassTolerance, Is.TypeOf());
+ Assert.That(inputTask.CommonParameters.ProductMassTolerance.Value, Is.EqualTo(40).Within(1e-9));
+ Assert.That(inputTask.CommonParameters.ProductMassTolerance_LowRes, Is.TypeOf());
+ Assert.That(inputTask.CommonParameters.ProductMassTolerance_LowRes.Value, Is.EqualTo(20).Within(1e-9));
+
+ DbForTask db = new(Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\P16150.fasta"), false);
+ string spectraFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\2019_09_16_StcEmix_35trig_EThcD25_rep1_9906.mgf");
+
+ new EverythingRunnerEngine(
+ new List<(string, MetaMorpheusTask)> { ("Task", inputTask) },
+ new List { spectraFile },
+ new List { db },
+ outputFolder).Run();
+
+ // Confirm written TOML exists
+ string writtenTaskToml = Path.Combine(outputFolder, "Task Settings", "Taskconfig.toml");
+ Assert.That(File.Exists(writtenTaskToml), Is.True, "Expected task config TOML was not written.");
+
+ // Confirm written TOML preserves tracked values
+ var writtenTask = Toml.ReadFile(writtenTaskToml, MetaMorpheusTask.tomlConfig);
+ Assert.That(writtenTask.CommonParameters.ProductMassTolerance, Is.TypeOf());
+ Assert.That(writtenTask.CommonParameters.ProductMassTolerance.Value, Is.EqualTo(40).Within(1e-9));
+ Assert.That(writtenTask.CommonParameters.ProductMassTolerance_LowRes, Is.TypeOf());
+ Assert.That(writtenTask.CommonParameters.ProductMassTolerance_LowRes.Value, Is.EqualTo(20).Within(1e-9));
+ }
+ finally
+ {
+ if (Directory.Exists(outputFolder))
+ {
+ Directory.Delete(outputFolder, true);
+ }
+ }
+ }
+
+ [Test]
+ public static void TestLowResTolerance_Da()
+ {
+ // Purpose:
+ // - Validate that fragment matching use `ChildScanMassTolerance` for child scans (isChildScan flag).
+ // - Confirm initial tolerances are set as expected: product, child, precursor.
+ // - For matched fragment ions, verify the observed m/z (converted to mass) is within the correct tolerance.
+
+ // --- initialize tolerances for test
+ var lowResTolerance = new AbsoluteTolerance(0.5);
+ var highResTolerance = new PpmTolerance(20);
+ // Create CommonParameters with explicit tolerances
+ var commonParameters = new CommonParameters(
+ productMassTolerance: highResTolerance,
+ productMassTolerance_LowRes: lowResTolerance,
+ dissociationType: DissociationType.ETD,
+ trimMsMsPeaks: false);
+ Assert.That(commonParameters.ProductMassTolerance_LowRes.GetRange(1000).Width, Is.EqualTo(lowResTolerance.GetRange(1000).Width));
+ Assert.That(commonParameters.ProductMassTolerance.GetRange(1000).Width, Is.EqualTo(highResTolerance.GetRange(1000).Width));
+
+ // Sanity-check the parameter values were set
+ string spectraFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\2019_09_16_StcEmix_35trig_EThcD25_rep1_4565.mgf");
+ var file = new MyFileManager(true).LoadFile(spectraFile, commonParameters);
+ var scan = MetaMorpheusTask.GetMs2Scans(file, spectraFile, commonParameters).First();
+
+ // Load a single MS2 scan from test data
+ var peptide = new PeptideWithSetModifications("TTGSLEPSSGASGPQVSSVK");
+ var glycoPeptide = new PeptideWithSetModifications("T[O-linked glycosylation:H1N1 on T]T[O-linked glycosylation:H1N1 on T]GSLEPSS[O-linked glycosylation:N1 on S]GASGPQVSSVK", GlobalVariables.AllModsKnownDictionary);
+ var fragmentsForEachGlycoPeptide = GlycoPeptides.OGlyGetTheoreticalFragments(commonParameters.DissociationType, null, peptide, glycoPeptide);
+
+ // Match fragments treating the scan as a child scan (isChildScan = true).
+ // Expect matches to satisfy child scan tolerance (potentially wider).
+ var childMatchedIons = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters);
+ Assert.That(childMatchedIons.Count, Is.GreaterThan(0));
+ foreach (var ion in childMatchedIons)
+ {
+ double matchedMass = ion.Mz.ToMass(ion.Charge);
+ double theoreticalMass = ion.NeutralTheoreticalProduct.NeutralMass;
+ // Verify matched ion is within child-scan tolerance
+ Assert.That(commonParameters.ProductMassTolerance_LowRes.Within(matchedMass, theoreticalMass));
+ Assert.That(lowResTolerance.Within(matchedMass, theoreticalMass));
+ }
+
+ var childMatchedIonsHighRes = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters);
+ Assert.That(childMatchedIonsHighRes.Count, Is.GreaterThan(0));
+ foreach (var ion in childMatchedIonsHighRes)
+ {
+ double matchedMass = ion.Mz.ToMass(ion.Charge);
+ double theoreticalMass = ion.NeutralTheoreticalProduct.NeutralMass;
+ // Verify matched ion is within high-res tolerance
+ Assert.That(commonParameters.ProductMassTolerance.Within(matchedMass, theoreticalMass));
+ Assert.That(highResTolerance.Within(matchedMass, theoreticalMass));
+ }
+ // With a wider tolerance, we expect to match the same or more ions than with a narrower tolerance.
+ Assert.That(childMatchedIons.Count >= childMatchedIonsHighRes.Count);
+ }
+
+ [Test]
+ public static void TestLowResTolerance_ppm()
+ {
+ // Purpose:
+ // - Validate that fragment matching use `ChildScanMassTolerance` for child scans (isChildScan flag).
+ // - Confirm initial tolerances are set as expected: product, child, precursor.
+ // - For matched fragment ions, verify the observed m/z (converted to mass) is within the correct tolerance.
+
+ // --- initialize tolerances for test
+ var lowResTolerance = new PpmTolerance(200);
+ var highResTolerance = new PpmTolerance(20);
+
+ // Create CommonParameters with explicit tolerances
+ var commonParameters = new CommonParameters(
+ productMassTolerance: highResTolerance,
+ productMassTolerance_LowRes: lowResTolerance,
+ dissociationType: DissociationType.ETD,
+ trimMsMsPeaks: false);
+ Assert.That(commonParameters.ProductMassTolerance_LowRes.GetRange(1000).Width, Is.EqualTo(lowResTolerance.GetRange(1000).Width));
+
+ // Sanity-check the parameter values were set
+ string spectraFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"GlycoTestData\2019_09_16_StcEmix_35trig_EThcD25_rep1_4565.mgf");
+ var file = new MyFileManager(true).LoadFile(spectraFile, commonParameters);
+ var scan = MetaMorpheusTask.GetMs2Scans(file, spectraFile, commonParameters).First();
+
+ // Load a single MS2 scan from test data
+ var peptide = new PeptideWithSetModifications("TTGSLEPSSGASGPQVSSVK");
+ var glycoPeptide = new PeptideWithSetModifications("T[O-linked glycosylation:H1N1 on T]T[O-linked glycosylation:H1N1 on T]GSLEPSS[O-linked glycosylation:N1 on S]GASGPQVSSVK", GlobalVariables.AllModsKnownDictionary);
+ var fragmentsForEachGlycoPeptide = GlycoPeptides.OGlyGetTheoreticalFragments(commonParameters.DissociationType, null, peptide, glycoPeptide);
+
+ // Match fragments treating the scan as a child scan (isChildScan = true).
+ // Expect matches to satisfy child scan tolerance (potentially wider).
+ var childMatchedIons = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters);
+ Assert.That(childMatchedIons.Count, Is.GreaterThan(0));
+ foreach (var ion in childMatchedIons)
+ {
+ double matchedMass = ion.Mz.ToMass(ion.Charge);
+ double theoreticalMass = ion.NeutralTheoreticalProduct.NeutralMass;
+ // Verify matched ion is within child-scan tolerance
+ Assert.That(commonParameters.ProductMassTolerance_LowRes.Within(matchedMass, theoreticalMass));
+ Assert.That(lowResTolerance.Within(matchedMass, theoreticalMass));
+ }
+
+ // Match fragments with narrow tolerance, and the expected matches should be satisfied high-res tolerance.
+ var childMatchedIonsHighRes = MetaMorpheusEngine.MatchFragmentIons(scan, fragmentsForEachGlycoPeptide, commonParameters);
+ Assert.That(childMatchedIonsHighRes.Count, Is.GreaterThan(0));
+ foreach (var ion in childMatchedIonsHighRes)
+ {
+ double matchedMass = ion.Mz.ToMass(ion.Charge);
+ double theoreticalMass = ion.NeutralTheoreticalProduct.NeutralMass;
+ // Verify matched ion is within child-scan tolerance
+ Assert.That(commonParameters.ProductMassTolerance.Within(matchedMass, theoreticalMass));
+ Assert.That(highResTolerance.Within(matchedMass, theoreticalMass));
+ }
+
+ // With a wider tolerance, we expect to match the same or more ions than with a narrower tolerance.
+ Assert.That(childMatchedIons.Count >= childMatchedIonsHighRes.Count);
+ }
+
+
+ [Test]
+ public static void LowRes_CalibrateAndSearch()
+ {
+ // This test verifies that calibration-updated precursor/product tolerances are propagated to the subsequent GlycoSearch via the calibration-generated file-specific TOML, and confirms the GlycoSearch task config TOML is written with expected settings.
+ string outputRoot = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestCalibThenGlyco");
+ Directory.CreateDirectory(outputRoot);
+
+ try
+ {
+ // GlycoSearchEngine expects these DB names to exist in global path lists.
+ string oglycanPath = "OGlycan.gdb";
+ string nglycanPath = "NGlycan_ForNoSearch.gdb";
+ if (!GlobalVariables.OGlycanDatabasePaths.Contains(oglycanPath))
+ {
+ GlobalVariables.OGlycanDatabasePaths.Add(oglycanPath);
+ }
+
+ if (!GlobalVariables.NGlycanDatabasePaths.Contains(nglycanPath))
+ {
+ GlobalVariables.NGlycanDatabasePaths.Add(nglycanPath);
+ }
+
+ // Use a calibratable mzML for calibration.
+ string rawFile = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\SmallCalibratible_Yeast.mzML");
+ string database = Path.Combine(TestContext.CurrentContext.TestDirectory, @"TestData\smalldb.fasta");
+
+ Tolerance originalPrecursorTolerance = new PpmTolerance(5);
+ Tolerance originalProductTolerance = new PpmTolerance(20);
+
+ // 1) Calibration task
+ var calibrationTask = new CalibrationTask();
+ calibrationTask.CommonParameters = new CommonParameters(
+ precursorMassTolerance: originalPrecursorTolerance,
+ productMassTolerance: originalProductTolerance);
+
+ // 2) Glyco task (will consume calibrated file-specific toml generated by calibration)
+ var glycoTask = new GlycoSearchTask();
+ glycoTask.CommonParameters = new CommonParameters(
+ precursorMassTolerance: originalPrecursorTolerance,
+ productMassTolerance: originalProductTolerance);
+
+ var runner = new EverythingRunnerEngine(
+ new List<(string, MetaMorpheusTask)>
+ {
+ ("Calibration", calibrationTask),
+ ("Glyco", glycoTask)
+ },
+ new List { rawFile },
+ new List { new DbForTask(database, false) },
+ outputRoot);
+
+ runner.Run();
+
+ // Calibration writes calibrated mzML and a file-specific toml next to it.
+ string calibratedTomlPath = Path.Combine(outputRoot, "Calibration", "SmallCalibratible_Yeast-calib.toml");
+ Assert.That(File.Exists(calibratedTomlPath), Is.True, "Calibration file-specific toml was not written.");
+
+ // Parse calibrated tolerances from the calibration-generated file-specific toml.
+ TomlTable calibratedTable = Toml.ReadFile(calibratedTomlPath, MetaMorpheusTask.tomlConfig);
+ var calibratedFileSpecific = new FileSpecificParameters(calibratedTable);
+ Assert.That(calibratedFileSpecific.PrecursorMassTolerance, Is.Not.Null);
+ Assert.That(calibratedFileSpecific.ProductMassTolerance, Is.Not.Null);
+
+ // Verify Glyco task consumed file-specific params from the calibrated file.
+ Assert.That(glycoTask.FileSpecificParameters, Is.Not.Null);
+ Assert.That(glycoTask.FileSpecificParameters.Count, Is.EqualTo(1));
+
+ // Verify Glyco task consumed file-specific params from the calibrated file.
+ Assert.That(glycoTask.FileSpecificParameters, Is.Not.Null);
+ Assert.That(glycoTask.FileSpecificParameters.Count, Is.EqualTo(1));
+
+ CommonParameters glycoFileSpecificParams = glycoTask.FileSpecificParameters[0].Parameters;
+ Assert.That(glycoFileSpecificParams.PrecursorMassTolerance.GetType(), Is.EqualTo(calibratedFileSpecific.PrecursorMassTolerance.GetType()));
+ Assert.That(glycoFileSpecificParams.ProductMassTolerance.GetType(), Is.EqualTo(calibratedFileSpecific.ProductMassTolerance.GetType()));
+ Assert.That(glycoFileSpecificParams.PrecursorMassTolerance.Value, Is.EqualTo(calibratedFileSpecific.PrecursorMassTolerance.Value).Within(1e-9));
+ Assert.That(glycoFileSpecificParams.ProductMassTolerance.Value, Is.EqualTo(calibratedFileSpecific.ProductMassTolerance.Value).Within(1e-9));
+
+ // Low-res tolerance inherits from the original ProductMassTolerance at construction (20 ppm),
+ // not the calibration-narrowed value, since calibration does not update low-res tolerance.
+ Assert.That(glycoFileSpecificParams.ProductMassTolerance_LowRes, Is.TypeOf());
+ Assert.That(glycoFileSpecificParams.ProductMassTolerance_LowRes.Value, Is.EqualTo(originalProductTolerance.Value).Within(1e-9));
+
+ // Confirm Glyco task TOML is written.
+ string glycoTaskTomlPath = Path.Combine(outputRoot, "Task Settings", "Glycoconfig.toml");
+ Assert.That(File.Exists(glycoTaskTomlPath), Is.True, "Glyco task config toml was not written.");
+ }
+ finally
+ {
+ if (Directory.Exists(outputRoot))
+ {
+ Directory.Delete(outputRoot, true);
+ }
+ }
+
+ }
+
}
}
diff --git a/MetaMorpheus/Test/GlycoTestData/GlycoSearchTaskconfigOGlycoTest_ToleranceTracking.toml b/MetaMorpheus/Test/GlycoTestData/GlycoSearchTaskconfigOGlycoTest_ToleranceTracking.toml
new file mode 100644
index 0000000000..9591e7a89d
--- /dev/null
+++ b/MetaMorpheus/Test/GlycoTestData/GlycoSearchTaskconfigOGlycoTest_ToleranceTracking.toml
@@ -0,0 +1,65 @@
+TaskType = "GlycoSearch"
+
+[_glycoSearchParameters]
+OGlycanDatabasefile = "OGlycan.gdb"
+NGlycanDatabasefile = "NGlycan.gdb"
+GlycoSearchType = "OGlycanSearch"
+OxoniumIonFilt = true
+DecoyType = "Reverse"
+GlycoSearchTopNum = 50
+MaximumOGlycanAllowed = 4
+DoParsimony = true
+NoOneHitWonders = false
+ModPeptidesAreDifferent = false
+WriteIndividualFiles = false
+WriteDecoys = true
+WriteContaminants = true
+
+[CommonParameters]
+TaskDescriptor = "GlycoSearchTask"
+MaxThreadsToUsePerFile = 7
+ListOfModsFixed = "Common Fixed\tCarbamidomethyl on C\t\tCommon Fixed\tCarbamidomethyl on U"
+ListOfModsVariable = "Common Variable\tOxidation on M"
+DoPrecursorDeconvolution = true
+UseProvidedPrecursorInfo = true
+DeconvolutionIntensityRatio = 3.0
+DeconvolutionMaxAssumedChargeState = 12
+DeconvolutionMassTolerance = "±4.0000 PPM"
+TotalPartitions = 1
+ProductMassTolerance = "±40.0000 PPM"
+PrecursorMassTolerance = "±10.0000 PPM"
+AddCompIons = false
+ScoreCutoff = 3.0
+ReportAllAmbiguity = true
+NumberOfPeaksToKeepPerWindow = 1000
+MinimumAllowedIntensityRatioToBasePeak = 0.01
+NormalizePeaksAccrossAllWindows = false
+TrimMs1Peaks = false
+TrimMsMsPeaks = false
+UseDeltaScore = false
+QValueOutputFilter = 1.0
+PepQValueOutputFilter = 1.0
+CustomIons = ["c", "zDot"]
+AssumeOrphanPeaksAreZ1Fragments = true
+MaxHeterozygousVariants = 4
+MinVariantDepth = 1
+AddTruncations = false
+DissociationType = "EThcD"
+SeparationType = "HPLC"
+MS2ChildScanDissociationType = "Unknown"
+MS3ChildScanDissociationType = "Unknown"
+
+[CommonParameters.DigestionParams]
+MaxMissedCleavages = 5
+InitiatorMethionineBehavior = "Variable"
+MinPeptideLength = 5
+MaxPeptideLength = 60
+MaxModificationIsoforms = 1024
+MaxModsForPeptide = 2
+Protease = "StcE-trypsin"
+SearchModeType = "Full"
+FragmentationTerminus = "Both"
+SpecificProtease = "StcE-trypsin"
+GeneratehUnlabeledProteinsForSilac = true
+KeepNGlycopeptide = false
+KeepOGlycopeptide = false
diff --git a/MetaMorpheus/Test/GuiTests/CustomMIonLossTests.cs b/MetaMorpheus/Test/GuiTests/CustomMIonLossTests.cs
new file mode 100644
index 0000000000..3b8ef901b3
--- /dev/null
+++ b/MetaMorpheus/Test/GuiTests/CustomMIonLossTests.cs
@@ -0,0 +1,544 @@
+using Chemistry;
+using EngineLayer;
+using GuiFunctions;
+using GuiFunctions.Models;
+using GuiFunctions.Util;
+using NUnit.Framework;
+using System;
+using System.IO;
+using System.Linq;
+
+namespace Test.GuiTests;
+
+[TestFixture]
+public class CustomMIonLossTests
+{
+ private string _testFilePath;
+
+ [SetUp]
+ public void Setup()
+ {
+ // Use a temporary test file path
+ _testFilePath = Path.Combine(Path.GetTempPath(), "TestCustomMIonLosses.txt");
+
+ // Clear any existing cache and test file
+ CustomMIonLossManager.ClearCache();
+ if (File.Exists(_testFilePath))
+ {
+ File.Delete(_testFilePath);
+ }
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ // Clean up test file
+ if (File.Exists(_testFilePath))
+ {
+ File.Delete(_testFilePath);
+ }
+ CustomMIonLossManager.ClearCache();
+ }
+
+ #region CustomMIonLoss Tests
+
+ [Test]
+ public void CustomMIonLoss_Constructor_CreatesValidObject()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+
+ // Act
+ var loss = new CustomMIonLoss("Water Loss", "-H2O", formula, AnalyteType.Peptide);
+
+ // Assert
+ Assert.That(loss.Name, Is.EqualTo("Water Loss"));
+ Assert.That(loss.Annotation, Is.EqualTo("-H2O"));
+ Assert.That(loss.ApplicableAnalyteType, Is.EqualTo(AnalyteType.Peptide));
+ Assert.That(loss.MonoisotopicMass, Is.EqualTo(formula.MonoisotopicMass).Within(0.0001));
+ }
+
+ [Test]
+ public void CustomMIonLoss_ToMIonLoss_ConvertsCorrectly()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H3N1");
+ var customLoss = new CustomMIonLoss("Ammonia Loss", "-NH3", formula, AnalyteType.Peptide);
+
+ // Act
+ var mIonLoss = customLoss.ToMIonLoss();
+
+ // Assert
+ Assert.That(mIonLoss.Name, Is.EqualTo("Ammonia Loss"));
+ Assert.That(mIonLoss.Annotation, Is.EqualTo("-NH3"));
+ Assert.That(mIonLoss.MonoisotopicMass, Is.EqualTo(customLoss.MonoisotopicMass).Within(0.0001));
+ }
+
+ [Test]
+ public void CustomMIonLoss_FromMIonLoss_CreatesCorrectly()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H1P1O3");
+ var mIonLoss = new Omics.Fragmentation.MIonLoss("Phosphate Loss", "-P", formula);
+
+ // Act
+ var customLoss = CustomMIonLoss.FromMIonLoss(mIonLoss, AnalyteType.Oligo);
+
+ // Assert
+ Assert.That(customLoss.Name, Is.EqualTo(mIonLoss.Name));
+ Assert.That(customLoss.Annotation, Is.EqualTo(mIonLoss.Annotation));
+ Assert.That(customLoss.ApplicableAnalyteType, Is.EqualTo(AnalyteType.Oligo));
+ Assert.That(customLoss.MonoisotopicMass, Is.EqualTo(mIonLoss.MonoisotopicMass).Within(0.0001));
+ }
+
+ [Test]
+ public void CustomMIonLoss_Serialize_CreatesCorrectFormat()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+ var loss = new CustomMIonLoss("Water Loss", "-H2O", formula, AnalyteType.Peptide);
+
+ // Act
+ var serialized = loss.Serialize();
+
+ // Assert
+ Assert.That(serialized, Is.EqualTo("Water Loss|-H2O|H2O|Peptide"));
+ }
+
+ [Test]
+ public void CustomMIonLoss_Deserialize_ParsesCorrectly()
+ {
+ // Arrange
+ var line = "Water Loss|-H2O|H2O1|Peptide";
+
+ // Act
+ var loss = CustomMIonLoss.Deserialize(line);
+
+ // Assert
+ Assert.That(loss.Name, Is.EqualTo("Water Loss"));
+ Assert.That(loss.Annotation, Is.EqualTo("-H2O"));
+ Assert.That(loss.ApplicableAnalyteType, Is.EqualTo(AnalyteType.Peptide));
+ Assert.That(loss.MonoisotopicMass, Is.GreaterThan(0));
+ }
+
+ [Test]
+ public void CustomMIonLoss_Deserialize_InvalidFormat_ThrowsException()
+ {
+ // Arrange
+ var invalidLine = "Water Loss|-H2O|H2O1"; // Missing AnalyteType
+
+ // Act & Assert
+ Assert.Throws(() => CustomMIonLoss.Deserialize(invalidLine));
+ }
+
+ [Test]
+ public void CustomMIonLoss_Deserialize_InvalidAnalyteType_ThrowsException()
+ {
+ // Arrange
+ var invalidLine = "Water Loss|-H2O|H2O1|InvalidType";
+
+ // Act & Assert
+ Assert.Throws(() => CustomMIonLoss.Deserialize(invalidLine));
+ }
+
+ [Test]
+ public void CustomMIonLoss_Deserialize_InvalidFormula_ThrowsException()
+ {
+ // Arrange
+ var invalidLine = "Water Loss|-H2O|InvalidFormula123|Peptide";
+
+ // Act & Assert
+ Assert.Throws(() => CustomMIonLoss.Deserialize(invalidLine));
+ }
+
+ [Test]
+ public void CustomMIonLoss_IsApplicableToCurrentMode_PeptideMode_ReturnsTrue()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+ var loss = new CustomMIonLoss("Water Loss", "-H2O", formula, AnalyteType.Peptide);
+
+ // Act
+ var isApplicable = loss.IsApplicableToCurrentMode(AnalyteType.Peptide);
+
+ // Assert
+ Assert.That(isApplicable, Is.True);
+ }
+
+ [Test]
+ public void CustomMIonLoss_IsApplicableToCurrentMode_OligoMode_ReturnsTrue()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H1P1O3");
+ var loss = new CustomMIonLoss("Phosphate Loss", "-P", formula, AnalyteType.Oligo);
+
+ // Act
+ var isApplicable = loss.IsApplicableToCurrentMode(AnalyteType.Oligo);
+
+ // Assert
+ Assert.That(isApplicable, Is.True);
+ }
+
+ [Test]
+ public void CustomMIonLoss_IsApplicableToCurrentMode_WrongMode_ReturnsFalse()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+ var loss = new CustomMIonLoss("Water Loss", "-H2O", formula, AnalyteType.Peptide);
+
+ // Act
+ var isApplicable = loss.IsApplicableToCurrentMode(AnalyteType.Oligo);
+
+ // Assert
+ Assert.That(isApplicable, Is.False);
+ }
+
+ [Test]
+ public void CustomMIonLoss_IsApplicableToCurrentMode_ProteoformExplicitMode_ReturnsTrue()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+ var loss = new CustomMIonLoss("Water Loss", "-H2O", formula, AnalyteType.Proteoform);
+
+ // Act
+ var isApplicable = loss.IsApplicableToCurrentMode(AnalyteType.Proteoform);
+
+ // Assert
+ Assert.That(isApplicable, Is.True);
+ }
+
+ [Test]
+ public void CustomMIonLoss_IsApplicableToCurrentMode_ProteoformInferredFromNull_ReturnsTrue_WhenNotRnaMode()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+ var loss = new CustomMIonLoss("Water Loss", "-H2O", formula, AnalyteType.Proteoform);
+
+ // Ensure IsRnaMode is false (proteoform mode)
+ bool originalIsRnaMode = GuiGlobalParamsViewModel.Instance.IsRnaMode;
+ try
+ {
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = false;
+
+ // Act - pass null currentAnalyteType
+ var isApplicable = loss.IsApplicableToCurrentMode(null);
+
+ // Assert
+ Assert.That(isApplicable, Is.True);
+ }
+ finally
+ {
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = originalIsRnaMode;
+ }
+ }
+
+ [Test]
+ public void CustomMIonLoss_IsApplicableToCurrentMode_ProteoformInferredFromNull_ReturnsFalse_WhenRnaMode()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+ var loss = new CustomMIonLoss("Water Loss", "-H2O", formula, AnalyteType.Proteoform);
+
+ // Ensure IsRnaMode is true (RNA mode)
+ bool originalIsRnaMode = GuiGlobalParamsViewModel.Instance.IsRnaMode;
+ try
+ {
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = true;
+
+ // Act - pass null currentAnalyteType
+ var isApplicable = loss.IsApplicableToCurrentMode(null);
+
+ // Assert - should return false when in RNA mode
+ Assert.That(isApplicable, Is.False);
+ }
+ finally
+ {
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = originalIsRnaMode;
+ }
+ }
+
+ [Test]
+ public void CustomMIonLoss_IsApplicableToCurrentMode_ProteoformExplicitMode_WithOligo_ReturnsFalse()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+ var loss = new CustomMIonLoss("Water Loss", "-H2O", formula, AnalyteType.Proteoform);
+
+ // Act
+ var isApplicable = loss.IsApplicableToCurrentMode(AnalyteType.Oligo);
+
+ // Assert
+ Assert.That(isApplicable, Is.False);
+ }
+
+ [Test]
+ public void CustomMIonLoss_SerializeDeserialize_RoundTrip_Successful()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("C2H4O2");
+ var originalLoss = new CustomMIonLoss("Acetic Acid Loss", "-CH3COOH", formula, AnalyteType.Peptide);
+
+ // Act
+ var serialized = originalLoss.Serialize();
+ var deserializedLoss = CustomMIonLoss.Deserialize(serialized);
+
+ // Assert
+ Assert.That(deserializedLoss.Name, Is.EqualTo(originalLoss.Name));
+ Assert.That(deserializedLoss.Annotation, Is.EqualTo(originalLoss.Annotation));
+ Assert.That(deserializedLoss.ApplicableAnalyteType, Is.EqualTo(originalLoss.ApplicableAnalyteType));
+ Assert.That(deserializedLoss.MonoisotopicMass, Is.EqualTo(originalLoss.MonoisotopicMass).Within(0.0001));
+ }
+
+ #endregion
+
+ #region CustomMIonLossManager Tests
+
+ [Test]
+ public void CustomMIonLossManager_GetCustomMIonLossFilePath_ReturnsValidPath()
+ {
+ // Act
+ var filePath = CustomMIonLossManager.GetCustomMIonLossFilePath();
+
+ // Assert
+ Assert.That(filePath, Is.Not.Null);
+ Assert.That(filePath, Does.Contain("Mods"));
+ Assert.That(filePath, Does.EndWith("CustomMIonLosses.txt"));
+ }
+
+ [Test]
+ public void CustomMIonLossManager_AddCustomMIonLoss_AddsSuccessfully()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("CO2");
+ var newLoss = new CustomMIonLoss("CO2 Loss", "-CO2", formula, AnalyteType.Peptide);
+
+ // Act
+ CustomMIonLossManager.AddCustomMIonLoss(newLoss);
+ var losses = CustomMIonLossManager.LoadCustomMIonLosses();
+
+ // Assert
+ Assert.That(losses.Any(l => l.Name == "CO2 Loss"), Is.True);
+ Assert.That(losses.Any(l => l.Annotation == "-CO2"), Is.True);
+ }
+
+ [Test]
+ public void CustomMIonLossManager_AddCustomMIonLoss_Duplicate_ThrowsException()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+ var loss1 = new CustomMIonLoss("Test Loss", "-TEST", formula, AnalyteType.Peptide);
+ var loss2 = new CustomMIonLoss("Test Loss", "-TEST2", formula, AnalyteType.Peptide);
+
+ // Act
+ CustomMIonLossManager.AddCustomMIonLoss(loss1);
+
+ // Assert
+ Assert.Throws(() => CustomMIonLossManager.AddCustomMIonLoss(loss2));
+ }
+
+ [Test]
+ public void CustomMIonLossManager_AddCustomMIonLoss_SameNameDifferentMode_Succeeds()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+ var peptideLoss = new CustomMIonLoss("Multi Mode Loss", "-MMM", formula, AnalyteType.Peptide);
+ var oligoLoss = new CustomMIonLoss("Multi Mode Loss", "-MMM", formula, AnalyteType.Oligo);
+
+ // Act
+ CustomMIonLossManager.AddCustomMIonLoss(peptideLoss);
+ CustomMIonLossManager.AddCustomMIonLoss(oligoLoss);
+ var losses = CustomMIonLossManager.LoadCustomMIonLosses();
+
+ // Assert
+ var multiModeLosses = losses.Where(l => l.Name == "Multi Mode Loss").ToList();
+ Assert.That(multiModeLosses.Count, Is.EqualTo(2));
+ Assert.That(multiModeLosses.Any(l => l.ApplicableAnalyteType == AnalyteType.Peptide), Is.True);
+ Assert.That(multiModeLosses.Any(l => l.ApplicableAnalyteType == AnalyteType.Oligo), Is.True);
+ }
+
+ [Test]
+ public void CustomMIonLossManager_SaveAndLoad_PreservesData()
+ {
+ // Arrange
+ var formula1 = ChemicalFormula.ParseFormula("H2O1");
+ var formula2 = ChemicalFormula.ParseFormula("H3N1");
+ var losses = new[]
+ {
+ new CustomMIonLoss("Water Loss", "-H2O", formula1, AnalyteType.Peptide),
+ new CustomMIonLoss("Ammonia Loss", "-NH3", formula2, AnalyteType.Oligo)
+ };
+
+ // Act
+ CustomMIonLossManager.SaveCustomMIonLosses(losses);
+ CustomMIonLossManager.ClearCache(); // Force reload from file
+ var loadedLosses = CustomMIonLossManager.LoadCustomMIonLosses();
+
+ // Assert
+ Assert.That(loadedLosses.Count, Is.GreaterThanOrEqualTo(2));
+ Assert.That(loadedLosses.Any(l => l.Name == "Water Loss" && l.ApplicableAnalyteType == AnalyteType.Peptide), Is.True);
+ Assert.That(loadedLosses.Any(l => l.Name == "Ammonia Loss" && l.ApplicableAnalyteType == AnalyteType.Oligo), Is.True);
+ }
+
+ [Test]
+ public void CustomMIonLossManager_ClearCache_ForcesReload()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("CO1");
+ var newLoss = new CustomMIonLoss("CO Loss", "-CO", formula, AnalyteType.Peptide);
+
+ // Act
+ var losses1 = CustomMIonLossManager.LoadCustomMIonLosses();
+ var initialCount = losses1.Count;
+
+ CustomMIonLossManager.AddCustomMIonLoss(newLoss);
+ CustomMIonLossManager.ClearCache();
+
+ var losses2 = CustomMIonLossManager.LoadCustomMIonLosses();
+
+ // Assert
+ Assert.That(losses2.Count, Is.EqualTo(initialCount + 1));
+ Assert.That(losses2.Any(l => l.Name == "CO Loss"), Is.True);
+ }
+
+ [Test]
+ public void CustomMIonLossManager_DeleteCustomMIonLoss_RemovesSuccessfully()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("SO3");
+ var lossToDelete = new CustomMIonLoss("SO3 Loss", "-SO3", formula, AnalyteType.Peptide);
+ CustomMIonLossManager.AddCustomMIonLoss(lossToDelete);
+
+ // Act
+ CustomMIonLossManager.DeleteCustomMIonLoss("SO3 Loss", AnalyteType.Peptide);
+ var losses = CustomMIonLossManager.LoadCustomMIonLosses();
+
+ // Assert
+ Assert.That(losses.Any(l => l.Name == "SO3 Loss" && l.ApplicableAnalyteType == AnalyteType.Peptide), Is.False);
+ }
+
+ [Test]
+ public void CustomMIonLossManager_DeleteCustomMIonLoss_NonExistent_DoesNotThrow()
+ {
+ // Act & Assert
+ Assert.DoesNotThrow(() => CustomMIonLossManager.DeleteCustomMIonLoss("NonExistent Loss", AnalyteType.Peptide));
+ }
+
+ [Test]
+ public void CustomMIonLossManager_GetAllMIonLossesForAnalyteType_Peptide_ReturnsCorrectLosses()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+ var peptideLoss = new CustomMIonLoss("Peptide Only Loss", "-PEP", formula, AnalyteType.Peptide);
+ CustomMIonLossManager.AddCustomMIonLoss(peptideLoss);
+
+ // Act
+ var peptideLosses = CustomMIonLossManager.GetAllMIonLossesForAnalyteType(AnalyteType.Peptide);
+
+ // Assert
+ Assert.That(peptideLosses.Any(l => l.Name == "Peptide Only Loss"), Is.True);
+ }
+
+ [Test]
+ public void CustomMIonLossManager_GetAllMIonLossesForAnalyteType_Oligo_ReturnsCorrectLosses()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H1P1O3");
+ var oligoLoss = new CustomMIonLoss("Oligo Only Loss", "-OLI", formula, AnalyteType.Oligo);
+ CustomMIonLossManager.AddCustomMIonLoss(oligoLoss);
+
+ // Act
+ var oligoLosses = CustomMIonLossManager.GetAllMIonLossesForAnalyteType(AnalyteType.Oligo);
+
+ // Assert
+ Assert.That(oligoLosses.Any(l => l.Name == "Oligo Only Loss"), Is.True);
+ }
+
+ [Test]
+ public void CustomMIonLossManager_GetAllMIonLossesForAnalyteType_FiltersCorrectly()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+ var peptideLoss = new CustomMIonLoss("Peptide Specific", "-PEP", formula, AnalyteType.Peptide);
+ var oligoLoss = new CustomMIonLoss("Oligo Specific", "-OLI", formula, AnalyteType.Oligo);
+
+ CustomMIonLossManager.AddCustomMIonLoss(peptideLoss);
+ CustomMIonLossManager.AddCustomMIonLoss(oligoLoss);
+
+ // Act
+ var peptideLosses = CustomMIonLossManager.GetAllMIonLossesForAnalyteType(AnalyteType.Peptide);
+ var oligoLosses = CustomMIonLossManager.GetAllMIonLossesForAnalyteType(AnalyteType.Oligo);
+
+ // Assert
+ Assert.That(peptideLosses.Any(l => l.Name == "Peptide Specific"), Is.True);
+ Assert.That(peptideLosses.Any(l => l.Name == "Oligo Specific"), Is.False);
+
+ Assert.That(oligoLosses.Any(l => l.Name == "Oligo Specific"), Is.True);
+ Assert.That(oligoLosses.Any(l => l.Name == "Peptide Specific"), Is.False);
+ }
+
+ [Test]
+ public void CustomMIonLossManager_LoadCustomMIonLosses_SkipsComments()
+ {
+ // Arrange
+ var filePath = CustomMIonLossManager.GetCustomMIonLossFilePath();
+ var lines = new[]
+ {
+ "# This is a comment",
+ "Water Loss|-H2O|H2O1|Peptide",
+ "# Another comment",
+ "",
+ "Ammonia Loss|-NH3|H3N1|Peptide"
+ };
+ File.WriteAllLines(filePath, lines);
+ CustomMIonLossManager.ClearCache();
+
+ // Act
+ var losses = CustomMIonLossManager.LoadCustomMIonLosses();
+
+ // Assert
+ Assert.That(losses.Count, Is.EqualTo(2));
+ Assert.That(losses.All(l => !l.Name.StartsWith("#")), Is.True);
+ }
+
+ [Test]
+ public void CustomMIonLossManager_LoadCustomMIonLosses_UsesCaching()
+ {
+ // Arrange
+ var formula = ChemicalFormula.ParseFormula("H2O1");
+ var loss = new CustomMIonLoss("Cache Test", "-CACHE", formula, AnalyteType.Peptide);
+ CustomMIonLossManager.AddCustomMIonLoss(loss);
+
+ // Act
+ var losses1 = CustomMIonLossManager.LoadCustomMIonLosses();
+ var losses2 = CustomMIonLossManager.LoadCustomMIonLosses(); // Should use cache
+
+ // Assert
+ Assert.That(losses1.Count, Is.EqualTo(losses2.Count));
+ Assert.That(losses1.Select(l => l.Name), Is.EquivalentTo(losses2.Select(l => l.Name)));
+ }
+
+ [Test]
+ public void CustomMIonLossManager_LoadCustomMIonLosses_InvalidLine_SkipsAndContinues()
+ {
+ // Arrange
+ var filePath = CustomMIonLossManager.GetCustomMIonLossFilePath();
+ var lines = new[]
+ {
+ "Water Loss|-H2O|H2O1|Peptide",
+ "Invalid|Line", // Invalid format
+ "Ammonia Loss|-NH3|H3N1|Peptide"
+ };
+ File.WriteAllLines(filePath, lines);
+ CustomMIonLossManager.ClearCache();
+
+ // Act
+ var losses = CustomMIonLossManager.LoadCustomMIonLosses();
+
+ // Assert
+ Assert.That(losses.Count, Is.EqualTo(2)); // Should skip invalid line
+ Assert.That(losses.Any(l => l.Name == "Water Loss"), Is.True);
+ Assert.That(losses.Any(l => l.Name == "Ammonia Loss"), Is.True);
+ }
+
+ #endregion
+}
diff --git a/MetaMorpheus/Test/GuiTests/FragmentParamsVM.cs b/MetaMorpheus/Test/GuiTests/FragmentParamsVM.cs
new file mode 100644
index 0000000000..8208d05e5a
--- /dev/null
+++ b/MetaMorpheus/Test/GuiTests/FragmentParamsVM.cs
@@ -0,0 +1,505 @@
+using System.IO;
+using EngineLayer;
+using GuiFunctions;
+using NUnit.Framework;
+using System.Linq;
+using Omics.Fragmentation;
+using Proteomics.ProteolyticDigestion;
+using Transcriptomics;
+using Transcriptomics.Digestion;
+using GuiFunctions.Util;
+using Chemistry;
+using GuiFunctions.Models;
+using TaskLayer;
+
+namespace Test.GuiTests;
+
+[TestFixture]
+public class FragmentParamsVM
+{
+ [SetUp]
+ public void Setup()
+ {
+ // Clear cache before each test
+ CustomMIonLossManager.ClearCache();
+
+ var path = CustomMIonLossManager.GetCustomMIonLossFilePath();
+ if (File.Exists(path))
+ File.Delete(path);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ // Clear cache after each test
+ CustomMIonLossManager.ClearCache();
+ }
+
+ [Test]
+ [NonParallelizable]
+ public void LossOptions_ProteinMode()
+ {
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = false;
+ var common = new CommonParameters(fragmentationParams: new FragmentationParams(), digestionParams: new DigestionParams());
+ var vm = new FragmentationParamsViewModel(common, new());
+ Assert.That(vm.AvailableMIonLosses, Is.Not.Empty);
+ Assert.That(vm.AvailableMIonLosses.Count, Is.GreaterThanOrEqualTo(1));
+ }
+
+ [Test]
+ [NonParallelizable]
+ public void LossOptions_RNAMode()
+ {
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = true;
+ var common = new CommonParameters(fragmentationParams: new RnaFragmentationParams(), digestionParams: new RnaDigestionParams());
+ var vm = new FragmentationParamsViewModel(common, new());
+ Assert.That(vm.AvailableMIonLosses, Is.Not.Empty);
+ Assert.That(vm.AvailableMIonLosses.Count, Is.GreaterThan(10));
+ }
+
+ [Test]
+ [NonParallelizable]
+ public void AddAndRemoveAllLosses()
+ {
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = true;
+ var vm = new FragmentationParamsViewModel(new(), new());
+
+ vm.AddAllLossesCommand.Execute(null);
+ Assert.That(vm.AvailableMIonLosses.Select(p => p.IsSelected), Is.All.EqualTo(true));
+
+ vm.RemoveAllLossesCommand.Execute(null);
+ Assert.That(vm.AvailableMIonLosses.Select(p => p.IsSelected), Is.All.EqualTo(false));
+ }
+
+ [Test]
+ [NonParallelizable]
+ public void Constructor_InitializesProperties()
+ {
+ // Arrange
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = false;
+ var common = new CommonParameters(
+ fragmentationParams: new FragmentationParams { GenerateMIon = true },
+ digestionParams: new DigestionParams());
+ var searchParams = new SearchParameters
+ {
+ MaxFragmentSize = 5000,
+ MinAllowedInternalFragmentLength = 4
+ };
+
+ // Act
+ var vm = new FragmentationParamsViewModel(common, searchParams);
+
+ // Assert
+ Assert.That(vm.GenerateMIon, Is.True);
+ Assert.That(vm.MaxFragmentMassDa, Is.EqualTo(5000));
+ Assert.That(vm.MinInternalIonLength, Is.EqualTo(4));
+ Assert.That(vm.GenerateInternalIons, Is.True);
+ }
+
+ [Test]
+ [NonParallelizable]
+ public void GenerateInternalIons_SetsMinLength()
+ {
+ // Arrange
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = false;
+ var vm = new FragmentationParamsViewModel(new(), new());
+
+ // Act
+ vm.GenerateInternalIons = true;
+
+ // Assert
+ Assert.That(vm.MinInternalIonLength, Is.EqualTo(4)); // Default value
+ }
+
+ [Test]
+ [NonParallelizable]
+ public void GenerateInternalIons_False_ResetsMinLength()
+ {
+ // Arrange
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = false;
+ var vm = new FragmentationParamsViewModel(new(), new());
+ vm.GenerateInternalIons = true;
+ vm.MinInternalIonLength = 6;
+
+ // Act
+ vm.GenerateInternalIons = false;
+
+ // Assert
+ Assert.That(vm.MinInternalIonLength, Is.EqualTo(0));
+ }
+
+ [Test]
+ [NonParallelizable]
+ public void GetSelectedMIonLosses_ReturnsOnlySelected()
+ {
+ // Arrange
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = false;
+ var vm = new FragmentationParamsViewModel(new(), new());
+
+ // Select only the first loss
+ vm.AvailableMIonLosses.First().IsSelected = true;
+
+ // Act
+ var selectedLosses = vm.GetSelectedMIonLosses();
+
+ // Assert
+ Assert.That(selectedLosses.Count, Is.EqualTo(1));
+ }
+
+ [Test]
+ [NonParallelizable]
+ public void ToFragmentationParams_ProteinMode_ReturnsCorrectType()
+ {
+ // Arrange
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = false;
+ var vm = new FragmentationParamsViewModel(new(), new());
+ vm.GenerateMIon = true;
+
+ // Act
+ var fragmentationParams = vm.ToFragmentationParams();
+
+ // Assert
+ Assert.That(fragmentationParams, Is.TypeOf());
+ Assert.That(fragmentationParams.GenerateMIon, Is.True);
+ }
+
+ [Test]
+ [NonParallelizable]
+ public void ToFragmentationParams_RNAMode_ReturnsCorrectType()
+ {
+ // Arrange
+ GuiGlobalParamsViewModel.Instance.IsRnaMode = true;
+ var vm = new FragmentationParamsViewModel(new(), new());
+ vm.GenerateMIon = true;
+ vm.ModsCanSuppressBaseLossFragments = true;
+
+ // Act
+ var fragmentationParams = vm.ToFragmentationParams();
+
+ // Assert
+ Assert.That(fragmentationParams, Is.TypeOf