Handle all-NaN channels in calc_adc_params (fixes #485)#571
Open
Leonard013 wants to merge 1 commit into
Open
Conversation
Writing a channel that is entirely NaN raised "ValueError: cannot convert float NaN to integer" and emitted an unsuppressed "All-NaN slice encountered" RuntimeWarning. - calc_adc_gain_baseline: `if pmin == np.nan` is always False, so the intended all-NaN branch never ran and NaN reached int(np.floor(...)). Use np.isnan(pmin) so the branch (adc_gain=1, baseline=1) executes. - calc_adc_params: scope np.nanmin/np.nanmax in warnings.catch_warnings() to silence the all-NaN RuntimeWarning (honors the existing "Should suppress" comment). np.errstate does not suppress this warning. Every NaN sample already maps to the format's reserved invalid-sample value on write and back to NaN on read, so an all-NaN channel round-trips to all-NaN, consistent with partial-NaN handling. Adds regression tests test_all_nan_single_channel and test_all_nan_mixed_channels. Fixes MIT-LCP#485. This change was written with the assistance of Claude (Anthropic). Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #485.
Why
Writing a signal whose channel is entirely NaN crashes:
Two root causes in
wfdb/io/_signal.py, both noted in #485:calc_adc_gain_baselinehasif pmin == np.nan:which is always False (NaN == NaNis False). For anall-NaN channel
pmin/pmaxare NaN, so execution falls through to the"regular varied signal" branch, computes a NaN
baseline, and dies atbaseline = int(np.floor(baseline))withValueError: cannot convert float NaN to integer.calc_adc_paramscallsnp.nanmin/np.nanmaxonthe all-NaN column, emitting
RuntimeWarning: All-NaN slice encountered. Theexisting
# Should suppress warning message.comment shows this was alreadyintended to be silenced.
What
calc_adc_gain_baseline:if pmin == np.nan:→if np.isnan(pmin):, so thepre-existing all-NaN branch (which sets
adc_gain = 1,baseline = 1) actuallyexecutes.
calc_adc_params: wrap thenp.nanmin/np.nanmaxcalls inwarnings.catch_warnings()+simplefilter("ignore", RuntimeWarning), scoped tojust those two lines.
import warnings.Note on suppression method
np.errstate(...)does not silence this warning — "All-NaN slice encountered"is raised through Python's
warningsmachinery (aRuntimeWarning), not thefloating-point error state.
warnings.catch_warnings()is required. Verifiedempirically.
Why the sentinel gain/baseline is correct (NaN → invalid-value round-trip)
WFDB stores a missing/invalid physical sample as the format's reserved digital
sentinel
INVALID_SAMPLE_VALUE[fmt](e.g.-2**15 = -32768for fmt16), thelowest digital code — which is exactly why
calc_adc_gain_baselinedoesdmin = dmin + 1(reserve the bottom code for NaN).In
adc(adc_inplace), NaN positions are recorded up front and, after thegain/baseline multiply-add, overwritten with the sentinel
(
np.copyto(p_signal, d_nan, where=nanlocs)). For an all-NaN channel every sampleis a
nanloc, so gain/baseline are never reflected in the stored data — they onlyneed to be a finite gain and an in-range int baseline so the
.heais well-formed.On read,
dacmapsd_signal == d_nanback tonp.nanbefore applyinggain/baseline. Hence the round-trip is exact regardless of the sentinel
gain/baseline, and this is identical to how a partial-NaN channel's individual
NaN samples are already handled (
test_physical_conversion:p_signal[5:10]=nan↔d_signal[5:10]=-32768).Observed round-trip (fmt 16): all-NaN channel → header
adc_gain=1.0, baseline=1,every digital sample
-32768, read back as all-NaN. Mixed 2-channel signal: normalchannel round-trips to max abs err 0.0; all-NaN channel reads back all-NaN. No
warnings.
Tests
Added to
tests/test_record.py::TestSignal, following the existingtest_adc_gain_{max,min}_boundarywrite/read round-trip style:test_all_nan_single_channel— single all-NaN channel; asserts noRuntimeWarningon write and read-back is all-NaN.test_all_nan_mixed_channels— 2 channels, one all-NaN + one normal; asserts noRuntimeWarning, normal channel round-trips within quantization tolerance, NaNchannel reads back all-NaN.
Both fail before the fix (
ValueError: cannot convert float NaN to integer)and pass after. Full
tests/test_record.py,tests/test_multi_record.py,tests/test_processing.pypass.black --checkclean.Notes for reviewers
adc_gain=1, baseline=1defaults are the pre-existing all-NaN branch values(the fix just makes the branch reachable). Any finite gain / in-range integer
baseline round-trips correctly since the samples are all the reserved sentinel;
happy to switch to
baseline = 0if preferred.np.nanmin/np.nanmaxwarning could arise in thee_p_signal(expanded) branch ofcalc_adc_params. The crash is already fixedthere (that branch shares
calc_adc_gain_baseline); warning suppression was scopedto the documented
p_signalpath (the one with the# Should suppresscomment,exercised by Handle all-NaN channels in calc_adc_params #485). Can extend to the expanded path if wanted.
docs/changes.rstis batched by maintainers at release time (current4.3.1hasno unreleased section), so no changelog entry is included.
Prepared with AI assistance (Claude Code): the reproduction, root-cause analysis,
fix, and tests were AI-drafted; the reasoning and verification (including the
NaN→sentinel round-trip trace) are documented above.