Skip to content
Closed
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
103 changes: 100 additions & 3 deletions mzLib/MassSpectrometry/WindowModeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,65 @@ public static class WindowModeHelper
public static void Run(ref double[] intensities, ref double[] mArray,
IFilteringParams filteringParams, double scanRangeMinMz, double scanRangeMaxMz, bool keepZeroPeaks = false)
{
Array.Sort(intensities, mArray);
// Backward-compatible thin wrapper: callers without a per-peak charge array
// continue to use the original 5-parameter signature unchanged. The 6-parameter
// overload below carries an optional `ref int[] chargeArray` so reader paths
// that loaded a per-peak charge array (PSI-MS MS:1000516) can keep it in sync
// with the windowing/filtering's reordering and pruning of mArray/intensities.
int[] noChargeArray = null;
Run(ref intensities, ref mArray, ref noChargeArray, filteringParams,
scanRangeMinMz, scanRangeMaxMz, keepZeroPeaks);
}

/// <summary>
/// Charge-array-aware overload. When <paramref name="chargeArray"/> is non-null and
/// its length matches <paramref name="mArray"/>, the same selection / reordering /
/// pruning that the windowing applies to mz+intensity is also applied to charges,
/// preserving per-peak alignment. Pass <c>null</c> (or a length-mismatched array)
/// to get the original behavior unchanged.
/// </summary>
public static void Run(ref double[] intensities, ref double[] mArray, ref int[] chargeArray,
IFilteringParams filteringParams, double scanRangeMinMz, double scanRangeMaxMz, bool keepZeroPeaks = false)
{
// Charges only follow the selection if the caller provided a length-aligned
// array. Mismatched lengths are ambiguous and would corrupt alignment further;
// drop the array in that case so the caller sees an honest "no charge data".
bool hasCharges = chargeArray != null && chargeArray.Length == mArray.Length;
if (chargeArray != null && !hasCharges)
{
chargeArray = null;
}

if (hasCharges)
{
// Index-permutation sort: Array.Sort handles 2 parallel arrays at most;
// for 3 parallel arrays we sort an index array by the intensity key and
// then materialize all three in the new order. Locals are needed because
// C# disallows capturing `ref` parameters inside the lambda comparator.
double[] intLocal = intensities;
double[] mzLocal = mArray;
int[] chgLocal = chargeArray;
int n = intLocal.Length;
int[] perm = new int[n];
for (int i = 0; i < n; i++) perm[i] = i;
Array.Sort(perm, (a, b) => intLocal[a].CompareTo(intLocal[b]));
var newInt = new double[n];
var newMz = new double[n];
var newChg = new int[n];
for (int i = 0; i < n; i++)
{
newInt[i] = intLocal[perm[i]];
newMz[i] = mzLocal[perm[i]];
newChg[i] = chgLocal[perm[i]];
}
intensities = newInt;
mArray = newMz;
chargeArray = newChg;
}
else
{
Array.Sort(intensities, mArray);
}
double TIC = intensities.Sum();

//filter low intensites based on a percent for the whole spectrum.
Expand Down Expand Up @@ -138,13 +196,20 @@ public static void Run(ref double[] intensities, ref double[] mArray,

List<double> reducedMzList = new List<double>();
List<double> reducedIntensityList = new List<double>();
// Parallel reduced charge list — only populated when the caller passed a
// length-aligned chargeArray. Indices into mArray/intensities are stored in
// mzInRange[rangeIndex], so charges follow the same selection by indexing
// into the (already-permuted) chargeArray with arrayIndex.
List<int> reducedChargeList = hasCharges ? new List<int>() : null;

foreach (int rangeIndex in mzInRange.Keys)
{
List<double> tempMzList = new List<double>();
List<int> tempChargeList = hasCharges ? new List<int>() : null;
foreach (int arrayIndex in mzInRange[rangeIndex])
{
tempMzList.Add(mArray[arrayIndex]);
if (hasCharges) tempChargeList.Add(chargeArray[arrayIndex]);
}
//There is no need to do any normalization unless there are multiple windows
if (filteringParams.NormalizePeaksAcrossAllWindows)
Expand All @@ -169,14 +234,46 @@ public static void Run(ref double[] intensities, ref double[] mArray,

if (tempMzList.Count > 0 && mzRangeIntensities[rangeIndex].Count > 0)
{
reducedMzList.AddRange(tempMzList.GetRange(0, Math.Min(tempMzList.Count, countOfPeaksToKeepPerWindow)));
int takeCount = Math.Min(tempMzList.Count, countOfPeaksToKeepPerWindow);
reducedMzList.AddRange(tempMzList.GetRange(0, takeCount));
reducedIntensityList.AddRange(mzRangeIntensities[rangeIndex].GetRange(0, Math.Min(mzRangeIntensities[rangeIndex].Count, countOfPeaksToKeepPerWindow)));
if (hasCharges)
reducedChargeList.AddRange(tempChargeList.GetRange(0, takeCount));
}
}

intensities = reducedIntensityList.ToArray();
mArray = reducedMzList.ToArray();
Array.Sort(mArray, intensities);
if (hasCharges)
{
// Final m/z sort must permute charges in lockstep — repeat the
// index-permutation idiom from the top of the method. Locals are needed
// because C# disallows capturing `ref` parameters inside the lambda.
chargeArray = reducedChargeList.ToArray();
double[] mzLocal = mArray;
double[] intLocal = intensities;
int[] chgLocal = chargeArray;
int n = mzLocal.Length;
int[] perm = new int[n];
for (int i = 0; i < n; i++) perm[i] = i;
Array.Sort(perm, (a, b) => mzLocal[a].CompareTo(mzLocal[b]));
var newMz = new double[n];
var newInt = new double[n];
var newChg = new int[n];
for (int i = 0; i < n; i++)
{
newMz[i] = mzLocal[perm[i]];
newInt[i] = intLocal[perm[i]];
newChg[i] = chgLocal[perm[i]];
}
mArray = newMz;
intensities = newInt;
chargeArray = newChg;
}
else
{
Array.Sort(mArray, intensities);
}
}
}
}
114 changes: 98 additions & 16 deletions mzLib/Readers/MzML/Mzml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -682,25 +682,50 @@ public override MsDataScan GetOneBasedScanFromDynamicConnection(int oneBasedScan
double zeroEquivalentIntensity = 0.01;
int zeroIntensityCount = intensities.Count(i => i < zeroEquivalentIntensity);
int intensityValueCount = intensities.Count();
// Mirror the static-reader path's charge-alignment policy: the per-peak
// chargeArray (if loaded) must follow every reorder/trim of mzs/intensities,
// otherwise charges silently misalign with their peaks. A length-mismatched
// chargeArray is treated as ambiguous and dropped.
bool hasCharges = chargeArray != null && chargeArray.Length == intensityValueCount;
if (chargeArray != null && !hasCharges) chargeArray = null;
if (zeroIntensityCount > 0 && zeroIntensityCount < intensityValueCount)
{
Array.Sort(intensities, mzs);
double[] nonZeroIntensities = new double[intensityValueCount - zeroIntensityCount];
double[] nonZeroMzs = new double[intensityValueCount - zeroIntensityCount];
intensities = intensities.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
mzs = mzs.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
Array.Sort(mzs, intensities);
if (hasCharges)
{
SortInPlaceByKey(intensities, mzs, chargeArray);
intensities = intensities.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
mzs = mzs.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
chargeArray = chargeArray.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
SortInPlaceByKey(mzs, intensities, chargeArray);
}
else
{
Array.Sort(intensities, mzs);
intensities = intensities.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
mzs = mzs.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
Array.Sort(mzs, intensities);
}
}


// peak filtering
if (filterParams != null && intensities.Length > 0 &&
((filterParams.ApplyTrimmingToMs1 && msOrder.Value == 1) || (filterParams.ApplyTrimmingToMsMs && msOrder.Value == 2) || (filterParams.ApplyTrimmingToMsN && msOrder.Value > 2)))
{
WindowModeHelper.Run(ref intensities, ref mzs, filterParams, scanLowerLimit, scanUpperLimit);
// Charge-aware overload — keeps charges aligned through the
// window selection / pruning / final sort that WindowModeHelper does.
WindowModeHelper.Run(ref intensities, ref mzs, ref chargeArray, filterParams, scanLowerLimit, scanUpperLimit);
}

Array.Sort(mzs, intensities);
if (hasCharges && chargeArray != null && chargeArray.Length == mzs.Length)
{
SortInPlaceByKey(mzs, intensities, chargeArray);
}
else
{
chargeArray = null;
Array.Sort(mzs, intensities);
}

range = new MzRange(scanLowerLimit, scanUpperLimit);
spectrum = new MzSpectrum(mzs, intensities, false);
Expand Down Expand Up @@ -951,22 +976,51 @@ private static MsDataScan GetMsDataOneBasedScanFromConnection(Generated.mzMLType
double zeroEquivalentIntensity = 0.01;
int zeroIntensityCount = intensities.Count(i => i < zeroEquivalentIntensity);
int intensityValueCount = intensities.Count();
// Charge alignment policy: if the mzML included a per-peak chargeArray, every
// reorder/trim below has to be applied to it in lockstep, otherwise charges end
// up pointing at the wrong peaks (lengths can still match while values misalign,
// so the reader's existing length-only policy doesn't catch this). When the
// captured chargeArray length doesn't match the peak count, drop it — preserving
// a misaligned array would corrupt downstream consumers.
bool hasCharges = chargeArray != null && chargeArray.Length == intensityValueCount;
if (chargeArray != null && !hasCharges) chargeArray = null;
if (zeroIntensityCount > 0 && zeroIntensityCount < intensityValueCount)
{
Array.Sort(intensities, masses);
double[] nonZeroIntensities = new double[intensityValueCount - zeroIntensityCount];
double[] nonZeroMzs = new double[intensityValueCount - zeroIntensityCount];
intensities = intensities.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
masses = masses.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
Array.Sort(masses, intensities);
if (hasCharges)
{
SortInPlaceByKey(intensities, masses, chargeArray);
intensities = intensities.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
masses = masses.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
chargeArray = chargeArray.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
SortInPlaceByKey(masses, intensities, chargeArray);
}
else
{
Array.Sort(intensities, masses);
intensities = intensities.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
masses = masses.SubArray(zeroIntensityCount, intensityValueCount - zeroIntensityCount);
Array.Sort(masses, intensities);
}
}

if (filterParams != null && intensities.Length > 0 && ((filterParams.ApplyTrimmingToMs1 && msOrder.Value == 1) || (filterParams.ApplyTrimmingToMsMs && msOrder.Value == 2) || (filterParams.ApplyTrimmingToMsN && msOrder.Value > 2)))
{
WindowModeHelper.Run(ref intensities, ref masses, filterParams, low, high);
// Charge-aware overload: WindowModeHelper carries the array through its
// own sort/select/reduce when it's non-null and length-aligned.
WindowModeHelper.Run(ref intensities, ref masses, ref chargeArray, filterParams, low, high);
}

Array.Sort(masses, intensities);
if (hasCharges && chargeArray != null && chargeArray.Length == masses.Length)
{
SortInPlaceByKey(masses, intensities, chargeArray);
}
else
{
// chargeArray went null at some point above (length mismatch after a
// filter), or there never was one — fall through to the original 2-array sort.
chargeArray = null;
Array.Sort(masses, intensities);
}
var mzmlMzSpectrum = new MzSpectrum(masses, intensities, false);

if (msOrder.Value == 1)
Expand Down Expand Up @@ -1108,6 +1162,34 @@ private static MsDataScan GetMsDataOneBasedScanFromConnection(Generated.mzMLType
);
}

/// <summary>
/// In-place sort of three parallel arrays (key, valuesA, valuesB) by ascending key.
/// <see cref="Array.Sort(Array, Array)"/> only handles two parallel arrays at a time;
/// this helper uses an index-permutation pass so the per-peak m/z, intensity, and
/// charge arrays stay aligned through every reorder/trim point in the reader paths.
/// </summary>
private static void SortInPlaceByKey(double[] keys, double[] valuesA, int[] valuesB)
{
int n = keys.Length;
if (valuesA.Length != n || valuesB.Length != n)
throw new ArgumentException("SortInPlaceByKey: parallel arrays must have equal length.");
int[] perm = new int[n];
for (int i = 0; i < n; i++) perm[i] = i;
Array.Sort(perm, (a, b) => keys[a].CompareTo(keys[b]));
var newKeys = new double[n];
var newA = new double[n];
var newB = new int[n];
for (int i = 0; i < n; i++)
{
newKeys[i] = keys[perm[i]];
newA[i] = valuesA[perm[i]];
newB[i] = valuesB[perm[i]];
}
Array.Copy(newKeys, keys, n);
Array.Copy(newA, valuesA, n);
Array.Copy(newB, valuesB, n);
}

/// <summary>
/// Converts a 64-based encoded byte array into an double[]
/// </summary>
Expand Down
Loading
Loading