diff --git a/pwiz_tools/Skyline/Model/Results/ResultsResources.designer.cs b/pwiz_tools/Skyline/Model/Results/ResultsResources.designer.cs
index 68a159b80f1..85aef6ffeb0 100644
--- a/pwiz_tools/Skyline/Model/Results/ResultsResources.designer.cs
+++ b/pwiz_tools/Skyline/Model/Results/ResultsResources.designer.cs
@@ -220,7 +220,7 @@ public static string ChromCacheBuilder_BuildNextFileInner_Importing__0__ {
return ResourceManager.GetString("ChromCacheBuilder_BuildNextFileInner_Importing__0__", resourceCulture);
}
}
-
+
///
/// Looks up a localized string similar to Recalculating scores for {0}.
///
@@ -777,7 +777,34 @@ public static string SpectrumFilter_CalcDiaIsolationValues_Unable_to_determine_i
"_scan_targeted_at__0_", resourceCulture);
}
}
-
+
+ ///
+ /// Looks up a localized string similar to Note: the spectral library has ion mobility values for "{0}", but they are being ignored because "Use spectral library ion mobility values when present" is not enabled. To apply ion mobility filtering, open Settings > Transition Settings > Ion Mobility and enable that option..
+ ///
+ public static string SpectrumFilter_IonMobilityFilteringDisabled_EnableSpectralLibrary {
+ get {
+ return ResourceManager.GetString("SpectrumFilter_IonMobilityFilteringDisabled_EnableSpectralLibrary", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Note: ion mobility values are available for "{0}", but no effective filter window is configured. To apply ion mobility filtering, open Settings > Transition Settings > Ion Mobility and set a window type (resolving power, fixed width, or linear range) with a non-zero value..
+ ///
+ public static string SpectrumFilter_IonMobilityFilteringDisabled_SetWindow {
+ get {
+ return ResourceManager.GetString("SpectrumFilter_IonMobilityFilteringDisabled_SetWindow", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Note: the spectral library has ion mobility values for "{0}", but ion mobility filtering is disabled. To apply filtering, open Settings > Transition Settings > Ion Mobility, set a window type (resolving power, fixed width, or linear range) with a value, and enable "Use spectral library ion mobility values when present"..
+ ///
+ public static string SpectrumFilter_IonMobilityFilteringDisabled_SetWindowAndEnableSpectralLibrary {
+ get {
+ return ResourceManager.GetString("SpectrumFilter_IonMobilityFilteringDisabled_SetWindowAndEnableSpectralLibrary", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to No isolation window found in {0} for precursor {1} (m/z={2:F04} {3}).
///
diff --git a/pwiz_tools/Skyline/Model/Results/ResultsResources.resx b/pwiz_tools/Skyline/Model/Results/ResultsResources.resx
index 2687ce0cac2..4b62522d021 100644
--- a/pwiz_tools/Skyline/Model/Results/ResultsResources.resx
+++ b/pwiz_tools/Skyline/Model/Results/ResultsResources.resx
@@ -311,6 +311,15 @@ It is recommended that you delete this file so that Skyline can create a new fil
Unable to determine isolation width for the scan targeted at {0}
+
+ Note: the spectral library has ion mobility values for "{0}", but they are being ignored because "Use spectral library ion mobility values when present" is not enabled. To apply ion mobility filtering, open Settings > Transition Settings > Ion Mobility and enable that option.
+
+
+ Note: ion mobility values are available for "{0}", but no effective filter window is configured. To apply ion mobility filtering, open Settings > Transition Settings > Ion Mobility and set a window type (resolving power, fixed width, or linear range) with a non-zero value.
+
+
+ Note: the spectral library has ion mobility values for "{0}", but ion mobility filtering is disabled. To apply filtering, open Settings > Transition Settings > Ion Mobility, set a window type (resolving power, fixed width, or linear range) with a value, and enable "Use spectral library ion mobility values when present".
+
Imported spectrum appears to be missing m/z or intensity values ({0} != {1})
diff --git a/pwiz_tools/Skyline/Model/Results/SpectrumFilter.cs b/pwiz_tools/Skyline/Model/Results/SpectrumFilter.cs
index 1c1b2f8bda2..9092bae214a 100644
--- a/pwiz_tools/Skyline/Model/Results/SpectrumFilter.cs
+++ b/pwiz_tools/Skyline/Model/Results/SpectrumFilter.cs
@@ -141,23 +141,19 @@ public SpectrumFilter(SrmDocument document, MsDataFileUri msDataFileUri, IFilter
// TIC and Base peak are meaningless with FAIMS, where we can't know the actual overall ion counts -also can't reliably share times with any ion mobility scheme
if (instrumentInfo != null && instrumentInfo.IonMobilityUnits != eIonMobilityUnits.none)
{
- if ((libraryIonMobilityInfo != null && !libraryIonMobilityInfo.IsEmpty) || _isWatersSonar)
+ _isIonMobilityFiltered = WouldApplyIonMobilityFiltering(
+ document.Settings, libraryIonMobilityInfo, moleculesThisPass, ionMobilityMax);
+
+ // Issue 4150: if filtering would not apply under current settings, probe simple
+ // settings variations to see whether a specific setting change would enable it,
+ // and warn the user about the specific change. When a retention time predictor
+ // is in use, SpectrumFilter is constructed twice for the same file (first pass
+ // sees only first-pass peptides, second pass sees all molecules) - skip the
+ // first pass so the warning fires only once and with the complete molecule set.
+ if (!_isIonMobilityFiltered && (retentionTimePredictor == null || !firstPass))
{
- _isIonMobilityFiltered = true;
- }
- else
- {
- foreach (var pair in moleculesThisPass.SelectMany(
- node => node.TransitionGroups.Select(nodeGroup => new PeptidePrecursorPair(node, nodeGroup))))
- {
- var ionMobility = document.Settings.GetIonMobilityFilter(
- pair.NodePep, pair.NodeGroup, null, libraryIonMobilityInfo, _ionMobilityFunctionsProvider, ionMobilityMax);
- _isIonMobilityFiltered = ionMobility.HasIonMobilityValue;
- if (_isIonMobilityFiltered)
- {
- break;
- }
- }
+ WarnIfSettingsChangeWouldEnableFiltering(
+ document.Settings, libraryIonMobilityInfo, moleculesThisPass, msDataFileUri, ionMobilityMax);
}
}
@@ -490,6 +486,128 @@ public SpectrumFilter(SrmDocument document, MsDataFileUri msDataFileUri, IFilter
InitIonMobilityAndRTLimits();
}
+ // Representative window used solely to probe "would enabling a filter window turn
+ // filtering on for this data?" - value is arbitrary, just needs to be non-empty.
+ private const double PROBE_RESOLVING_POWER = 50;
+
+ ///
+ /// Returns true if ion mobility filtering would actually produce a filter for at least
+ /// one precursor in the document under the given settings. Used both to set
+ /// from real settings and to probe hypothetical
+ /// settings variations when diagnosing a "filtering disabled" warning (issue 4150).
+ ///
+ private bool WouldApplyIonMobilityFiltering(SrmSettings settings,
+ LibraryIonMobilityInfo libraryIonMobilityInfo,
+ IList molecules, double ionMobilityMax)
+ {
+ // Waters SONAR maps mz bins to IM; the "filter" is always derived from the mz filter.
+ if (_isWatersSonar)
+ {
+ return true;
+ }
+
+ foreach (var pair in molecules.SelectMany(
+ node => node.TransitionGroups.Select(nodeGroup => new PeptidePrecursorPair(node, nodeGroup))))
+ {
+ var ionMobility = settings.GetIonMobilityFilter(
+ pair.NodePep, pair.NodeGroup, null, libraryIonMobilityInfo, _ionMobilityFunctionsProvider, ionMobilityMax);
+ if (ionMobility != null && ionMobility.HasIonMobilityValue)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Issue 4150: with current settings no precursor produces an ion mobility filter. Probe
+ // the obvious setting variations (add a window width, enable spectral-library IM, both)
+ // to see whether a specific change would enable filtering. If one would, emit a
+ // non-blocking notice that tells the user exactly which setting to flip. Spectral-library
+ // IM info is settings-invariant so can be reused across probes.
+ private void WarnIfSettingsChangeWouldEnableFiltering(SrmSettings settings,
+ LibraryIonMobilityInfo libraryIonMobilityInfo,
+ IList molecules, MsDataFileUri msDataFileUri, double ionMobilityMax)
+ {
+ bool WouldWith(SrmSettings probe) =>
+ WouldApplyIonMobilityFiltering(probe, libraryIonMobilityInfo, molecules, ionMobilityMax);
+
+ // Skip probes that can't change the outcome: if a setting is already on, the
+ // corresponding "With..." variation is a no-op, and since base filtering is already
+ // false that probe would also be false. When both are on, no setting change can
+ // enable filtering, so there's no actionable warning.
+ var imFiltering = settings.TransitionSettings.IonMobilityFiltering;
+ var existingCalc = imFiltering?.FilterWindowWidthCalculator;
+ bool hasWindow = HasUsableWindow(existingCalc);
+ bool hasSpectralLib = imFiltering?.UseSpectralLibraryIonMobilityValues ?? false;
+ if (hasWindow && hasSpectralLib)
+ {
+ return;
+ }
+
+ // Prefer the user's existing window if one is both selected and usable - probing with
+ // their real values keeps the "would this change fix it?" test faithful instead of
+ // substituting an arbitrary resolving power. Fall back to a default probe if no
+ // window type is set OR the selected type has a zero-valued primary field
+ // (e.g., fixed_width=0, linear_range widths both 0), which would produce a
+ // zero-width filter and make the probe a silent no-op.
+ var probeWindow = hasWindow
+ ? existingCalc
+ : new IonMobilityWindowWidthCalculator(PROBE_RESOLVING_POWER);
+ SrmSettings WithWindow(SrmSettings s) => s.ChangeTransitionSettings(
+ s.TransitionSettings.ChangeIonMobilityFiltering(
+ s.TransitionSettings.IonMobilityFiltering.ChangeFilterWindowWidthCalculator(probeWindow)));
+ SrmSettings WithSpectralLib(SrmSettings s) => s.ChangeTransitionSettings(
+ s.TransitionSettings.ChangeIonMobilityFiltering(
+ s.TransitionSettings.IonMobilityFiltering.ChangeUseSpectralLibraryIonMobilityValues(true)));
+
+ string message = null;
+ if (!hasWindow && WouldWith(WithWindow(settings)))
+ {
+ // Window alone is enough: IMSDB or explicit IM is present.
+ message = ResultsResources.SpectrumFilter_IonMobilityFilteringDisabled_SetWindow;
+ }
+ else if (!hasSpectralLib && WouldWith(WithSpectralLib(settings)))
+ {
+ // Enabling spectral-lib IM alone is enough: window is already configured.
+ message = ResultsResources.SpectrumFilter_IonMobilityFilteringDisabled_EnableSpectralLibrary;
+ }
+ else if (!hasWindow && !hasSpectralLib && WouldWith(WithSpectralLib(WithWindow(settings))))
+ {
+ // Need both: no IMSDB or explicit IM, but spectral-lib IM is usable once enabled and a window is set.
+ message = ResultsResources.SpectrumFilter_IonMobilityFilteringDisabled_SetWindowAndEnableSpectralLibrary;
+ }
+ // else: no setting change would enable filtering, so there's no actionable warning.
+
+ if (message != null)
+ {
+ var fileName = msDataFileUri?.GetFileName() ?? string.Empty;
+ Messages.WriteAsyncUserMessage(message, fileName);
+ }
+ }
+
+ // A calculator is "usable" as a probe only if its active mode has a non-zero primary
+ // field. A mode-selected-but-all-zero calculator produces a zero-width filter that makes
+ // the WithWindow probe a silent no-op, which would incorrectly suppress the warning.
+ private static bool HasUsableWindow(IonMobilityWindowWidthCalculator calc)
+ {
+ if (calc == null)
+ {
+ return false;
+ }
+ switch (calc.WindowWidthMode)
+ {
+ case IonMobilityWindowWidthCalculator.IonMobilityWindowWidthType.resolving_power:
+ return calc.ResolvingPower > 0;
+ case IonMobilityWindowWidthCalculator.IonMobilityWindowWidthType.fixed_width:
+ return calc.FixedWindowWidth > 0;
+ case IonMobilityWindowWidthCalculator.IonMobilityWindowWidthType.linear_range:
+ return calc.PeakWidthAtIonMobilityValueZero > 0 ||
+ calc.PeakWidthAtIonMobilityValueMax > 0;
+ default:
+ return false;
+ }
+ }
+
public bool ProvidesCollisionalCrossSectionConverter { get { return _ionMobilityFunctionsProvider != null; } }
public eIonMobilityUnits IonMobilityUnits
diff --git a/pwiz_tools/Skyline/TestFunctional/CrosslinkImsTest.cs b/pwiz_tools/Skyline/TestFunctional/CrosslinkImsTest.cs
index 438be620529..3b2ae95b219 100644
--- a/pwiz_tools/Skyline/TestFunctional/CrosslinkImsTest.cs
+++ b/pwiz_tools/Skyline/TestFunctional/CrosslinkImsTest.cs
@@ -18,8 +18,11 @@
*/
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using pwiz.Skyline.Controls;
using pwiz.Skyline.Model;
+using pwiz.Skyline.Model.DocSettings;
using pwiz.Skyline.Model.Lib;
+using pwiz.Skyline.Model.Results;
using pwiz.Skyline.SettingsUI;
using pwiz.Skyline.SettingsUI.IonMobility;
using pwiz.SkylineTestUtil;
@@ -98,6 +101,39 @@ protected override void DoTest()
ionMobilityLibraryDlg.OkDialog();
});
OkDialog(transitionSettingsUi, transitionSettingsUi.OkDialog);
+
+ VerifyIonMobilityFilteringDisabledWarning();
+ }
+
+ // Issue 4150: when re-importing data while the IM filter window is disabled but IM values
+ // remain available (here from the IMSDB created earlier in this test), Skyline should
+ // emit a non-blocking informational message to the Immediate Window naming the specific
+ // fix - in this case, setting a filter window width.
+ private void VerifyIonMobilityFilteringDisabledWarning()
+ {
+ const string fileName = "CrosslinkImsTest.mzML";
+
+ RunUI(() => SkylineWindow.ModifyDocument("Disable IM filtering window", doc =>
+ doc.ChangeSettings(doc.Settings.ChangeTransitionSettings(
+ doc.Settings.TransitionSettings.ChangeIonMobilityFiltering(
+ doc.Settings.TransitionSettings.IonMobilityFiltering.ChangeFilterWindowWidthCalculator(
+ IonMobilityWindowWidthCalculator.EMPTY))))));
+ RunUI(() => SkylineWindow.ModifyDocument("Remove results",
+ doc => doc.ChangeMeasuredResults(null)));
+ // Save so the existing .skyd is rewritten and re-import doesn't trigger a confirmation dialog
+ RunUI(() => SkylineWindow.SaveDocument());
+
+ ImportResultsFile(TestFilesDir.GetTestPath(fileName));
+
+ TryWaitForOpenForm();
+ var immediateWindow = FindOpenForm();
+ Assert.IsNotNull(immediateWindow, @"Immediate window should be shown");
+
+ // The IMSDB is populated at this point, so setting a filter window alone would enable
+ // filtering. The diagnostic should pick the SetWindow-specific message, not a generic one.
+ var expected = string.Format(
+ ResultsResources.SpectrumFilter_IonMobilityFilteringDisabled_SetWindow, fileName);
+ RunUI(() => StringAssert.Contains(immediateWindow.TextContent, expected));
}
}
}