Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions pwiz_tools/Skyline/Controls/Graphs/GraphFullScan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,29 @@ private void GetMaxMzIntensity(out double maxMz, out double maxIntensity)
double intensity = SumIntensities(fullScans, minMz, indices, minIonMobilityVal, maxIonMobilityVal);
maxIntensity = Math.Max(maxIntensity, intensity);
}

if (maxMz <= 0)
{
// None of the scans have any peaks in them because they measured no ions. Show the
// m/z range that the instrument was scanning so that the axes still get drawn.
maxMz = GetMaxScanWindow(fullScans);
}
}

/// <summary>
/// The highest scan window upper limit declared by any of the spectra, or zero if none of
/// them says what m/z range it was measuring.
/// </summary>
private static double GetMaxScanWindow(IEnumerable<MsDataSpectrum> spectra)
{
double maxScanWindow = 0;
foreach (var spectrum in spectra)
{
var upperLimit = spectrum.Metadata?.ScanWindowUpperLimit;
if (upperLimit.HasValue)
maxScanWindow = Math.Max(maxScanWindow, upperLimit.Value);
}
return maxScanWindow;
}

private void GetIonMobilityRange(out double minIonMobility, out double maxIonMobility)
Expand Down Expand Up @@ -2271,21 +2294,28 @@ private void ZoomXAxis()
private void ApplyXZoomToPane(GraphPane pane)
{
var xScale = pane.XAxis.Scale;
xScale.MinAuto = xScale.MaxAuto = false;

if (magnifyBtn.Checked)
{
double mz = _msDataFileScanHelper.Source == ChromSource.ms1
? _msDataFileScanHelper.ScanProvider.Transitions[_msDataFileScanHelper.TransitionIndex].PrecursorMz
: _msDataFileScanHelper.ScanProvider.Transitions[_msDataFileScanHelper.TransitionIndex].ProductMz;
xScale.MinAuto = xScale.MaxAuto = false;
xScale.Min = mz - 1.5;
xScale.Max = mz + 3.5;
}
else if (_requestedRange != null)
else if (_requestedRange != null && _requestedRange.Max > _requestedRange.Min)
{
xScale.MinAuto = xScale.MaxAuto = false;
xScale.Min = _requestedRange.Min;
xScale.Max = _requestedRange.Max;
}
else
{
// There is no m/z range to show. Leave the axis auto-scaled, since collapsing it to
// a single value makes ZedGraph skip drawing the axis entirely.
xScale.MinAuto = xScale.MaxAuto = true;
}
}

public void SetMzScale(MzRange range)
Expand Down Expand Up @@ -2458,6 +2488,13 @@ public bool ShowPropertiesSheet

public bool HasChromatogramData => false;

/// <summary>
/// The maximum for an intensity axis, which is never zero. ZedGraph draws nothing at all --
/// not even the axes -- when the minimum of any of a pane's axis scales equals its maximum,
/// so a scan which measured no ions would otherwise produce a completely blank graph.
/// </summary>
private double IntensityAxisMax => _maxIntensity > 0 ? _maxIntensity * 1.1 : 1;

private void ZoomYAxis()
{
if (_msDataFileScanHelper.ScanProvider == null || _msDataFileScanHelper.ScanProvider.Transitions.Length == 0)
Expand All @@ -2479,7 +2516,7 @@ private void ZoomYAxis()
if (isSpectrum)
{
yScale.Min = 0;
yScale.Max = _maxIntensity * 1.1;
yScale.Max = IntensityAxisMax;
if (magnifyBtn.Checked)
{
yScale.MaxAuto = true;
Expand Down Expand Up @@ -2540,11 +2577,11 @@ private void ZoomStickYAxis()
maxY = pt.Y;
}
}
yScale.Max = maxY > 0 ? maxY * 1.1 : _maxIntensity * 1.1;
yScale.Max = maxY > 0 ? maxY * 1.1 : IntensityAxisMax;
}
else
{
yScale.Max = _maxIntensity * 1.1;
yScale.Max = IntensityAxisMax;
}
_stickSpectrumPane.AxisChange();
}
Expand All @@ -2566,7 +2603,7 @@ private void ResetStickYAxis()
yScale.MinAuto = false;
_heatMapPane.LockYAxisMinAtZero = true;
yScale.Min = 0;
yScale.Max = _maxIntensity * 1.1;
yScale.Max = IntensityAxisMax;
// Magnify on → auto-fit Y to data in the zoomed X range during next paint.
yScale.MaxAuto = magnifyBtn.Checked;
}
Expand Down
22 changes: 18 additions & 4 deletions pwiz_tools/Skyline/Model/Results/SpectraChromDataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,20 @@ public static bool HasSpectrumData(MsDataFileImpl dataFile)
return dataFile.SpectrumCount > 0;
}

/// <summary>
/// Returns true if a spectrum has no m/z values and should be ignored.
/// A spectrum which measured no ions is a valid measurement of zero intensity, but only if
/// we know which m/z values it was measuring. That range comes from the scan window limits,
/// so a spectrum which declares them is not considered empty.
/// </summary>
public static bool IsEmptySpectrum(MsDataSpectrum spectrum)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this one as public for now.

SpectraChromDataProvider is declared internal sealed, so a public member on it is already scoped to this assembly rather than exposed to anything outside it - the accessibility change would be a stylistic one rather than a real narrowing of the public surface. It also matches HasSpectrumData immediately above it, which is public static in the same position for the same reason.

Beyond consistency, IsEmptySpectrum encodes a policy decision - what counts as an empty spectrum, and specifically that a scan declaring scan window limits is a valid measurement of zero rather than an absence of data. That rule is a plausible thing for other extraction code to need to share rather than restate, so I would rather not close it off yet.

Leaving the thread unresolved so a human reviewer can overrule this if they would rather keep the enclosing type's surface minimal - it is a one-word change either way.

{
if (spectrum.Mzs != null && spectrum.Mzs.Length != 0)
return false;
return spectrum.Metadata?.ScanWindowLowerLimit == null &&
spectrum.Metadata?.ScanWindowUpperLimit == null;
}

private class Spectra : IDisposable
{
private bool _runningAsync;
Expand Down Expand Up @@ -1218,7 +1232,7 @@ private SpectrumInfo ReadSpectrum(ref int i)
// Assertion for testing ID to spectrum index support
// int iFromId = dataFile.GetSpectrumIndex(dataSpectrum.Id);
// Assume.IsTrue(i == iFromId);
if (nextSpectrum.Mzs.Length == 0)
if (IsEmptySpectrum(nextSpectrum))
continue;

double? rt = nextSpectrum.RetentionTime;
Expand Down Expand Up @@ -1574,7 +1588,7 @@ public MsDataSpectrum[] Lookahead(MsDataSpectrum dataSpectrum, out double? rt)
while (_lookAheadIndex++ < _lenSpectra)
{
_rt = dataSpectrum.RetentionTime;
if (_rt.HasValue && dataSpectrum.Mzs.Length != 0)
if (_rt.HasValue && !IsEmptySpectrum(dataSpectrum))
{
spectrumList.Add(dataSpectrum);
if (!rtReported.HasValue)
Expand Down Expand Up @@ -1630,7 +1644,7 @@ public MsDataSpectrum[] Lookahead(MsDataSpectrum dataSpectrum, out double? rt)
while (_lookAheadIndex++ < _lenSpectra)
{
_rt = dataSpectrum.RetentionTime;
if (_rt.HasValue && dataSpectrum.Mzs.Length != 0)
if (_rt.HasValue && !IsEmptySpectrum(dataSpectrum))
{
spectrumList.Add(dataSpectrum);
rtTotal += dataSpectrum.RetentionTime.Value;
Expand All @@ -1649,7 +1663,7 @@ public MsDataSpectrum[] Lookahead(MsDataSpectrum dataSpectrum, out double? rt)
{
// No need to search forward, this isn't IMS or Agilent ramped-CE data
rtReported = dataSpectrum.RetentionTime;
if (rtReported.HasValue && dataSpectrum.Mzs.Length != 0)
if (rtReported.HasValue && !IsEmptySpectrum(dataSpectrum))
{
spectrumList.Add(dataSpectrum);
}
Expand Down
4 changes: 4 additions & 0 deletions pwiz_tools/Skyline/Model/Results/SpectrumFilterPair.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,11 @@ private ExtractedSpectrum FilterSpectrumList(MsDataSpectrum[] spectra,
iPeak = ~iPeak;
}
if (iPeak >= mzArray.Length)
{
// The extracted intensities for the remaining targets will be zero so we can stop extracting
// Consider: we probably still need to keep checking "hasScanWindowCoverage"
break; // No further overlap
}
}

// TODO:(bspratt) for full frame diaPASEF MS2, try not sorting - make IM the initial binary search range (and deal with mz that rolls over)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ protected override void DoTest()
Assert.AreEqual(1, transitionGroup.Results.Count);
var transitionGroupChromInfo = transitionGroup.Results[0].First();
// Verify the peak area is what we expect
Assert.AreEqual(1.460189E+08f, transitionGroupChromInfo.Area.Value);
Assert.AreEqual(147639168f, transitionGroupChromInfo.Area.Value);

// Reimport the file with "UseSelectiveExtraction" set to "true"
RunUI(() => SkylineWindow.ModifyDocument("Change selective extraction",
Expand Down Expand Up @@ -87,7 +87,7 @@ protected override void DoTest()
Assert.AreEqual(1, transitionGroup.Results.Count);
transitionGroupChromInfo = transitionGroup.Results[0].First();
// Verify that the peak area is a smaller number because the chromatogram extraction was more selective
Assert.AreEqual(119880880f, transitionGroupChromInfo.Area.Value);
Assert.AreEqual(121269248f, transitionGroupChromInfo.Area.Value);
}
}
}
1 change: 1 addition & 0 deletions pwiz_tools/Skyline/TestFunctional/TestFunctional.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@
<Compile Include="UpdateGlobalStandardTest.cs" />
<Compile Include="UpgradeTest.cs" />
<Compile Include="VolcanoPlotFormattingTest.cs" />
<Compile Include="ZeroLengthSpectraTest.cs" />
<Compile Include="ZeroLengthSrmChromatogramTest.cs" />
</ItemGroup>
<ItemGroup>
Expand Down
100 changes: 100 additions & 0 deletions pwiz_tools/Skyline/TestFunctional/ZeroLengthSpectraTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Original author: Nicholas Shulman <nicksh .at. u.washington.edu>,
* MacCoss Lab, Department of Genome Sciences, UW
*
* Copyright 2026 University of Washington - Seattle, WA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using Microsoft.VisualStudio.TestTools.UnitTesting;
using pwiz.CommonMsData;
using pwiz.Skyline.Controls.Graphs;
using pwiz.Skyline.Model;
using pwiz.Skyline.Model.Results;
using pwiz.SkylineTestUtil;
using System.Linq;
using ZedGraph;

namespace pwiz.SkylineTestFunctional
{
/// <summary>
/// Verifies that spectra with no m/z's and intensities are still included in the extracted chromatogram.
/// </summary>
[TestClass]
public class ZeroLengthSpectraTest : AbstractFunctionalTestEx
{
[TestMethod]
public void TestZeroLengthSpectra()
{
TestFilesZip = @"TestFunctional\ZeroLengthSpectraTest.zip";
RunFunctionalTest();
}

protected override void DoTest()
{
RunUI(()=>SkylineWindow.OpenFile(TestFilesDir.GetTestPath("ZeroLengthSpectraTest.sky")));
var msDataFilePath = new MsDataFilePath(TestFilesDir.GetTestPath("S_1.mzML"));
ImportResultsFile(msDataFilePath.FilePath);
using var dataFile = msDataFilePath.OpenMsDataFile(new OpenMsDataFileParams());
var ms1Spectra = Enumerable.Range(0, dataFile.SpectrumCount).Select(dataFile.GetSpectrum)
.Where(spectrum => spectrum.Level == 1).ToList();
var emptyMs1Spectra = ms1Spectra.Where(spectrum => spectrum.Mzs.Length == 0).ToList();
Assert.AreNotEqual(0, emptyMs1Spectra.Count);
Assert.AreNotEqual(emptyMs1Spectra.Count, ms1Spectra.Count);
var document = SkylineWindow.Document;
var peptideDocNode = document.Molecules.First();
Assert.IsTrue(document.MeasuredResults.TryLoadChromatogram(0, peptideDocNode, peptideDocNode.TransitionGroups.First(), (float) document.Settings.TransitionSettings.Instrument.MzMatchTolerance, out var chromatogramGroupInfos));
Assert.AreEqual(1, chromatogramGroupInfos.Length);
var chromatogramInfo = chromatogramGroupInfos[0].GetRawTransitionInfo(0);
Assert.IsNotNull(chromatogramInfo);
Assert.AreEqual(ms1Spectra.Count, chromatogramInfo.Times.Count);
RunUI(() =>
{
SkylineWindow.SelectedPath =
SkylineWindow.Document.GetPathTo((int)SrmDocument.Level.TransitionGroups, 0);
SkylineWindow.SetTransformChrom(TransformChrom.raw);
});

// Click on a point in the chromatogram which came from one of the empty spectra
ClickChromatogram(22.3, 1e4);
var graphFullScan = WaitForOpenForm<GraphFullScan>();
RunUI(() =>
{
graphFullScan.SetZoom(false);
AssertAxesNotDegenerate(graphFullScan.ZedGraphControl.GraphPane);

// The empty spectrum still says which m/z values it was measuring, and the x-axis
// is supposed to show that range instead of collapsing to nothing
var scanWindowUpperLimit = emptyMs1Spectra.Max(spectrum => spectrum.Metadata.ScanWindowUpperLimit);
Assert.IsNotNull(scanWindowUpperLimit);
AssertEx.IsGreaterThanOrEqual(graphFullScan.ZedGraphControl.GraphPane.XAxis.Scale.Max,
scanWindowUpperLimit.Value);
});
}

/// <summary>
/// Asserts that the graph will actually be drawn. ZedGraph skips the axes, the grid and the
/// curves when the minimum of any of a pane's axis scales is not less than its maximum,
/// which leaves nothing but the title, so a degenerate range means a blank graph.
/// </summary>
private static void AssertAxesNotDegenerate(GraphPane graphPane)
{
AssertEx.IsGreaterThan(graphPane.XAxis.Scale.Max, graphPane.XAxis.Scale.Min);
AssertEx.IsGreaterThan(graphPane.X2Axis.Scale.Max, graphPane.X2Axis.Scale.Min);
foreach (var yAxis in graphPane.YAxisList)
AssertEx.IsGreaterThan(yAxis.Scale.Max, yAxis.Scale.Min);
foreach (var y2Axis in graphPane.Y2AxisList)
AssertEx.IsGreaterThan(y2Axis.Scale.Max, y2Axis.Scale.Min);
}
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,24 @@
3.01
],
[
1.77,
3.02
1.76,
2.99
],
[
2.05,
2.04,
3.16
],
[
2.06,
3.66
2.08,
3.68
],
[
1.94,
1.95,
3.17
],
[
1.63,
3.36
1.64,
3.38
]
],
"DiffPeptideCounts": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,30 @@
"MassErrorStats": [
[
3.03,
4.37
4.38
],
[
2.78,
4.1
2.79,
4.12
],
[
3.88,
3.89,
4.25
],
[
5.87,
3.58
3.55
],
[
4.69,
4.17
],
[
-0.07,
3.42
-0.08,
3.43
],
[
1.01,
1.02,
3.63
]
],
Expand All @@ -47,13 +47,13 @@
"UnpolishedProteins": 9,
"PolishedProteins": 11,
"ScoringModelCoefficients": [
0.2332,
-0.667,
3.4044,
-0.0138,
-0.4653,
0.9273,
0.112,
-0.0497
0.2442,
-0.6843,
3.403,
0.0632,
-0.4762,
0.9162,
0.0914,
-0.0489
]
}
Loading