|
ACTS
Experiment-independent tracking
|
This page is the canonical reference for the ACTS Examples material mapping chain, and a step-by-step guide to producing a map for a detector of your own. It assumes you have the detector described both as an ACTS Acts::TrackingGeometry and as a detailed simulation geometry that Geant4 can navigate (typically DD4hep, which gives you both from one source).
For the conceptual picture — what the mapping does and why it is structured the way it is — see Material mapping.
The chain is:
Only the designation step is detector-specific work, and it is the only step that differs between Gen1 and Gen3. The rest is running three scripts:
End to end for the Open Data Detector, which already has its surfaces designated and so skips step 1:
The rest of this page walks through the same chain for a detector that does not have a map yet.
Mapping only writes material onto surfaces you have explicitly designated as material-carrying. A fresh detector designates nothing, so this is where you start.
How you designate depends on which geometry generation you build with, and it is the only part of this guide that does. Steps 2 to 5 are identical either way.
This writes geometry-map.json in the current directory (among other outputs). The relevant part is in runGeometry() in Examples/Scripts/Python/geometry.py, which configures a MaterialMapJsonConverter and a JsonMaterialWriter.
Point the script at your own detector by replacing the getOpenDataDetector() call at the bottom of the file.
geometry-map.json has one entry per surface, which for a real detector is thousands of entries. writeMapConfig.py collapses it into one representative entry per surface category per volume:
In config-map.json, for every surface category you want to map:
A homogeneous surface is 1 x 1. Start coarse: bins with no hits produce no material, and a fine binning with too few recorded tracks gives you a map full of holes.
geometry-map.json is now your mapping configuration: the same file you pass as --matconfig in step 3.
In Gen3 the geometry is built from a blueprint, and material is designated as part of that construction rather than annotated onto the result. Steps 1a to 1d do not apply: there is no geometry dump to edit, and no --matconfig file. You edit the blueprint code instead.
Acts::BlueprintNode::addMaterial inserts a Acts::MaterialDesignatorBlueprintNode, which wraps exactly one child and marks up that child's volume faces as it is connected:
The two-axis form of Acts::MaterialDesignatorBlueprintNode::configureFace attaches a Acts::ProtoGridSurfaceMaterial — a binning specification with no content, which is exactly what the Gen1 JSON route produces via ProtoSurfaceMaterial. It is the direct equivalent of setting "mapMaterial": true plus a bin count, and it is what you want if you intend to run the mapping.
There is a second form taking an Acts::ISurfaceMaterial directly:
That assigns real material immediately, so there is nothing to map — useful for a beam pipe or a support tube whose material you already know. GenericDetector uses this throughout (Examples/Detectors/GenericDetector/src/GenericDetector.cpp).
Both snippets are taken from docs/examples/material_designation.cpp, which is compiled as part of the docs-examples target, so they cannot drift from the API.
Faces are named per volume-bounds type: Acts::CylinderVolumeBounds::Face (OuterCylinder, InnerCylinder, PositiveDisc, NegativeDisc), Acts::CuboidVolumeBounds::Face (NegativeXFace, PositiveXFace, …) and likewise for trapezoid and diamond bounds.
Things to know before you use it:
Once construction is done the two generations converge completely. The designated material sits on ordinary surfaces, so hasMaterial() is true, trackingGeometry.extractMaterialSurfaces() collects them, and Acts::BinnedSurfaceMaterialAccumulator consumes ProtoGridSurfaceMaterial alongside ProtoSurfaceMaterial. Steps 2 to 5 below are unchanged — just omit --matconfig in step 3, since the geometry already carries the designation.
Shoots geantinos through the detailed geometry and records what they traverse, into mydet_geant4.root. -n is events, -t is tracks per event, so the above is a million tracks; --eta-range and --phi-range restrict the solid angle.
This step is independent of your surface choices — you only need to redo it if the detailed geometry changes, not when you retune binning. It is by far the slowest step, so record generously once and reuse the file.
--matconfig loads your configuration through acts.IMaterialDecorator.fromFile(), which puts an Acts::ProtoSurfaceMaterial (a binning specification with no content yet) on every surface you enabled. On a Gen3 geometry, omit --matconfig — the blueprint already designated the surfaces, and the decorator would be ignored anyway. The script then reads those surfaces back out:
and hands the resulting list to Acts::IntersectionMaterialAssigner and Acts::BinnedSurfaceMaterialAccumulator. That list is the only geometry input the mapper gets, which is what makes it generation-agnostic.
Outputs:
| File | Contents |
|---|---|
| mydet_material_map.json | the material map, human-readable |
| mydet_material_map.root | the same map, for production use |
| mydet_material_mapped.root | recorded interactions that found a surface |
| mydet_material_unmapped.root | recorded interactions that did not |
_unmapped.root is the one to look at when something is wrong. A large unmapped fraction means the material had nowhere to go — see Troubleshooting.
Substitute your own detector and the map you just produced. This snippet comes from docs/examples/test_material_map.py, which runs as part of the pytest suite.
.json, .cbor and .root are all accepted. This is exactly how the ODD picks up data/odd-material-maps.root by default.
This re-records material, now from your mapped map instead of from Geant4, so you can compare the two. -p additionally runs a real propagator with a navigator and writes mydet_validated_propagated.root.
Comparing the default and -p outputs is worth doing: the default collection is navigation-independent, so a difference between them is a navigation problem (material the navigator does not reach), not a mapping problem.
To compare against the Geant4 input:
Examples/Scripts/MaterialMapping/material_mapping_check.py -i mydet_material_mapped.root plots how far each interaction was moved to reach its assigned surface, which is the quickest way to spot material being attached to the wrong thing.
What you are aiming for looks like this — Geant4, mapped, validated and propagated profiles of t_X0 against eta, with a ratio panel:
Iterating on binning does not require re-recording. Edit config-map.json, re-run configureMap.py and step 3 against the same recorded file.
Rules of thumb:
These are properties of the current navigation-less mapper, not of your setup.
Volume material is not produced. Acts::MaterialMapper retrieves volume assignments from the assignment finder and then discards them; only surface material is accumulated, and finalizeMaps() returns an empty volume map (Core/src/Material/MaterialMapper.cpp). ACTS currently has no way to map volume material.
mappingType is not honoured. The "mappingType" key round-trips through the JSON and is stored on the material, but the current assignment does plain nearest-intersection matching — its own comment says "no pre/post matching" (Core/src/Material/MaterialInteractionAssignment.cpp). PreMapping, PostMapping and Sensor have no effect. To steer assignment, use the globalVetos, localVetos and reAssignments hooks in Acts::MaterialInteractionAssignment::Options.
Everything lands in _unmapped.root. Nothing was designated. On Gen1, confirm that geometry-map.json actually contains "mapMaterial": true somewhere — if you forgot processNonMaterial=True in step 1a, or forgot to run configureMap.py in step 1d, the file will be syntactically fine and semantically empty. On Gen3, check that you are not passing --matconfig and expecting it to do something: blueprint geometry ignores decorators, so the designation has to be in the blueprint itself.
Gen3 construction throws on a material merge. A designated portal face is being merged during container stacking. Move the designation outward to the enclosing container's face rather than reaching for keepGoingOnMaterialMergeFailure, which discards the material.
The map is full of empty bins. Too fine a binning for the number of recorded tracks. Coarsen it, or record more.
Material appears on the wrong surface. Expected when candidate surfaces are close together: assignment picks the nearest intersection with no notion of "before" or "after". Use material_mapping_check.py to see the assignment distances, and the veto/re-assignment hooks to correct specific surfaces.
Validation disagrees with Geant4, but only with -p. That is navigation, not mapping. The mapped material is fine; the navigator is not finding all of it.
Python/Examples/tests/test_material_mapping.py runs the whole chain against the ODD and is the most reliable executable reference for it. The ODD itself ships a finished map at data/odd-material-maps.root.
The chain is additionally guarded by tests in Python/Examples/tests:
Those tests are the executable reference for expected output structure and for regression tracking. If this page and they disagree, they are right.