Geometry patching¶
Geometry patching is the trick that lets k4Bench add or remove detectors
without ever touching the original XML. It lives in
k4bench.geometry.patcher and
k4bench.geometry.index.
Purpose¶
DD4hep geometries are split across many XML files linked by document references
such as <include ref="..."/> and <includes><file
ref="..."/></includes>, and they usually live on a read-only CVMFS mount.
To benchmark "the geometry minus detector X" you need a modified geometry — but
you cannot edit the originals, and naïvely copying just one file breaks the
document graph. The patcher solves this by producing a self-consistent set of
temporary XML files with the requested detectors removed, and handing ddsim
a patched top-level file that transparently points at them.
How it works¶
Step 1 — Index the geometry once¶
GeometryIndex walks DD4hep document
references recursively from the top-level XML. In one traversal it records
reachable files, forward and reverse graph edges, detector declarations, plugin
argument values, and every DD4hep filesystem reference.
flowchart TD
TOP["ALLEGRO_o1_v03.xml"] --> A["ECalBarrel.xml"]
TOP --> B["HCalBarrel.xml"]
TOP --> C["DriftChamber.xml"]
A --> AA["ECal_materials.xml"]
Discovery uses a lenient index so listing detectors can still return useful
results from a partly broken tree. Patching uses a strict index and refuses to
write a geometry if any locally resolvable reachable file cannot be read.
Refs containing $ (for example ${DD4hepINSTALL}/...) remain unresolved on
purpose because ddsim applies its own search path.
Step 2 — Apply one removal transform¶
All modes reduce to a set of detector names to remove. The same engine removes every declaration of those names. A single-removal run passes one name; keep-only passes the complement of the requested keep set.
Step 3 — Remove orphaned plugins¶
DD4hep <plugin> elements often reference a detector by name in an
<argument value="...">. When a detector is removed, its plugins would dangle,
so the patcher deletes any <plugin> whose argument values name a removed
detector.
The sweep is over every reachable file, against the complete set of removed names — a plugin and the detector it names need not share a file. After each patch attempt, the generated tree is checked for plugins naming detectors that disappeared collaterally; any such files are patched in a subsequent pass. A file that loses only a plugin this way gets a patched copy like any other.
Plugin removal is heuristic
This relies on the DD4hep convention that detector identity is encoded in
argument value attributes. Plugins that reference detectors differently
(other attributes, child elements) are not caught and may survive,
potentially causing a ddsim error. If a sweep run fails only for one
detector with a plugin-related message, this is the first thing to check.
Step 4 — Find and allocate the replacement graph¶
The index's reverse edges identify every ancestor of a modified file with a
plain breadth-first traversal. Each replacement is allocated a deterministic
NNN_<original-name> path inside one private patch directory before anything
is written, so diamonds and modified-parent/modified-child shapes are safe.
Allocation happens after the removals, because a removed <detector> can carry
the only document reference that reached a subtree. Removing Outer and the
Nested detector inside its <include> in the same patch leaves the nested
file with no surviving edge, so it gets no replacement at all. Reachability is
recomputed over the whole graph, so a file the removal merely un-nested — one
still reached through another branch — keeps its replacement and its own
removals.
Step 5 — Rewrite filesystem references¶
Because the patched files land in a temp directory (not next to the originals),
all relative file references must become absolute or ddsim can't find them.
The patcher rewrites ref="..." to absolute paths — but only on
<include>, <gdmlFile>, and <file> elements, the three DD4hep element types
whose ref is guaranteed to be a filesystem path. Other elements (e.g.
<detector ref="...">, where ref is a logical name) are left untouched. Refs
with $ or already-absolute paths are skipped.
Generated replacements are retargeted in the same walk that absolutizes remaining relative refs.
Step 6 — Validate before ddsim¶
This is the subtle part, and it applies to both modes. A patched file is only reached by ddsim if every file on the path from the top level down to it references the patched copy.
flowchart LR
TOP["top.xml"] --> A["A.xml"] --> B["B.xml<br/>(owns the detector)"]
TOP2["top_tmp"] --> A2["A_tmp<br/>(redirected)"] --> B2["B_tmp<br/>(detector removed)"]
Rewriting the top level's own includes covers top → owner and nothing deeper:
for top → A → B with only B patched, no include in top resolves to B, so
A needs a redirected copy too even though nothing was removed from it. Skipping
that does not fail loudly — the patched copy is simply never referenced, ddsim
loads the original subtree, and the run silently keeps the detector it was
supposed to drop.
Before returning, the patcher reindexes the generated tree strictly. It rejects missing filesystem targets, removed detectors that remain, unexpected detector names, generated subfiles that are unreachable, and original files that should have been replaced but remain reachable — including those whose replacement was deliberately skipped, since one still being reached means the edge survived and skipping it was wrong. Detectors that disappear because they were reachable only inside a removed detector are recorded as collateral removals.
Inputs¶
- The original top-level compact XML path.
- A detector name (single removal) or a set of names to keep.
Outputs¶
Each patch owns one system-temp directory prefixed
_k4bench_patch_. It contains the generated top level and deterministic
NNN_<original-name> replacement files. Removing the directory cleans up the
whole patch atomically from the caller's perspective.
The context managers (use these)¶
You almost never call the builders directly. Two context managers guarantee cleanup even on exception:
from pathlib import Path
from k4bench.geometry.patcher import patched_geometry, patched_geometry_keep_only
# Remove a single detector
with patched_geometry(Path("ALLEGRO_o1_v03.xml"), "ECalBarrel") as tmp_xml:
... # tmp_xml is the patched top-level; run ddsim against it
# Keep only a subset
with patched_geometry_keep_only(Path("ALLEGRO_o1_v03.xml"), {"Vertex", "DriftChamber"}) as tmp_xml:
...
On exit (normal or exceptional) the complete patch directory is removed.
Failure modes¶
| Symptom | Cause | What to do |
|---|---|---|
DetectorNotFoundError |
the name isn't a <detector name> in any reachable file |
check spelling; list names with get_detector_names |
GeometryParseError |
a locally reachable file is missing or malformed | repair the include tree before patching |
PatchValidationError |
the generated include graph, refs, or detector set is inconsistent | do not run ddsim; report the patcher failure |
Could not absolutize ref '...' warning |
a relative ref points at a missing file | verify the geometry is complete |
| ddsim fails only for one swept detector | an orphaned plugin survived removal | inspect that detector's <plugin> definitions (see the warning above) |
See also¶
- Sweep modes — which patching path each mode uses.
geometry.scanner/geometry.patcher— full API.