From 2250ca773806814af06ac2991b5148ec221f6e96 Mon Sep 17 00:00:00 2001 From: meowcat Date: Thu, 2 Nov 2023 22:09:16 +0100 Subject: [PATCH] Skyline: improve spectral library search to allow for matches within descriptions In the spectral library explorer, you can now place a * in front of the filter search term to look within descriptions. Filters beginning with * search within descriptions, e.g. "*PEET" will match "PEETID" and "AAPEETB", but "PEET" will only match "PEETID" as before. Requested and co-authored by user Michele Stravs --------- Co-authored-by: Michele Stravs Co-authored-by: Brian Pratt --- .../SettingsUI/ViewLibraryDlg.Designer.cs | 3 + .../Skyline/SettingsUI/ViewLibraryDlg.cs | 22 +-- .../Skyline/SettingsUI/ViewLibraryDlg.resx | 12 ++ .../SettingsUI/ViewLibraryPepInfoList.cs | 128 ++++++++++++++---- .../TestFunctional/LibraryExplorerTest.cs | 44 +++++- 5 files changed, 165 insertions(+), 44 deletions(-) diff --git a/pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.Designer.cs b/pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.Designer.cs index 3fb865fc53..6a2e973a88 100644 --- a/pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.Designer.cs +++ b/pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.Designer.cs @@ -106,6 +106,7 @@ private void InitializeComponent() this.toolStripSeparator15 = new System.Windows.Forms.ToolStripSeparator(); this.zoomSpectrumContextMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator27 = new System.Windows.Forms.ToolStripSeparator(); + this.toolTip1 = new System.Windows.Forms.ToolTip(this.components); ((System.ComponentModel.ISupportInitialize)(this.splitPeptideList)).BeginInit(); this.splitPeptideList.Panel1.SuspendLayout(); this.splitPeptideList.Panel2.SuspendLayout(); @@ -279,6 +280,7 @@ private void InitializeComponent() resources.ApplyResources(this.textPeptide, "textPeptide"); this.textPeptide.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; this.textPeptide.Name = "textPeptide"; + this.toolTip1.SetToolTip(this.textPeptide, resources.GetString("textPeptide.ToolTip")); this.textPeptide.TextChanged += new System.EventHandler(this.textPeptide_TextChanged); this.textPeptide.KeyDown += new System.Windows.Forms.KeyEventHandler(this.PeptideTextBox_KeyDown); // @@ -817,5 +819,6 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripButton btnCopy; private System.Windows.Forms.ToolStripButton btnSave; private System.Windows.Forms.ToolStripButton btnPrint; + private System.Windows.Forms.ToolTip toolTip1; } } diff --git a/pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.cs b/pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.cs index e87b710064..d16296f626 100644 --- a/pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.cs +++ b/pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.cs @@ -1228,21 +1228,7 @@ private TextSequence GetCategoryValueTextSequence(int index, Graphics graphics) var propertyName = _peptides.comboFilterCategoryDict .FirstOrDefault(x => x.Value == selectedCategory).Key; - var propertyValue = ViewLibraryPepInfoList.GetStringValue(propertyName, pepInfo); - - // Shorten precursor m/z values to be uniform and match the tool tip - if (selectedCategory.Equals(Resources.PeptideTipProvider_RenderTip_Precursor_m_z)) - { - propertyValue = FormatPrecursorMz(double.TryParse(propertyValue, out var mz) ? mz : 0); - } - else if(selectedCategory.Equals(Resources.PeptideTipProvider_RenderTip_CCS)) - { - propertyValue = FormatCCS(double.Parse(propertyValue)); - } - else if(selectedCategory.Equals(Resources.PeptideTipProvider_RenderTip_Ion_Mobility)) - { - propertyValue = FormatIonMobility(double.Parse(propertyValue), pepInfo.IonMobilityUnits); - } + var propertyValue = ViewLibraryPepInfoList.GetFormattedPropertyValue(propertyName, pepInfo); categoryText = CreateTextSequence(propertyValue, false); } else @@ -1854,17 +1840,17 @@ public ViewLibrarySettings(bool associateProteins) }*/ } - private static string FormatPrecursorMz(double precursorMz) + internal static string FormatPrecursorMz(double precursorMz) { return string.Format(@"{0:F04}", precursorMz); } - private static string FormatIonMobility(double mobility, string units) + internal static string FormatIonMobility(double mobility, string units) { return string.Format(@"{0:F04} {1}", mobility, units); } - private static string FormatCCS(double CCS) + internal static string FormatCCS(double CCS) { return string.Format(@"{0:F04}", CCS); } diff --git a/pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.resx b/pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.resx index 3da3f7e7ef..b9904f5d74 100644 --- a/pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.resx +++ b/pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.resx @@ -639,6 +639,12 @@ 2 + + 436, 17 + + + Filters beginning with * search within descriptions, e.g. *PEET will match PEETID and AAPEETB + textPeptide @@ -1665,6 +1671,12 @@ System.Windows.Forms.ToolStripSeparator, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + toolTip1 + + + System.Windows.Forms.ToolTip, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ViewLibraryDlg diff --git a/pwiz_tools/Skyline/SettingsUI/ViewLibraryPepInfoList.cs b/pwiz_tools/Skyline/SettingsUI/ViewLibraryPepInfoList.cs index a7562dafa3..33a536e2e7 100644 --- a/pwiz_tools/Skyline/SettingsUI/ViewLibraryPepInfoList.cs +++ b/pwiz_tools/Skyline/SettingsUI/ViewLibraryPepInfoList.cs @@ -135,6 +135,47 @@ private List FindValidCategories(List categories) return categories.Where(category => _allEntries.Any(entry => !GetStringValue(category, entry).Equals(string.Empty))).ToList(); } + /// + /// Find the string value of a property for a ViewLibraryPepInfo, formatted as in user display + /// + internal static string GetFormattedPropertyValue(string propertyName, ViewLibraryPepInfo pepInfo) + { + var property = typeof(ViewLibraryPepInfo).GetProperty(propertyName); + if (property is null) + { + return string.Empty; + } + + var value = property.GetValue(pepInfo); + if (value is null) + { + return string.Empty; + } + + string propertyValue; + var dbl = value as double? ?? 0; + + // Shorten precursor m/z values to be uniform and match the tool tip + if (propertyName.Equals(PRECURSOR_MZ)) + { + propertyValue = ViewLibraryDlg.FormatPrecursorMz(dbl); + } + else if (propertyName.Equals(CCS)) + { + propertyValue = ViewLibraryDlg.FormatCCS(dbl); + } + else if (propertyName.Equals(ION_MOBILITY)) + { + propertyValue = ViewLibraryDlg.FormatIonMobility(dbl, pepInfo.IonMobilityUnits); + } + else + { + propertyValue = value.ToString(); + } + + return propertyValue; + } + /// /// Find the string value of a property for a ViewLibraryPepInfo /// @@ -258,6 +299,29 @@ private List PrefixSearchByProperty(string filterText) } + /// + /// Find entries which contain the filter text in the given property + /// + private List ContainsSearchByProperty(string filterText) + { + if (string.IsNullOrEmpty(filterText)) + { + return new List(); + } + var orderedList = _listCache.GetOrCreate(_selectedFilterCategory); // List of indexes of items that have values for property of interest + IEnumerable matches; + if (_accessionNumberTypes.Contains(_selectedFilterCategory)) + { + matches = orderedList.Where(item => _allEntries[item].OtherKeysDict[_selectedFilterCategory] + .IndexOf(filterText, StringComparison.OrdinalIgnoreCase) >= 0); + } + else + { + matches = orderedList.Where(item => GetFormattedPropertyValue(_selectedFilterCategory, _allEntries[item]) + .IndexOf(filterText, StringComparison.OrdinalIgnoreCase) >= 0); + } + return matches.ToList(); + } /// /// Find the indices of entries matching the filter text according to the filter type @@ -278,33 +342,49 @@ public IList Filter(string filterText, string filterCategory) // We have to deal with the UnmodifiedTargetText separately from the adduct because the // adduct has special sorting which is different than the way adduct.ToString() would sort. - // Find the indices of entries that have a field that could match the search term if something was appended to it - var filteredIndices = PrefixSearchByProperty(filterText); - - // Special filtering for numeric properties - if (double.TryParse(filterText, NumberStyles.Any, CultureInfo.CurrentCulture, out var result) && _continuousFields.Contains(_selectedFilterCategory)) + List filteredIndices; + var isWildcard = filterText.StartsWith(@"*"); + if (isWildcard) { - // Add entries that are close to the filter text numerically - // Create a list of object references sorted by their absolute difference from target - var sortedByDifference = _allEntries.OrderBy(entry => Math.Abs(double.Parse(GetStringValue(_selectedFilterCategory, entry), NumberStyles.Any, CultureInfo.CurrentCulture) - result)); - - // Then return everything before the first entry with a difference exceeding our match tolerance - var results = sortedByDifference.TakeWhile(entry => !(Math.Abs( - double.Parse(GetStringValue(_selectedFilterCategory, entry), NumberStyles.Any, CultureInfo.CurrentCulture) - result) > FILTER_TOLERANCE)).Select(IndexOf).ToList(); - filteredIndices = filteredIndices.Union(results).ToList(); + filterText = filterText.Substring(1); } - - // If we have not found any matches yet and it is a peptide list look at all the entries which could match - // the target text, if they had something appended to them. - if (!filteredIndices.Any()) + if (isWildcard && filterText.Length>0) { - var range = CollectionUtil.BinarySearch(_allEntries, - info => string.Compare(info.UnmodifiedTargetText, 0, filterText, 0, - info.UnmodifiedTargetText.Length, - StringComparison.OrdinalIgnoreCase)); - // Return the elements from the range whose DisplayText actually matches the filter text. - return ImmutableList.ValueOf(new RangeList(range).Where(i => _allEntries[i].DisplayText - .StartsWith(filterText, StringComparison.OrdinalIgnoreCase))); + // For strings starting in "*", find the indices of entries that have a field + // that contain the search term + filteredIndices = ContainsSearchByProperty(filterText); + } + else + { + // Otherwise, find the indices of entries that have a field that could match the search term + // if something was appended to it + filteredIndices = PrefixSearchByProperty(filterText); + + // Special filtering for numeric properties + if (double.TryParse(filterText, NumberStyles.Any, CultureInfo.CurrentCulture, out var result) && _continuousFields.Contains(_selectedFilterCategory)) + { + // Add entries that are close to the filter text numerically + // Create a list of object references sorted by their absolute difference from target + var sortedByDifference = _allEntries.OrderBy(entry => Math.Abs(double.Parse(GetStringValue(_selectedFilterCategory, entry), NumberStyles.Any, CultureInfo.CurrentCulture) - result)); + + // Then return everything before the first entry with a difference exceeding our match tolerance + var results = sortedByDifference.TakeWhile(entry => !(Math.Abs( + double.Parse(GetStringValue(_selectedFilterCategory, entry), NumberStyles.Any, CultureInfo.CurrentCulture) - result) > FILTER_TOLERANCE)).Select(IndexOf).ToList(); + filteredIndices = filteredIndices.Union(results).ToList(); + } + + // If we have not found any matches yet and it is a peptide list look at all the entries which could match + // the target text, if they had something appended to them. + if (!filteredIndices.Any()) + { + var range = CollectionUtil.BinarySearch(_allEntries, + info => string.Compare(info.UnmodifiedTargetText, 0, filterText, 0, + info.UnmodifiedTargetText.Length, + StringComparison.OrdinalIgnoreCase)); + // Return the elements from the range whose DisplayText actually matches the filter text. + return ImmutableList.ValueOf(new RangeList(range).Where(i => _allEntries[i].DisplayText + .StartsWith(filterText, StringComparison.OrdinalIgnoreCase))); + } } // Return the indices of the matches sorted alphabetically by display text return ImmutableList.ValueOf(filteredIndices.OrderBy(info => info)); diff --git a/pwiz_tools/Skyline/TestFunctional/LibraryExplorerTest.cs b/pwiz_tools/Skyline/TestFunctional/LibraryExplorerTest.cs index b4b4edc4a8..3bced23494 100644 --- a/pwiz_tools/Skyline/TestFunctional/LibraryExplorerTest.cs +++ b/pwiz_tools/Skyline/TestFunctional/LibraryExplorerTest.cs @@ -24,6 +24,7 @@ using System.Linq; using System.Windows.Forms; using Microsoft.VisualStudio.TestTools.UnitTesting; +using pwiz.Common.Chemistry; using pwiz.MSGraph; using pwiz.Skyline.Alerts; using pwiz.Skyline.Controls.SeqNode; @@ -225,18 +226,25 @@ private void TestFilterFunctionality() Assert.IsTrue(StringComparer.OrdinalIgnoreCase.Compare(x.DisplayText, y.DisplayText) <= 0); } + // Find all that contain chlorine + FilterListAndVerifyCount(filterTextBox, pepList, "*Cl", 3); + // Entering the formula for Midazolam should filter out all other spectra var midazolamFormula = "C18H13ClFN3"; FilterListAndVerifyCount(filterTextBox, pepList, midazolamFormula, 1); + FilterListAndVerifyCount(filterTextBox, pepList, midazolamFormula.Replace("C18", "*"), 1); // Wildcard // Check case insensitivity FilterListAndVerifyCount(filterTextBox, pepList, midazolamFormula.ToLowerInvariant(), 1); + FilterListAndVerifyCount(filterTextBox, pepList, midazolamFormula.ToLowerInvariant().Replace("c18", "*"), 1); // Clearing search box should bring up every entry FilterListAndVerifyCount(filterTextBox, pepList, "", 6); + FilterListAndVerifyCount(filterTextBox, pepList, "*", 6); // Should have same effect as no filter // Entering 'SD' should filter out all entries as nothing starts with SD FilterListAndVerifyCount(filterTextBox, pepList, "SD", 0); + FilterListAndVerifyCount(filterTextBox, pepList, "*SD", 0); // Nor does anything contain SD // Clearing the filter text box should bring up every entry FilterListAndVerifyCount(filterTextBox, pepList, "", 6); @@ -253,8 +261,10 @@ private void TestFilterFunctionality() var midazolamMzStr = midazolamMz.ToString("G", CultureInfo.CurrentCulture); var inexactMidazolamMzStr = (midazolamMz + 0.05).ToString("G", CultureInfo.CurrentCulture); - // Entering '32' should filter the list down to three entries + // Entering '32' should filter the list down to two entries FilterListAndVerifyCount(filterTextBox, pepList, midazolamMzStr.Substring(0, 2), 2); + // Entering '*26.0' should yield just one entry + FilterListAndVerifyCount(filterTextBox, pepList, midazolamMzStr.Replace("326", "*26").Substring(0, 4), 1); // Entering the exact precursor m/z of Midazolam should narrow the list down to only Midazolam FilterListAndVerifyCount(filterTextBox, pepList, midazolamMzStr, 1); @@ -270,6 +280,7 @@ private void TestFilterFunctionality() filterCategoryComboBox.FindStringExact("cas"); }); FilterListAndVerifyCount(filterTextBox, pepList, "4928", 1); + FilterListAndVerifyCount(filterTextBox, pepList, "*928", 1); // Wildcard // Now switch to a list with multiple molecular IDs RunUI(() => { libComboBox.SelectedIndex = libComboBox.FindStringExact(MULTIPLE_MOL_IDS); }); @@ -295,6 +306,7 @@ private void TestFilterFunctionality() }); FilterListAndVerifyCount(filterTextBox, pepList, "123", 1); + FilterListAndVerifyCount(filterTextBox, pepList, "*23", 1); // Wildcard // Now test search behavior on a peptide list @@ -305,6 +317,8 @@ private void TestFilterFunctionality() // Searching for a peptide sequence should work as well FilterListAndVerifyCount(filterTextBox, pepList, "CY", 2); + FilterListAndVerifyCount(filterTextBox, pepList, "*CY", 31); // Wildcard + FilterListAndVerifyCount(filterTextBox, pepList, "*Y", 80); // Wildcard // Now test filtering by precursor m/z RunUI(() => @@ -315,6 +329,7 @@ private void TestFilterFunctionality() // Precursor searching should work here as well FilterListAndVerifyCount(filterTextBox, pepList, "6", 13); + FilterListAndVerifyCount(filterTextBox, pepList, "*" + (6.4).ToString("G", CultureInfo.CurrentCulture), 3); // Switch to library with both molecules and peptides ShowDialog( @@ -323,9 +338,34 @@ private void TestFilterFunctionality() // Verify that we found all of the filter categories expectedCategories = new List(categories); - expectedCategories.AddRange(new List{ Resources.PeptideTipProvider_RenderTip_Ion_Mobility , Resources.PeptideTipProvider_RenderTip_CCS , "InChI", smiles}); + var inchi = "InChI"; + expectedCategories.AddRange(new List { Resources.PeptideTipProvider_RenderTip_Ion_Mobility, Resources.PeptideTipProvider_RenderTip_CCS, inchi, smiles }); VerifyFilterCategories(filterCategoryComboBox, expectedCategories); + // * Match IM units + RunUI(() => + { + filterCategoryComboBox.SelectedIndex = + filterCategoryComboBox.FindStringExact(Resources.PeptideTipProvider_RenderTip_Ion_Mobility); + }); + FilterListAndVerifyCount(filterTextBox, pepList, + "*" + IonMobilityValue.GetUnitsString(eIonMobilityUnits.drift_time_msec), 2); + + // Match SMILES + RunUI(() => + { + filterCategoryComboBox.SelectedIndex = filterCategoryComboBox.FindStringExact(smiles); + }); + FilterListAndVerifyCount(filterTextBox, pepList, "*=", 1); + + // Match InChI + RunUI(() => + { + filterCategoryComboBox.SelectedIndex = filterCategoryComboBox.FindStringExact(inchi); + }); + FilterListAndVerifyCount(filterTextBox, pepList, "*C32", 1); + + // Close the spectral library explorer OkDialog(_viewLibUI , _viewLibUI.CancelDialog); }