Skip to content

Phase 1: derive TMS geometry bounds from a GDML survey instead of hardcoded constants - #298

Open
SFBayLaser wants to merge 11 commits into
mainfrom
phase1-geometry-survey
Open

Phase 1: derive TMS geometry bounds from a GDML survey instead of hardcoded constants#298
SFBayLaser wants to merge 11 commits into
mainfrom
phase1-geometry-survey

Conversation

@SFBayLaser

Copy link
Copy Markdown
Member

Part of a proposed 3-phase restructuring to prepare dune-tms for 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_Bar plane/bar validation, event viewer bounds) at it instead.

Also includes: reconciliation with PR #280's node-path-lookup BarNumber scheme (adopted #280's approach for BarNumber, kept our survey-based validation as a complement), and a follow-up fix for a PlaneNumber collision 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 in TMS_Reco.cpp. Fixed by using the same z-ordered lookup table BarNumber already relies on.

Verification: build clean in the SL7/Apptainer container; full-spill regression run confirms RecoHitPlane/TrueHitPlane now span the correct 0-81 range with zero negative sentinels, and nHits/ntracks/NTrueHits deltas vs a pristine-main baseline collapsed to <1% after the PlaneNumber fix (down from double-digit-percent swings before it).

Closes #291.

@SFBayLaser

Copy link
Copy Markdown
Member Author

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.

SFBayLaser and others added 9 commits August 12, 2026 12:40
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.

Copilot AI left a comment

Copy link
Copy Markdown

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 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 to TMS_Geom survey-derived accessors.
  • Moved geometry volume-name substrings from TMS_Constants.h into config/TMS_Default_Config.toml and plumbed them through TMS_Manager for 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, and return 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, and return 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.

Comment thread src/TMS_Geom.h Outdated
Comment thread src/TMS_Bar.cpp Outdated
Comment thread src/TMS_Manager.cpp Outdated
Comment thread src/TMS_Geom.h
- 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment thread src/TMS_Geom.cpp Outdated
…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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 "

@SFBayLaser

Copy link
Copy Markdown
Member Author

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.

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.

Phase 1: derive TMS geometry bounds from a GDML survey instead of hardcoded constants

2 participants