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.py — I3Calorimetry
i3extractor.py — I3Extractor
i3highesteparticleextractor.py — I3HighestEparticleExtractor
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
Motivation
I3Calorimetry— and theI3Extractorhelper 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).I3Calorimetryis never instantiated, and none oftotal_track_energy/total_cascade_energy/filter_track_list/get_primaries/check_primary_energy/split_mc_tree/find_in_ice_daughters/get_all_parentsis ever exercised.This is especially pressing in light of #866, which revamps
I3Calorimetryinto 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:
Currently untested code
Branches/line numbers are against the #866 revision. The precondition
assert hasattr(self, "mctree")guards in theI3Extractormethods are omitted below.i3calorimetry.py—I3Calorimetryframe_contains_info: returnsTrue(both keys);Falsewhenmctreeabsent;Falsewhenmmctracklistabsent (short-circuit)__call__: split-tree particle-count assertion — pass, and theValueError("Split mctree has different number of particles…")path__call__:len(target_tree) > 0vs== 0(thee_track/e_cascadedefault-to-0.0branches); same forbkg_tree__call__: target track sanity checkValueError("Energy deposited in target is greater than primary energy")__call__: target total check — pass via relative tolerance, pass via the< 0.5absolute slack, and theValueError("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!= 0no-warning__call__: fraction zero-guards —fraction_target_total,target_cascade_fraction, andfraction_cascade_total(thee_total == 0branch must notUnboundLocalErroroncascade_fraction_tot)__call__:target_primaries_energy > 0→ numericfraction_primaryvs<= 0→None__call__: output-dict assembly (keys suffixed with_extractor_name;e_*_total = target + bkg) and the_excludefiltertotal_track_energy:is_corsika=True(unfilteredMMCTrackList) vsFalse(filter_track_list)total_track_energy: empty harvest →0; harvested track id present → proceed; id absent (get_particleRuntimeError) →continuetotal_track_energy: hull-intersection — pass;firstNaN;first >= particle.length;secondNaN;second <= 0; starting-trackfirst < 0passestotal_track_energy:get_energysucceeds; the"sum of losses is smaller than energy at last checkpoint"RuntimeError→ warn +continue; any otherRuntimeError→ re-raisetotal_track_energy:entrance_energy=True(+= e0, erase whole subtree) vsFalse(+= e0 - e1)total_track_energy: deposited-mode erase — viae1 == 0, viaintersections.second < particle.length, and the no-erase branch (track stops in-volume, descendants kept)total_cascade_energy: empty primaries →0; non-leaf → extend queue; leafis_trackskip; leafshape == Darkskip; leaf collectedtotal_cascade_energy: no collectible leaves →0; NaNlength→0; cascade-in-hull counted; cascade-out-of-hull excluded; non-cascade leaf excluded by thecascade_boolmaskfilter_track_list: keep (id in tree);"particleID not found"→continue; otherRuntimeError→ re-raise; return type isI3MMCTrackListi3extractor.py—I3Extractorcheck_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-typeValueErrorsget_primaries: CORSIKA (all primaries, args ignored); non-CORSIKAdaughters=False, highest=True(argmax);highest=False(all);daughters=Truewith a top-level in-ice neutrino (filter);daughters=Truewith no top-level in-ice neutrino →find_in_ice_daughtersrecursion; 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=Trueneutrino primary in lineage (erased from bkg) vs not-in-lineage (erased from main) vs non-neutrino (erased from main);highest=Falseneutrino primary (erased from bkg) vs non-neutrino (erased from main)split_mc_tree: latent bug —highest=Truewith no in-ice neutrino daughters →np.argmaxon an empty array raises (no guard)get_all_parents: no parent →[]; multi-level lineage walki3highesteparticleextractor.py—I3HighestEparticleExtractorNot directly tested, and its correctness is unverified-by-dependency:
get_tracks,highest_energy_starting,highest_energy_track/highest_energy_bundleall consume the untestedget_primaries/check_primary_energy/find_in_ice_daughters. (Note it never callssplit_mc_treeorget_all_parents, and always useshighest_energy_primary=True, so those remain reachable only via direct unit tests ofI3Extractor.)What a fixture of 8 events covers
through_going_muontotal_track_energy+filter_track_listkeep; intersection passes; deposited erase viasecond < length; in/out-of-hull cascades; top-level in-ice-νget_primaries; non-CORSIKAsplit_mc_treestopping_track_containede1 == 0; in-hull cascadestarting_trackfirst < 0passes the entrance checktau_to_mu_decaylengthhandlingnan_primary_energycheck_primary_energyNaN → daughters fallbacknon_top_level_in_ice_nuget_primariesrecursion +find_in_ice_daughtersno_descendants_reach_hulle_total == 0warning; fraction zero-guards; out-of-hull cascade; emptyMMCTrackListcorsika_muon_bundleis_corsikaget_primaries/split_mc_tree/total_track_energy(unfiltered); empty target tree;fraction_primary = None; background pathResidual gaps
entrance_energy=Truepaths (entrance accumulation + subtree erase)highest_energy_primary=Truepaths —get_primariesargmax,split_mc_treelineage 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…"RuntimeErrorpath (the known MuonGun issue; hard to hand-seed)tau_to_mu_decaydeliberately targets the ~17% µ channel)split_mc_tree(mixed tree)get_primaries"No in-ice daughter of primary neutrino found"warning + empty returnValueErrors (energy-exceeds-primary, split-count mismatch, NaN-with-no-daughters, invalid-type guards)