Phase 1: derive TMS geometry bounds from a GDML survey instead of hardcoded constants - #298
Phase 1: derive TMS geometry bounds from a GDML survey instead of hardcoded constants#298SFBayLaser wants to merge 11 commits into
Conversation
|
I should note that the 4 PRs will be "breaking changes" so I would not expect things to "work" without all four PRs taken together. |
TMS_Bar, TMS_Geom, and TMS_Event walked the GDML node tree by matching hardcoded volume-name substrings compiled into TMS_Constants.h, so a naming change in a new geometry required a recompile. These now live in config/TMS_Default_Config.toml under [Geometry.VolumeNames], read through new TMS_Manager::Get_GEOMETRY_VOLUME_*() accessors. No behavior change: same substring matches, same default values, just sourced from config instead of a compiled constant. First step of the geometry-from-gdml restructuring (see design proposal).
…om it
TMS_Geom::SetGeometry() now walks the full GDML node tree once (new
TMS_Geom.cpp, TMS_Geom::SurveyGeometry/SurveyNodeRecursive) and records
the plane numbers, module numbers, and scintillator-bar bounding box
actually present in the loaded geometry, instead of relying solely on
the hardcoded expectations in TMS_Constants.h.
GetXStartOfTMS()/GetXEndOfTMS() and friends (the "bars only" bounding
box) now return survey results when available, falling back to the
TMS_Constants.h values (with a one-time warning) if no survey has run.
TMS_Bar::CheckBar()'s plane-number and global-bar-number sanity checks
do the same: validate against what the survey observed rather than
TMS_Const::nPlanes/nModules, so they can't silently drift from the
geometry actually being used.
Explicitly NOT covered by the survey yet (still on TMS_Const:: for
now, and documented as such in TMS_Geom.h):
- the steel-inclusive "TMS mass" bounding box -- no steel volume is
identified by name anywhere in this codebase yet
- the LAr active-volume bounding box
- the thin/thick steel region z-boundary
These need their own volume-name identification work; see the
real-data restructuring design proposal.
Unverified: no ROOT/Geant4/edep-sim toolchain was available to build
or run this. Structure was checked by hand (brace balance, signature
matching between TMS_Geom.h and TMS_Geom.cpp, TOML syntax validated
with Python's tomllib). The recursive TGeoManager::CdDown()/CdTop()
walk is new territory relative to the CdUp()-only walks already
proven elsewhere in this file and is the part most worth checking
first against a real GDML -- the startup log line it prints
("[TMS_Geom] Geometry survey found N planes...") should read close to
100 planes / 8 modules for the existing geometry, and the printed bar
region bounding box should be close to the TMS_Constants.h comparison
line printed right after it.
First real-data test (run.log against a MiniProdN5p1 file) showed the survey finding 52 planes / 6 modules against TMS_Constants.h's expected 100 / 8, while its bar-region bounding box came out *larger* than TMS_Constants.h on every axis -- inconsistent with a genuinely smaller detector, and a strong signal of a counting bug rather than a real geometry difference. The bug: nPlanes/nModules were counted via std::set<int> on TGeoNode::GetNumber(), which is a copy number scoped to a node's parent volume, not a tree-wide unique id. Two physically distinct planes under different parent modules that happen to reuse the same local copy number were being silently merged, undercounting. This did not affect TMS_Bar::CheckBar()'s actual validation (that checks membership of a hit's raw plane number in the set, which still contains every raw value that occurs anywhere) -- only the diagnostic plane/module counts printed at startup, and TMS_Geom's GetNPlanesSurveyed()/GetNModulesSurveyed() accessors, which nothing else consumes yet. Fix: since the traversal visits each node exactly once, a plain counter incremented per visit gives an exact count with no assumption about numbering uniqueness. Kept the original std::set<int> (renamed in comments, not behavior) for CheckBar()'s membership checks, which were already correct.
Second real-data run showed the plane-count fix working (52 -> 82,
much closer to TMS_Constants.h's 100) but exposed a different issue:
module-name matches came back as 492 against an expected 8, while the
distinct raw module-number count stayed at 6 in both runs.
492 = 82 (planes) x 6 (distinct modules), exactly. That means, unlike
planes, "Module"-named nodes are nested inside each plane's subtree
rather than being distinct top-level groupings -- the traversal
necessarily walks through a plane's 6 module sub-nodes on its way to
the scintillator bars underneath, so every plane contributes 6
matches for the same 6 module numbers. The per-visit counter that
correctly fixed the plane count is the wrong metric for modules in
this geometry.
This does not affect TMS_Bar::CheckBar()'s actual pass/fail logic
(set membership was always correct), only the diagnostic printout and
CheckBar()'s error-message text, both updated:
- SurveyGeometry() now labels the top-line number as "module-name
matches" rather than "modules", explains the discrepancy, and
auto-detects this specific nesting pattern (match count == planes
x distinct count) to print an explicit note when it fires.
- New TMS_Geom::GetNDistinctPlaneNumbers()/GetNDistinctModuleNumbers()
report the distinct-set size; TMS_Bar::CheckBar()'s error strings
now use these instead of the match-count accessors, so a thrown
error describes "N distinct module numbers" accurately instead of
"492 module numbers".
Net read on two real-data runs so far: this geometry very likely has
6 modules per plane (not 8) and ~82 planes (not 100), on top of the
already-larger bounding box -- three independent signals pointing at
a genuinely different/updated TMS geometry rather than survey bugs.
Worth someone confirming against this geometry's actual design.
TMS_Kalman.cpp, TMS_Reco.cpp, TMS_Bar.cpp, and TMS_EventViewer.cpp each read TMS_Const::TMS_Start*/TMS_End* arrays directly instead of asking TMS_Geom for what the loaded GDML's survey actually found. Point all of them at the survey-derived box instead. Most notably, TMS_Reco.cpp's hit-cleaning filter was silently dropping hits outside a stale hardcoded z-range with no log message, and TMS_Bar.cpp's BarNumber (written to every output tree) was indexed from a hardcoded start rather than this geometry's actual bar-region start. Also adds a drift-detection warning in TMS_Geom.cpp comparing the TOML fiducial box against the surveyed bbox, without silently overriding it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e-name constant TMS_Bar::FindModules() was using TMS_Geom's z-ordered GetPlaneNumberForCurrentNode() index, but CheckBar() validates plane numbers against the survey's fLayout.planeNumbers, which is built from raw TGeoNode numbers. The two schemes aren't interchangeable and the mismatch threw spurious "Plane number N was not among..." errors for legitimate planes. Reverted to the raw geom->GetCurrentNode()->GetNumber(). TMS_Geom.h's BuildPlaneLookupRecursive() still referenced the now-deleted TMS_Const::TMS_ModuleLayerName (removed when volume-name strings moved to config in 70051af); this compiled only because it wasn't touched by the rebase's line-based merge. Pointed it at TMS_Manager::GetInstance().Get_GEOMETRY_VOLUME_ModuleLayer() instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The old comment described the pre-#280 coordinate-offset formula ("start counting from -3520mm onwards"), no longer accurate now that BarNumber comes from TMS_Geom's node-path lookup table.
The raw-node-number revert in f2ba55d (needed at the time to match CheckBar()'s raw-node-based survey validation) turned out to reintroduce the same non-uniqueness bug PR #280 fixed for BarNumber, but for planes: the geometry survey's own diagnostic confirms one real geometry has 82 physically distinct planes but only 52 distinct raw plane-node numbers. PlaneNumber isn't just a label -- TMS_Reco.cpp uses it as a literal z-ordered coordinate (aNode(PlaneNumber, BarNumber) in A* pathfinding, and a "candidates are z-ordered so we can early-exit the scan" loop optimization that silently breaks if PlaneNumber isn't monotonic with z). A colliding, non-contiguous PlaneNumber violates that assumption. Switches PlaneNumber back to TMS_Geom::GetPlaneNumberForCurrentNode() (the same z-ordered, collision-free lookup table BarNumber already uses via GetBarNumberForCurrentNode(), and the scheme main has used unmodified this whole time), and updates CheckBar() to validate it via a simple range check against GetNumberOfScintillatorPlanes() instead of the raw-node survey set -- removing the exact validate-against-the- wrong-scheme mismatch that motivated the original revert in the first place, this time by fixing the value instead of the check.
TMS_OBJ is a hand-maintained object-file list, separate from src/CMakeLists.txt's file(GLOB *.cpp), so it never picked up TMS_Geom.cpp automatically. The CMake build (used locally) was unaffected, but GitHub CI's plain `make` path fails to link TMS_Geom::SurveyGeometry() as a result.
a625e40 to
16e1c6b
Compare
There was a problem hiding this comment.
Pull request overview
This PR is Phase 1 of making dune-tms geometry-driven by deriving TMS bounds and (some) indexing constraints from the loaded GDML via a one-time geometry survey, replacing several hardcoded geometry constants and volume-name strings that could drift out of sync across GDML variants.
Changes:
- Added
TMS_Geom::SurveyGeometry()(and build integration) to walk the GDML node tree once and derive the scintillator-bar bounding box plus surveyed plane/module identity sets. - Switched multiple reconstruction/viewer bounds checks (Kalman, hit cleaning, event viewer, etc.) from
TMS_Const::*hardcoded bounds toTMS_Geomsurvey-derived accessors. - Moved geometry volume-name substrings from
TMS_Constants.hintoconfig/TMS_Default_Config.tomland plumbed them throughTMS_Managerfor config-driven geometry traversal.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/TMS_Reco.cpp | Updates hit cleaning / Hough bounds to use TMS_Geom survey-derived TMS Z/X extents. |
| src/TMS_Manager.h | Adds getters/storage for config-driven geometry volume-name substrings. |
| src/TMS_Manager.cpp | Loads new [Geometry.VolumeNames] TOML keys into TMS_Manager. |
| src/TMS_Kalman.cpp | Uses surveyed TMS start/end bounds for inside-box checks and Kalman bounds validation. |
| src/TMS_Geom.h | Introduces survey layout struct, survey-backed TMS bounds accessors, and volume-name usage via TMS_Manager. |
| src/TMS_Geom.cpp | Implements the recursive GDML node walk and survey reporting / safety cap behavior. |
| src/TMS_EventViewer.cpp | Switches viewer axis bounds and thin/thick line extents to TMS_Geom bounds. |
| src/TMS_Event.cpp | Replaces hardcoded volume-name filtering with config-driven TMS_Manager volume-name substrings. |
| src/TMS_Constants.h | Removes compiled-in volume-name strings (now TOML-driven) and documents the change. |
| src/TMS_Bar.h | Updates GetBarNumber() comment to reflect node-path lookup semantics. |
| src/TMS_Bar.cpp | Switches volume-name matching to TOML-driven values; updates bar/module validation to use survey results when available. |
| src/Makefile | Adds TMS_Geom.o to build now that TMS_Geom.cpp exists. |
| config/TMS_Default_Config.toml | Adds [Geometry.VolumeNames] defaults for config-driven geometry traversal. |
Suppressed comments (2)
src/TMS_Bar.cpp:262
- Same issue as above:
throw;outside a catch block will terminate the program, andreturn false;is unreachable. Throw a concrete exception (or return an error) so failures are diagnosable.
if (!tmsGeom.IsSurveyedModuleNumber(GlobalBarNumber)) {
std::cerr << "Global bar number " << GlobalBarNumber << " was not among the "
<< tmsGeom.GetNDistinctModuleNumbers() << " distinct module numbers found by the geometry survey." << std::endl;
throw;
return false;
}
src/TMS_Bar.cpp:269
- Same issue as above:
throw;outside a catch block will terminate the program, andreturn false;is unreachable. Throw a concrete exception so the failure mode is actionable.
if (GlobalBarNumber >= TMS_Const::nModules || GlobalBarNumber < 0) {
std::cerr << "Global bar number does not agree with expectation of between 0 to " << TMS_Const::nModules << std::endl;
std::cerr << "Has the geometry been updated without updating the geometry constants in TMS_Constants.h?" << std::endl;
throw;
return false;
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- TMS_Bar::CheckBar(): replace 4 bare `throw;` (calls std::terminate() with no message, since nothing catches it) with a proper std::runtime_error carrying context. Same net effect (still aborts on invalid geometry) but now actually diagnosable. - TMS_Geom: HasGeometrySurvey() incorrectly returned fLayout.valid, which only tracks whether the bar-region bbox has data. A geometry with real plane/module structure but zero bar nodes would report false and skip the survey-based CheckBar() path even though IsSurveyedModuleNumber() had real data to check against. Add a separate `surveyed` flag set whenever SurveyGeometry() completes an untruncated traversal, independent of whether any bars were found. - TMS_Geom: GetStartOfTMSMass()/GetEndOfTMSMass() took their X coordinate from GetXStartOfTMS()/GetXEndOfTMS() (bar-region bounds) instead of GetXStartOfTMSMass()/GetXEndOfTMSMass() (the steel-inclusive compiled bounds) -- a copy-paste bug, pre-existing on main and not introduced by this branch. Currently dead code (no callers in src/ or app/), fixed while touching this file anyway. - TMS_Manager: the 13 new required [Geometry.VolumeNames] TOML keys would throw an unhelpful low-level toml11 exception on an older/hand-trimmed config. Wrap in try/catch and rethrow a std::runtime_error naming the missing section and how to fix it.
…center SurveyNodeRecursive() was transforming only a scintillator bar's local origin (0,0,0) to master coordinates and folding just that single point into the bar-region bounding box. That underestimates the true detector extents by roughly half a bar's dimensions on every face (worse for a rotated bar), since it never accounts for the bar's actual size. This bbox isn't cosmetic -- GetStartOfTMS()/GetEndOfTMS() back the Kalman fit bounds and TMS_Reco.cpp's hit-rejection filter directly, so the underestimate could misclassify genuinely-in-bar hits near the physical edge of the detector as outside the TMS. Fix: transform all 8 corners of each bar's TGeoBBox (using its GetOrigin()/GetDX()/GetDY()/GetDZ()) to master and expand the bbox to include each one, instead of just the center. Found by GitHub Copilot's automated review on PR #298.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/TMS_Bar.cpp:281
- This plane-range failure message still points to updating TMS_Constants.h, but PlaneNumber is now derived from TMS_Geom’s z-ordered lookup-table scheme. Updating the message to reference the lookup/config makes it much easier to debug real geometry mismatches.
if (PlaneNumber >= expected_nplanes || PlaneNumber < 0) {
std::cerr << "Plane number does not agree with expectation of between 0 to " << expected_nplanes << std::endl;
std::cerr << "Has the geometry been updated without updating the geometry constants in TMS_Constants.h?" << std::endl;
throw std::runtime_error("TMS_Bar::CheckBar(): plane number out of the expected range");
src/TMS_Bar.cpp:248
- This error message suggests updating TMS_Constants.h, but BarNumber comes from the geometry bar lookup (node-path map). When BarNumber < 0, the likely actionable fix is checking the loaded GDML and the config-driven volume-name substrings, not recompiling constants; the current message can send users in the wrong direction.
This issue also appears on line 278 of the same file.
std::cerr << "Bar number was not found in the geometry bar lookup." << std::endl;
std::cerr << "Has the geometry been updated without updating the geometry constants in TMS_Constants.h?" << std::endl;
std::cout << "Bar number: " << BarNumber << std::endl;
throw std::runtime_error("TMS_Bar::CheckBar(): bar number not found in the geometry bar lookup");
src/TMS_Manager.cpp:127
- The exception text has a small grammar issue: “missing (or has a malformed)” reads awkwardly and can be simplified without changing meaning.
throw std::runtime_error(
"TMS_Manager: config '" + filename + "' is missing (or has a malformed) "
"[Geometry.VolumeNames] section. This section was added by the geometry-survey "
|
It seems the issue found in TMS_Bar.cpp was, in the end, the source of the "non-determinisn" attributed to the Time Slicer. So this has been resolved going forward. |
Part of a proposed 3-phase restructuring to prepare
dune-tmsfor real (non-MC) data (discussed in #nd_muon_spectrometer_code). This is Phase 1. Heads-up issue: #291.Problem: several places (
TMS_Constants.h, config, various bounds checks) hardcoded the TMS geometry's bounding box/plane/module counts for one specific geometry, and silently drift out of sync with any other GDML.Fix: added a one-time geometry survey (
TMS_Geom::SurveyGeometry()) that derives these values from whatever GDML is actually loaded, and pointed the existing hardcoded bounds checks (Kalman fit bounds, hit-rejection filter, Hough seed bounds,TMS_Barplane/bar validation, event viewer bounds) at it instead.Also includes: reconciliation with PR #280's node-path-lookup
BarNumberscheme (adopted #280's approach forBarNumber, kept our survey-based validation as a complement), and a follow-up fix for aPlaneNumbercollision bug this surfaced -- raw ROOT node numbers aren't globally unique across all physical planes in some geometries (81 distinct planes vs only 51 raw node values in one test geometry), which silently broke A* pathfinding's plane-ordering assumption inTMS_Reco.cpp. Fixed by using the same z-ordered lookup tableBarNumberalready relies on.Verification: build clean in the SL7/Apptainer container; full-spill regression run confirms
RecoHitPlane/TrueHitPlanenow span the correct 0-81 range with zero negative sentinels, andnHits/ntracks/NTrueHitsdeltas vs a pristine-main baseline collapsed to <1% after thePlaneNumberfix (down from double-digit-percent swings before it).Closes #291.