Skip to content

Add unit tests for I3Calorimetry via a committed, public i3 test fixture #905

Description

@sevmag

Motivation

I3Calorimetry — and the I3Extractor helper methods it relies on — currently has zero behavioral test coverage. The only extractor test, tests/data/test_i3extractor.py, smoke-tests the constructors of three other extractors (I3FeatureExtractorIceCube86, I3TruthExtractor, I3RetroExtractor). I3Calorimetry is never instantiated, and none of total_track_energy / total_cascade_energy / filter_track_list / get_primaries / check_primary_energy / split_mc_tree / find_in_ice_daughters / get_all_parents is ever exercised.

This is especially pressing in light of #866, which revamps I3Calorimetry into intricate, order-dependent logic with many subtle edge cases (see PR #866 discussion)

Proposed solution

Commit a small, hand-seeded i3 fixture that carries no IceCube-internal data, so it is safe to ship and run in CI:

  • GCD taken from GraphNeT's own public test data;
  • PROPOSAL run with open cross-section tables;
  • a fixed RNG seed → byte-reproducible regeneration;
  • generated inside the GraphNeT icetray Docker image.

Currently untested code

Branches/line numbers are against the #866 revision. The precondition assert hasattr(self, "mctree") guards in the I3Extractor methods are omitted below.

i3calorimetry.pyI3Calorimetry

  • frame_contains_info: returns True (both keys); False when mctree absent; False when mmctracklist absent (short-circuit)
  • __call__: split-tree particle-count assertion — pass, and the ValueError("Split mctree has different number of particles…") path
  • __call__: len(target_tree) > 0 vs == 0 (the e_track/e_cascade default-to-0.0 branches); same for bkg_tree
  • __call__: target track sanity check ValueError("Energy deposited in target is greater than primary energy")
  • __call__: target total check — pass via relative tolerance, pass via the < 0.5 absolute slack, and the ValueError("Total energy is greater than primary energy")
  • __call__: background total check — same three branches ("Total background energy is greater than primary energy")
  • __call__: e_total == 0.0"No energy deposited in the hull" warning; vs != 0 no-warning
  • __call__: fraction zero-guards — fraction_target_total, target_cascade_fraction, and fraction_cascade_total (the e_total == 0 branch must not UnboundLocalError on cascade_fraction_tot)
  • __call__: target_primaries_energy > 0 → numeric fraction_primary vs <= 0None
  • __call__: output-dict assembly (keys suffixed with _extractor_name; e_*_total = target + bkg) and the _exclude filter
  • total_track_energy: is_corsika=True (unfiltered MMCTrackList) vs False (filter_track_list)
  • total_track_energy: empty harvest → 0; harvested track id present → proceed; id absent (get_particle RuntimeError) → continue
  • total_track_energy: hull-intersection — pass; first NaN; first >= particle.length; second NaN; second <= 0; starting-track first < 0 passes
  • total_track_energy: get_energy succeeds; the "sum of losses is smaller than energy at last checkpoint" RuntimeError → warn + continue; any other RuntimeError → re-raise
  • total_track_energy: entrance_energy=True (+= e0, erase whole subtree) vs False (+= e0 - e1)
  • total_track_energy: deposited-mode erase — via e1 == 0, via intersections.second < particle.length, and the no-erase branch (track stops in-volume, descendants kept)
  • total_cascade_energy: empty primaries → 0; non-leaf → extend queue; leaf is_track skip; leaf shape == Dark skip; leaf collected
  • total_cascade_energy: no collectible leaves → 0; NaN length0; cascade-in-hull counted; cascade-out-of-hull excluded; non-cascade leaf excluded by the cascade_bool mask
  • filter_track_list: keep (id in tree); "particleID not found"continue; other RuntimeError → re-raise; return type is I3MMCTrackList

i3extractor.pyI3Extractor

  • check_primary_energy: list of finite-energy primaries (append path); list with a NaN-energy primary that has daughters (flatten/extend path); single finite particle returned as-is; single NaN-energy + daughters → warn + return daughters; single NaN-energy + no daughters → ValueError; the two defensive invalid-type ValueErrors
  • get_primaries: CORSIKA (all primaries, args ignored); non-CORSIKA daughters=False, highest=True (argmax); highest=False (all); daughters=True with a top-level in-ice neutrino (filter); daughters=True with no top-level in-ice neutrino → find_in_ice_daughters recursion; recursion still empty → "No in-ice daughter…" warning + empty return; daughters=True, highest=False (keep all in-ice)
  • find_in_ice_daughters: empty input → []; InIce particle appended; non-InIce → recurse into daughters; non-InIce subtree with no InIce descendant → []
  • split_mc_tree: CORSIKA early-return (empty target, full bkg); highest=True neutrino primary in lineage (erased from bkg) vs not-in-lineage (erased from main) vs non-neutrino (erased from main); highest=False neutrino primary (erased from bkg) vs non-neutrino (erased from main)
  • split_mc_tree: latent bughighest=True with no in-ice neutrino daughters → np.argmax on an empty array raises (no guard)
  • get_all_parents: no parent → []; multi-level lineage walk

i3highesteparticleextractor.pyI3HighestEparticleExtractor

Not directly tested, and its correctness is unverified-by-dependency: get_tracks, highest_energy_starting, highest_energy_track/highest_energy_bundle all consume the untested get_primaries / check_primary_energy / find_in_ice_daughters. (Note it never calls split_mc_tree or get_all_parents, and always uses highest_energy_primary=True, so those remain reachable only via direct unit tests of I3Extractor.)

What a fixture of 8 events covers

Fixture event Key branches exercised
through_going_muon non-CORSIKA total_track_energy + filter_track_list keep; intersection passes; deposited erase via second < length; in/out-of-hull cascades; top-level in-ice-ν get_primaries; non-CORSIKA split_mc_tree
stopping_track_contained deposited erase via e1 == 0; in-hull cascade
starting_track starting-track first < 0 passes the entrance check
tau_to_mu_decay track not erased (decays in-volume); NaN-length handling
nan_primary_energy check_primary_energy NaN → daughters fallback
non_top_level_in_ice_nu get_primaries recursion + find_in_ice_daughters
no_descendants_reach_hull e_total == 0 warning; fraction zero-guards; out-of-hull cascade; empty MMCTrackList
corsika_muon_bundle is_corsika get_primaries/split_mc_tree/total_track_energy (unfiltered); empty target tree; fraction_primary = None; background path

Residual gaps

  • entrance_energy=True paths (entrance accumulation + subtree erase)
  • highest_energy_primary=True paths — get_primaries argmax, split_mc_tree lineage block, get_all_parents (need a multi-/stacked-primary event)
  • filter_track_list "particleID not found" skip + re-raise (needs a split where a track's particle lives in the other tree)
  • total_track_energy "sum of losses…" RuntimeError path (the known MuonGun issue; hard to hand-seed)
  • genuine tau double-bang (non-µ decay, two in-hull cascades; tau_to_mu_decay deliberately targets the ~17% µ channel)
  • multiple neutrino primaries / non-neutrino primary in a non-CORSIKA split_mc_tree (mixed tree)
  • get_primaries "No in-ice daughter of primary neutrino found" warning + empty return
  • the defensive ValueErrors (energy-exceeds-primary, split-count mismatch, NaN-with-no-daughters, invalid-type guards)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions