Skip to content

Commit

Permalink
Skyline: improve spectral library search to allow for matches within …
Browse files Browse the repository at this point in the history
…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 <[email protected]>
Co-authored-by: Brian Pratt <[email protected]>
  • Loading branch information
3 people authored Nov 2, 2023
1 parent 614b6d4 commit 2250ca7
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 44 deletions.
3 changes: 3 additions & 0 deletions pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 4 additions & 18 deletions pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
12 changes: 12 additions & 0 deletions pwiz_tools/Skyline/SettingsUI/ViewLibraryDlg.resx
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,12 @@
<data name="textPeptide.TabIndex" type="System.Int32, mscorlib">
<value>2</value>
</data>
<metadata name="toolTip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>436, 17</value>
</metadata>
<data name="textPeptide.ToolTip" xml:space="preserve">
<value>Filters beginning with * search within descriptions, e.g. *PEET will match PEETID and AAPEETB</value>
</data>
<data name="&gt;&gt;textPeptide.Name" xml:space="preserve">
<value>textPeptide</value>
</data>
Expand Down Expand Up @@ -1665,6 +1671,12 @@
<data name="&gt;&gt;toolStripSeparator27.Type" xml:space="preserve">
<value>System.Windows.Forms.ToolStripSeparator, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="&gt;&gt;toolTip1.Name" xml:space="preserve">
<value>toolTip1</value>
</data>
<data name="&gt;&gt;toolTip1.Type" xml:space="preserve">
<value>System.Windows.Forms.ToolTip, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="&gt;&gt;$this.Name" xml:space="preserve">
<value>ViewLibraryDlg</value>
</data>
Expand Down
128 changes: 104 additions & 24 deletions pwiz_tools/Skyline/SettingsUI/ViewLibraryPepInfoList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,47 @@ private List<string> FindValidCategories(List<string> categories)
return categories.Where(category => _allEntries.Any(entry => !GetStringValue(category, entry).Equals(string.Empty))).ToList();
}

/// <summary>
/// Find the string value of a property for a ViewLibraryPepInfo, formatted as in user display
/// </summary>
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;
}

/// <summary>
/// Find the string value of a property for a ViewLibraryPepInfo
/// </summary>
Expand Down Expand Up @@ -258,6 +299,29 @@ private List<int> PrefixSearchByProperty(string filterText)

}

/// <summary>
/// Find entries which contain the filter text in the given property
/// </summary>
private List<int> ContainsSearchByProperty(string filterText)
{
if (string.IsNullOrEmpty(filterText))
{
return new List<int>();
}
var orderedList = _listCache.GetOrCreate(_selectedFilterCategory); // List of indexes of items that have values for property of interest
IEnumerable<int> 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();
}

/// <summary>
/// Find the indices of entries matching the filter text according to the filter type
Expand All @@ -278,33 +342,49 @@ public IList<int> 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<int> 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));
Expand Down
44 changes: 42 additions & 2 deletions pwiz_tools/Skyline/TestFunctional/LibraryExplorerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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); });
Expand All @@ -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
Expand All @@ -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(() =>
Expand All @@ -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<AddModificationsDlg>(
Expand All @@ -323,9 +338,34 @@ private void TestFilterFunctionality()

// Verify that we found all of the filter categories
expectedCategories = new List<string>(categories);
expectedCategories.AddRange(new List<string>{ Resources.PeptideTipProvider_RenderTip_Ion_Mobility , Resources.PeptideTipProvider_RenderTip_CCS , "InChI", smiles});
var inchi = "InChI";
expectedCategories.AddRange(new List<string> { 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);
}
Expand Down

0 comments on commit 2250ca7

Please sign in to comment.