Keep MS:1000516 charge array aligned through reader reorder/trim points#1062
Keep MS:1000516 charge array aligned through reader reorder/trim points#1062pcarvalho75 wants to merge 2 commits into
Conversation
Follow-up to smith-chem-wisc#1060. The chargeArray was loaded in source order and passed straight to the MsDataScan constructor, but the reader paths reorder/trim mzs and intensities in three places without touching chargeArray: 1. Zero-intensity peak removal (sort-by-intensity + SubArray + sort- by-mz) in both GetMsDataOneBasedScanFromConnection (static) and GetOneBasedScanFromDynamicConnection (dynamic). 2. Optional WindowModeHelper.Run pruning when FilteringParams is set. 3. The unconditional final Array.Sort(masses, intensities) at the end of each path. Without this fix, charges silently misalign with their peaks whenever the source mzML has any peak with intensity below 0.01, any out-of- order m/z, or filterParams non-null. Lengths can still match while values point at the wrong peaks, so the existing length-only policy doesn't catch it. Changes: * WindowModeHelper.cs: new charge-aware overload Run(ref intensities, ref mArray, ref chargeArray, ...). The existing 5-parameter signature is preserved as a thin wrapper that delegates with chargeArray = null, so the eight other callers are untouched. When chargeArray is non-null and length-matches mArray, the new overload carries it through the intensity-sort, the per-window selection, and the final m/z-sort via index permutation. Length-mismatched arrays are dropped (preserving an ambiguous one would compound the corruption this PR is meant to prevent). * Mzml.cs: both reader paths now use a small SortInPlaceByKey helper for 3-array permutation when chargeArray is present. Pass chargeArray to the new WindowModeHelper overload. Defensive length-check before each operation; chargeArray is set to null and dropped from the constructed MsDataScan if any step would leave alignment ambiguous. Tests: * ChargeArrayUnsortedMzInputAlignsAfterReaderSort - constructs an MzSpectrum with mzs in reverse order via the shouldCopy:false ctor (writer emits unsorted bytes verbatim), then asserts the post-sort (mz, charge) pairing matches the source pairing peak-for-peak. * ChargeArraySurvivesZeroIntensityFilter - source includes a peak with intensity 0.005, charge 9. After the reader's zero-intensity trim, asserts only the >0.01 peaks survive AND charges align with the surviving (mz, intensity) pairs (i.e. the dropped charge is the one paired with the dropped intensity, not just "the first one"). * ChargeArraySurvivesFilteringParamsPruning - 6 peaks, FilteringParams with NumberOfPeaksToKeepPerWindow=3, NumberOfWindows=1. Asserts the 3 most-intense peaks survive AND their charges still line up with their original mz values after WindowModeHelper's intensity-sort, per-window selection, and final m/z-sort. Verified: 6/6 ChargeArray tests pass; 103/103 in the WindowMode + SpectrumProcessing + TestMzML + Dynamic sweep. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1062 +/- ##
========================================
Coverage 81.45% 81.45%
========================================
Files 366 366
Lines 46899 47018 +119
Branches 5556 5579 +23
========================================
+ Hits 38200 38298 +98
- Misses 7620 7634 +14
- Partials 1079 1086 +7
🚀 New features to boost your workflow:
|
|
Heads-up: while finalizing this PR we kept digging and realized there is a deeper issue underneath what @trishorts caught. I have opened #1063 as a superseding PR that addresses the root cause; this one is still valid as-is if you would prefer to land the smaller fix first, and we can close this one in favor of #1063 whenever you decide. Wanted to lay out what slipped by us all in #1060 + this PR, since the same bug class exists in two more places and the structurally-correct fix is to move What this PR does (#1062)Patches three reorder/trim points across two reader paths so
That is six fix sites total, plus a new charge-aware overload on What we missed (and what #1063 fixes)The bug class is broader than the reader paths. As long as 1.
|
Closes the patch-coverage gap codecov flagged on smith-chem-wisc#1062 (84% -> ~96% expected) and on this PR by exercising the branches the happy-path tests don't reach: * ChargeArrayThroughDynamicReaderPath - mirrors ChargeArraySurvivesZeroIntensityFilter through InitiateDynamicConnection + GetOneBasedScanFromDynamicConnection. Confirms both reader paths apply the permutation/mask consistently. * ChargeArrayDroppedWhenLengthMismatchedOnDisk - writes a normal scan, then surgically appends one extra 32-bit float to the chargeArray's base64-encoded binary on disk so the decoded length is N+1 while mzs/intensities are N. Confirms the reader DROPS the misaligned chargeArray rather than shipping it (defensive branch in both reader paths). * WindowModeHelperLengthMismatchedChargeArrayDropped - direct unit test of WindowModeHelper.Run's length-mismatch null-out branch, plus that mzs/intensities still process correctly without charges. * WindowModeHelperEmptyWindowPreservesChargeAlignment - 2 windows over [0..1000], both peaks land in window 1 (window 2 is empty); asserts charges still pair correctly with mzs after the per-window selection's empty-window branch is hit in the charge-aware path. 15/15 charge tests pass; 115/115 in the broader regression sweep (TestMzML + WindowMode + SpectrumProcessing + Dynamic + XCorr + MzSpectrum + MsDataScan). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@pcarvalho75 I think the best approach will be to make one Pull request that fixes the issue. That way you do not need to go through the review process with us twice. If #1063 resolves the issues here in #1062 as well, then I recommend reviewing the failing tests in 1063 and have a single PR to resolve these issues. |
|
also note that there are labels. if you can, select the "ready for review" label to make sure that we focus on it. I really like the verbose comments in the PR. that makes my reviewing a much easier task. be sure to include xml documentation in the code for key aspects so that we can understand and respect the contract |
|
Hi.
I might be able to only submit the fix next week as I'm in an international
trip. But I will definitely submit as soon as I can.
Cheers
Paulo
…On Tue, May 12, 2026, 21:10 trishorts ***@***.***> wrote:
*trishorts* left a comment (smith-chem-wisc/mzLib#1062)
<#1062 (comment)>
also note that there are labels. if you can, select the "ready for review"
label to make sure that we focus on it. I really like the verbose comments
in the PR. that makes my reviewing a much easier task. be sure to include
xml documentation in the code for key aspects so that we can understand and
respect the contract
—
Reply to this email directly, view it on GitHub
<#1062 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AB4SFLWV7RBJGXFLZCV4N5T42O4OTAVCNFSM6AAAAACY2AICXOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DIMZVHAZTOMRZGI>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
Closing — superseded by #1063 per @nbollis's consolidation request. #1063 already absorbs the reorder/trim sync work from this PR (charge-aware |
What
Follow-up to #1060. Fix the
chargeArraypermutation-sync gap @trishorts identified in review: the per-peak charge array was being loaded in source order and passed straight to theMsDataScanconstructor, but the reader paths reorder/trim mzs and intensities in three places without touchingchargeArray:Array.Sort(intensities, mzs)+SubArray+Array.Sort(mzs, intensities)) in bothGetMsDataOneBasedScanFromConnection(static) andGetOneBasedScanFromDynamicConnection(dynamic).WindowModeHelper.Runpruning whenFilteringParamsis set.Array.Sort(masses, intensities)at the end of each path.Why
Without this fix, charges silently misalign with their peaks whenever the source mzML has any peak with intensity below
0.01, any out-of-order m/z, orfilterParamsis non-null. Lengths can still match while values point at the wrong peaks, so the reader's existing length-only policy can't catch it. Self-roundtrip tests with pre-sorted, non-zero-intensity, no-filter data wouldn't expose it — exactly how it slipped past #1060's tests.The motivating consumers (YADA, mzR, pymzML) trust that
ChargeArray[i]describes the peak atMassSpectrum.XArray[i]. Without the fix, that contract holds in source order but breaks the moment the reader reorders anything.Implementation
WindowModeHelper.csNew charge-aware overload
Run(ref intensities, ref mArray, ref chargeArray, ...). The existing 5-parameter signature is preserved as a thin wrapper that delegates withchargeArray = null, so the eight other callers (MzSpectrum, TofSpectraMerger, ThermoRawFileReader, Mgf, Bruker, plus the four existing tests) compile and behave unchanged.When
chargeArrayis non-null and length-matchesmArray, the new overload carries it through:refparameters in lambdas).mzInRange[rangeIndex]already stores indices into the sorted arrays, so charges follow trivially viachargeArray[arrayIndex].Length-mismatched arrays are dropped — preserving an ambiguous one would compound the corruption this PR is meant to prevent.
Mzml.csreader pathsBoth
GetMsDataOneBasedScanFromConnectionandGetOneBasedScanFromDynamicConnectionnow use a small privateSortInPlaceByKey(double[] keys, double[] valuesA, int[] valuesB)helper for the 3-array sorts. Each reorder/trim point gets the charge-aware branch whenchargeArrayis non-null and length-aligned, and falls back to the original 2-arrayArray.Sortwhen there's no charge data. The newWindowModeHelper.Runoverload receiveschargeArraydirectly.Defensive policy: any time the length invariant would be violated,
chargeArrayis set tonulland theMsDataScanis constructed without it. That's better than silently shipping misaligned charges.Tests
Three new regression tests in
TestMzML.cs, one per reorder/trim point:ChargeArrayUnsortedMzInputAlignsAfterReaderSortArray.Sort(masses, intensities). Constructs anMzSpectrumwith mzs in reverse order via theshouldCopy:falsector (writer emits unsorted bytes verbatim —MzSpectrum.Get64BitXarraydoes no sort), asserts the post-sort(mz, charge)pairing matches the source pairing peak-for-peak.ChargeArraySurvivesZeroIntensityFilter0.005, charge9. After the reader's trim, asserts only the>0.01peaks survive AND charges align with the surviving(mz, intensity)pairs (the dropped charge is the one paired with the dropped intensity, not just "the first one").ChargeArraySurvivesFilteringParamsPruningWindowModeHelper.Runpath. 6 peaks,FilteringParams { numberOfPeaksToKeepPerWindow: 3, numberOfWindows: 1, applyTrimmingToMs1: true }. Asserts the 3 most-intense peaks survive AND their charges still line up with their original mz values after the intensity-sort, per-window top-N selection, and final m/z-sort.Broader regression: 103/103 in the
TestMzML + WindowMode + SpectrumProcessing + Dynamicsweep.Backward compatibility
Strictly additive on the public API:
WindowModeHelper.Run— old 5-parameter signature unchanged; new 6-parameter overload added alongside.Mzml.cs— both reader paths produce identical output for any mzML without anMS:1000516charge array (thehasCharges == falsebranches are byte-equivalent to the pre-PR code).MsDataScan.ChargeArray— public type and semantics unchanged.Files changed
mzLib/MassSpectrometry/WindowModeHelper.csRunoverload; existing signature delegates.mzLib/Readers/MzML/Mzml.cschargeArrayeverywhere they touch m/z or intensities; new privateSortInPlaceByKeyhelper for 3-array sorting.mzLib/Test/FileReadingTests/SpectraFileReading/TestMzML.cscc @trishorts @nbollis — addresses the follow-up issue from the PR #1060 review thread.