Skip to content

fix(par-dsp): handle peaks with no candidate events in evtsel - #51

Open
ggmarshall wants to merge 1 commit into
perf-par-dspfrom
fix/evtsel-zero-event-peaks
Open

fix(par-dsp): handle peaks with no candidate events in evtsel#51
ggmarshall wants to merge 1 commit into
perf-par-dspfrom
fix/evtsel-zero-event-peaks

Conversation

@ggmarshall

Copy link
Copy Markdown
Contributor

Problem

The p18/r000 production died at build_pars_evtsel_geds for channel V01404A:

RuntimeError: raw_in was not a file name, Table, or LH5Iterator: None
  legenddataflowscripts/par/geds/dsp/evtsel.py:407 in par_geds_dsp_evtsel
  dspeed/build_dsp.py:189 in build_dsp

V01404A has a hard DAQ trigger threshold: its minimum daqenergy is exactly 1501
across all 49 cal files of p18/r000, and the same floor holds in every p18 run
r000–r006. With the raw cal curve a = 1.01167 that is a ~1518 keV threshold, so
the 583.191, 727.33 and 860.564 keV peaks have zero candidate events, while 1620.5
(7315) and 2614.553 (12112) are normal. It is the only channel of 59 with this
signature.

With an empty mask the len(idx_i[0]) > 0 guard skips the read, so peak_dict["obj_buf"]
stays at its None initialiser. On the last raw file the flush fires unconditionally
(... or file == raw_files[-1]) and the first-flush branch had no None/empty
guard — unlike the second-flush branch, which already checked
obj_buf is not None and len(obj_buf) > 0. So None reached build_dsp.

This is a pre-existing latent bug, not a regression from the perf work on this branch.

Changes

  • evtsel.py — extract the pk_dicts construction into build_peak_dicts, which
    drops peaks with no candidate events and logs a warning naming each one. Add a
    defensive obj_buf is None or len(...) == 0 guard on the flush so None can never
    reach build_dsp regardless of how the buffer got there; the now-redundant guard on
    the second branch collapses to else.
  • eopt.py, dplms.py — fail fast when a peak the config asks for is absent from
    the peak file. Both filter the peak column with np.isin and never checked
    membership. A missing peak degraded silently: in eopt.py an empty slice makes
    flat_val NaN, both clamps are skipped because nan < 1 and nan > 4 are each
    False, and "nan*us" is injected into the cusp/zac/etrap filter parameters — or it
    blew up later with an IndexError on an empty table.
  • cfgtools.py — new require_peaks_present helper next to require_config_keys,
    same "list everything that is missing" style.
  • tests/test_peak_selection.py — 7 tests covering the skip, the logging and the
    helper.

Behaviour is unchanged for any channel with at least one candidate event in each peak
window.

Verification

  • pytest: 71 passed.
  • pre-commit: clean.
  • Reinstalled into the production apptainer venv and reran the real failing job through
    snakemake: exit 0, no traceback, three skipping this peak warnings, and the peak
    file now holds {1620: 5797, 2614: 8948}.
  • The 2614.553 and 1620.5 energy limits are byte-identical to the crashed run
    (1748.7557954482058 / 1844.971441718002 and 1099.76778580735 / 1147.7177982685862),
    confirming no numerical change for peaks that do have data.

Separate pre-existing issue, not fixed here

inputs/dataprod/config/par/geds/dsp/eopt/l200-p01-r%-T%-ICPC-dsp_full_eopt.yaml
requests peak 238.632, which the evtsel peak config never writes. That config will now
fail loudly rather than silently producing nan-filled optimiser output. No shipped p18
config is affected — p18 uses dsp_eopt.yaml, which requests only 2614.553.

Notes

Based on perf-par-dsp (#44) rather than main, since cfgtools.py does not exist on
main yet and the touched files are identical on this branch.

Per AI_POLICY.md: this change was drafted with AI assistance (Claude) and reviewed by
the submitter.

A channel whose DAQ trigger threshold sits above a configured gamma line
yields an empty rough-energy mask for that peak. Nothing is ever read into
the peak's buffer, so `obj_buf` stays at its `None` initialiser, and the
end-of-file flush (`... or file == raw_files[-1]`) then passed `None`
straight to `build_dsp`:

    RuntimeError: raw_in was not a file name, Table, or LH5Iterator: None

The second-flush branch already guarded on `obj_buf is not None and
len(obj_buf) > 0`; the first-flush branch did not.

Seen in production on l200-p18-r000 channel V01404A, whose minimum
daqenergy is 1501 across all 49 cal files (~1518 keV after the raw cal
curve), so the 583.191, 727.33 and 860.564 keV peaks have zero candidates
while 1620.5 and 2614.553 are normal.

Extract the pk_dicts construction into `build_peak_dicts`, which skips
peaks with no candidate events and warns, and add a defensive None/empty
guard on the flush so `None` can never reach `build_dsp` regardless. Peaks
with at least one candidate event are unaffected.

Also fail fast in the peak-file consumers: eopt and dplms filter on the
`peak` column with `np.isin` and never checked that the peaks their config
requests are present. A missing peak degraded silently -- in eopt an empty
slice makes `flat_val` NaN, both clamps are skipped because `nan < 1` and
`nan > 4` are each False, and `"nan*us"` is injected into the cusp/zac/etrap
filter parameters -- or blew up later with an IndexError. New
`require_peaks_present` helper alongside `require_config_keys`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 7, 2026 08:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the par-geds DSP calibration pipeline against “configured peak has zero candidate events” cases by skipping such peaks during event selection and by failing fast when downstream consumers request peaks that are absent from the peak file.

Changes:

  • Add build_peak_dicts to drop peaks with no candidate events (and warn), and guard the evtsel flush so None/empty buffers never reach dspeed.build_dsp.
  • Add require_peaks_present config/IO validation helper and use it in eopt.py and dplms.py to error out when requested peaks are missing.
  • Add unit tests covering peak skipping, logging, and missing-peak validation behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/test_peak_selection.py New tests for skipping empty peaks, warning logs, and require_peaks_present error semantics.
src/legenddataflowscripts/utils/cfgtools.py Adds require_peaks_present helper for validating required peak labels exist in a peak file.
src/legenddataflowscripts/utils/init.py Re-exports require_peaks_present from legenddataflowscripts.utils.
src/legenddataflowscripts/par/geds/dsp/evtsel.py Introduces build_peak_dicts and adds a defensive flush guard to prevent None buffers from being processed.
src/legenddataflowscripts/par/geds/dsp/eopt.py Validates peak presence before filtering/optimisation to avoid silent NaNs or late failures.
src/legenddataflowscripts/par/geds/dsp/dplms.py Validates peak presence before filtering/processing to avoid silent empty selections.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants