Skip to content

geocad: speed up OCCShapeCreation for partial geometry exports#58

Open
wdconinc wants to merge 6 commits into
mainfrom
perf-geocad-shape-creation
Open

geocad: speed up OCCShapeCreation for partial geometry exports#58
wdconinc wants to merge 6 commits into
mainfrom
perf-geocad-shape-creation

Conversation

@wdconinc
Copy link
Copy Markdown
Contributor

Motivation

Running npdet_to_step part <volume> on a large detector geometry (e.g. ePIC epic_craterlake.xml) was extremely slow because OCCShapeCreation contained three stacked performance problems.

Changes

Fix 1 — Pre-collect relevant volumes (CollectRelevantVolumes)

For partial exports, OCCShapeCreation previously converted every volume in the geometry to an OCC shape (~3000 unique volumes in ePIC), even though only the few dozen volumes inside the requested sub-tree are ever used.

A new CollectRelevantVolumes() method mirrors the skip logic of OCCPartialTreeCreation to determine — in a single O(N) tree walk with no OCC operations — exactly which volumes will be needed. CreatePartialGeometry now passes this filter to OCCShapeCreation, which then converts only those volumes.

Fix 2 — Replace O(V×N) nested TGeoIterator with a pre-built mother map

The original OCCShapeCreation launched a fresh TGeoIterator from the geometry root for each of the V volumes in GetListOfVolumes() in order to find that volume's mother — O(V×N) total with V ≈ 3000 volumes and N ≈ 60 000 nodes in ePIC (≈ 180 M node visits). This is replaced by a single O(N) tree walk that builds a TGeoVolume* → mother TGeoVolume* map before the volume loop; each lookup is then O(1).

Fix 3 — Defer UpdateAssemblies() to end of method

XCAFDoc_DocumentTool::ShapeTool::UpdateAssemblies() was called after every volume insertion (three call sites inside the loop). It is now called once at the end of OCCShapeCreation.

Performance

Test case: npdet_to_step part LumiDirectPCAL_vol -o out $DETECTOR_PATH/epic_craterlake.xml on the full ePIC epic_craterlake.xml detector geometry.

Wall time
Before >10 min (terminated)
After ~33 s

The remaining ~33 s is dominated by DD4hep geometry loading; the OCCShapeCreation step itself is now negligible for small partial exports.

Scope

The full-geometry path (CreateGeometry / OCCTreeCreation) is unaffected in semantics and also benefits from fixes 2 and 3.

Three performance improvements to TOCCToStep::OCCShapeCreation:

Fix 1: Pre-collect relevant volumes before OCCShapeCreation
Add CollectRelevantVolumes() which mirrors OCCPartialTreeCreation's
skip logic to return only the TGeoVolume* set reachable from the
requested sub-tree.  CreatePartialGeometry now passes this filter to
OCCShapeCreation so that only the few dozen volumes in the target
sub-tree are converted to OCC shapes, rather than every volume in
the full detector geometry (~3000 in ePIC).

Fix 2: Replace O(V*N) nested TGeoIterator with a pre-built mother map
The original OCCShapeCreation launched a fresh TGeoIterator from the
top of the geometry for every volume in GetListOfVolumes() to find
that volume's mother — O(V*N) with V~3000 volumes and N~60000 nodes.
This is now replaced by a single O(N) tree walk that builds a
TGeoVolume* -> mother TGeoVolume* map before the volume loop.
Each volume's parent label is then looked up in O(1).

Fix 3: Defer UpdateAssemblies() to end of method
UpdateAssemblies() was called after every volume insertion (three
call sites inside the loop).  It is now called once at the end.

Measured with 'npdet_to_step part LumiDirectPCAL_vol' on the ePIC
epic_craterlake.xml full detector geometry:
  Before: >10 minutes (terminated)
  After:   ~33 seconds

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 20, 2026 01:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Speeds up GeoCAD partial geometry exports (npdet_to_step part ...) by avoiding unnecessary ROOT→OCC conversions and reducing repeated geometry tree traversals inside TOCCToStep.

Changes:

  • Added CollectRelevantVolumes() to precompute the set of volumes needed for partial exports and pass that filter into OCCShapeCreation().
  • Replaced per-volume root-to-leaf scans with a single pre-built volume→mother map in OCCShapeCreation().
  • Deferred UpdateAssemblies() to a single call at the end of OCCShapeCreation().

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/geocad/src/TOCCToStep.h Extends OCCShapeCreation() API to accept a volume filter; declares CollectRelevantVolumes() helpers.
src/geocad/src/TOCCToStep.cxx Implements filtered volume collection and optimizes shape creation by precomputing parent relationships and deferring assembly updates.
src/geocad/src/TGeoToStep.cxx Uses CollectRelevantVolumes() to avoid converting irrelevant volumes during partial export flows.

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

Comment thread src/geocad/src/TOCCToStep.cxx Outdated
Comment thread src/geocad/src/TOCCToStep.cxx
Comment thread src/geocad/src/TOCCToStep.cxx Outdated
Comment thread src/geocad/src/TOCCToStep.cxx
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 20, 2026

@wdconinc
Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: wdconinc <4656391+wdconinc@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 20, 2026

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts and merged origin/main into this branch in commit 0cdac21.

@wdconinc
Copy link
Copy Markdown
Contributor Author

Copilot AI temporarily deployed to github-pages May 20, 2026 13:04 Inactive
- CollectRelevantVolumes: call SetType(1) when level reaches max_level
  to avoid visiting deeper nodes that will only be skipped.  Guard with
  max_level >= 0 so unlimited-depth (-1) is unaffected.

- Replace std::map<TGeoVolume*,TGeoVolume*> motherMap with
  std::unordered_map for O(1) average lookup instead of O(log N).
  Add <algorithm> and <unordered_map> includes.

- When volume_filter is provided, sort volumes_to_process by name
  before processing to give deterministic label/shape creation order
  independent of pointer values across runs and platforms.

- After building the full-tree mother map, post-process it so that
  every volume in the filter has its recorded mother within the filter
  (or Top).  A full-tree scan may otherwise record a first-encountered
  mother that lies outside the selected sub-tree when a volume is
  reused at multiple positions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@wdconinc
Copy link
Copy Markdown
Contributor Author

Export is unaffected by this (DRICH is hidden due to the large number of volumes making navigation hard):
image

wdconinc and others added 2 commits May 22, 2026 17:16
When max_level is -1 (unlimited depth, the default), the condition
'level > max_level' was always true for any visited node (level >= 1),
causing every node to be skipped and leaving only the top volume in
the filter set.  OCCShapeCreation then processed no volumes, and
OCCPartialTreeCreation found null labels and threw Standard_NullObject.

Guard the depth-skip check the same way as the SetType(1) call above it:
  if (max_level >= 0 && level > max_level) continue;

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TGeoIdentity() calls RegisterYourself() in its constructor, causing
spurious 'Registered matrix was removed' warnings at destruction.
TGeoHMatrix() is semantically equivalent (identity transform) but
never auto-registers.  This is consistent with the fix applied in
PR #57 to the original OCCShapeCreation; the performance rewrite
inadvertently reintroduced TGeoIdentity() at three call sites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The crash occurred because OCCShapeCreation was processing filter
volumes in alphabetical order, which could label a child volume
before its parent.  When the parent was encountered later it was
skipped (already labeled), but with a hierarchy structure that
misled FillOCCWithNode into calling NbComponents on null labels.

Fix: build volumes_to_process in tree-traversal (DFS) order during
the existing O(N) motherMap scan.  DFS guarantees that each volume's
first encounter comes after all its ancestors, so every parent label
is non-null when the child is processed.  The else-branch that created
an on-the-fly mother label is kept as a safety fallback.

As defense-in-depth, add null guards in FillOCCWithNode so that a
null labelMother or fLabel causes the walk-up to skip that level
instead of crashing with Standard_NullObject.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

3 participants