Warning, /acts/docs/pages/examples/material_mapping_howto.md is written in an unsupported language. File is not indexed.
0001 @page material_mapping_howto Material mapping
0002
0003 @anchor material_mapping_workflow
0004 # Material mapping
0005
0006 This page is the canonical reference for the ACTS Examples material mapping
0007 chain, and a step-by-step guide to producing a map for a detector of your own.
0008 It assumes you have the detector described both as an ACTS
0009 @ref Acts::TrackingGeometry and as a detailed simulation geometry that Geant4
0010 can navigate (typically DD4hep, which gives you both from one source).
0011
0012 For the conceptual picture — what the mapping does and why it is structured the
0013 way it is — see @ref material_mapping.
0014
0015 The chain is:
0016
0017 ```
0018 geometry ──▶ designate surfaces ──▶ record (Geant4) ──▶ map ──▶ use ──▶ validate
0019 ```
0020
0021 Only the designation step is detector-specific work, and it is the only step
0022 that differs between Gen1 and Gen3. The rest is running three scripts:
0023
0024 - Recording: `Examples/Scripts/Python/material_recording.py`
0025 - Mapping: `Examples/Scripts/Python/material_mapping.py`
0026 - Validation: `Examples/Scripts/Python/material_validation.py`
0027
0028 End to end for the Open Data Detector, which already has its surfaces
0029 designated and so skips step 1:
0030
0031 ```console
0032 python material_recording.py -n 1000 -t 1000 -o odd_material_geant4
0033 python material_mapping.py -n 1000000 -i odd_material_geant4.root -o odd_material
0034 python material_validation.py -n 1000 -t 1000 -m odd_material_map.root -o odd_material_validated -p
0035 ```
0036
0037 > [!tip]
0038 > Run these from `Examples/Scripts/Python`, or prefix each with
0039 > `python Examples/Scripts/Python/<script>.py ...` from the repository root.
0040
0041 The rest of this page walks through the same chain for a detector that does not
0042 have a map yet.
0043
0044 ## Step 1: designate which surfaces carry material
0045
0046 Mapping only writes material onto surfaces you have explicitly designated as
0047 material-carrying. A fresh detector designates nothing, so this is where you
0048 start.
0049
0050 **How you designate depends on which geometry generation you build with, and it
0051 is the only part of this guide that does.** Steps 2 to 5 are identical either
0052 way.
0053
0054 - **Gen1** — designation is a post-hoc annotation of an already-built geometry:
0055 dump it to JSON, flip flags, feed the file back in. Follow steps 1a to 1d
0056 below.
0057 - **Gen3** — designation happens *in the blueprint, during construction*. There
0058 is no JSON round-trip. Skip to
0059 @ref material_mapping_howto_gen3 "Designating material in Gen3".
0060
0061 ### 1a. Dump the geometry to JSON
0062
0063 ```console
0064 python Examples/Scripts/Python/geometry.py
0065 ```
0066
0067 This writes `geometry-map.json` in the current directory (among other outputs).
0068 The relevant part is in `runGeometry()` in
0069 `Examples/Scripts/Python/geometry.py`, which configures a
0070 `MaterialMapJsonConverter` and a `JsonMaterialWriter`.
0071
0072 > [!important]
0073 > The converter must be configured with `processNonMaterial=True`. Surfaces that
0074 > do not already carry material are otherwise skipped entirely
0075 > (`Plugins/Json/src/MaterialMapJsonConverter.cpp:383`), so they never appear in
0076 > the dump and you have nothing to switch on. `geometry.py` already sets this;
0077 > if you write your own dumping script, do not omit it.
0078
0079 Point the script at your own detector by replacing the `getOpenDataDetector()`
0080 call at the bottom of the file.
0081
0082 ### 1b. Reduce it to an editable config
0083
0084 `geometry-map.json` has one entry per surface, which for a real detector is
0085 thousands of entries. `writeMapConfig.py` collapses it into one representative
0086 entry per surface *category* per volume:
0087
0088 ```console
0089 python Examples/Scripts/MaterialMapping/writeMapConfig.py geometry-map.json config-map.json
0090 ```
0091
0092 ### 1c. Edit the config
0093
0094 In `config-map.json`, for every surface category you want to map:
0095
0096 - set `"mapMaterial": true`
0097 - set the `"bins"` entries under `"binUtility" -> "binningdata"` to the
0098 granularity you want
0099
0100 A homogeneous surface is `1 x 1`. Start coarse: bins with no hits produce no
0101 material, and a fine binning with too few recorded tracks gives you a map full
0102 of holes.
0103
0104 ### 1d. Write the choices back
0105
0106 ```console
0107 python Examples/Scripts/MaterialMapping/configureMap.py geometry-map.json config-map.json
0108 ```
0109
0110 > [!warning]
0111 > This rewrites `geometry-map.json` **in place**. Keep a copy of the pristine
0112 > dump, or be ready to regenerate it with `geometry.py`.
0113 >
0114 > It also only copies the bin *counts* back, not the bin ranges or axis types —
0115 > those come from the geometry dump. Editing `"min"`, `"max"` or `"type"` in
0116 > `config-map.json` has no effect.
0117
0118 `geometry-map.json` is now your **mapping configuration**: the same file you
0119 pass as `--matconfig` in step 3.
0120
0121 @anchor material_mapping_howto_gen3
0122 ### Designating material in Gen3
0123
0124 In Gen3 the geometry is built from a blueprint, and material is designated as
0125 part of that construction rather than annotated onto the result. Steps 1a to 1d
0126 do not apply: there is no geometry dump to edit, and no `--matconfig` file. You
0127 edit the blueprint code instead.
0128
0129 @ref Acts::BlueprintNode::addMaterial inserts a
0130 @ref Acts::MaterialDesignatorBlueprintNode, which wraps exactly one child and
0131 marks up that child's volume faces as it is connected:
0132
0133 @snippet{trimleft} examples/material_designation.cpp Designate Proto Material
0134
0135 The two-axis form of @ref Acts::MaterialDesignatorBlueprintNode::configureFace
0136 attaches a @ref Acts::ProtoGridSurfaceMaterial — a binning specification with no
0137 content, which is exactly what the Gen1 JSON route produces via
0138 `ProtoSurfaceMaterial`. It is the direct equivalent of setting
0139 `"mapMaterial": true` plus a bin count, and it is what you want if you intend to
0140 run the mapping.
0141
0142 There is a second form taking an @ref Acts::ISurfaceMaterial directly:
0143
0144 @snippet{trimleft} examples/material_designation.cpp Designate Homogeneous Material
0145
0146 That assigns real material immediately, so there is nothing to map — useful for
0147 a beam pipe or a support tube whose material you already know. `GenericDetector`
0148 uses this throughout (`Examples/Detectors/GenericDetector/src/GenericDetector.cpp`).
0149
0150 Both snippets are taken from `docs/examples/material_designation.cpp`, which is
0151 compiled as part of the `docs-examples` target, so they cannot drift from the
0152 API.
0153
0154 Faces are named per volume-bounds type: @ref Acts::CylinderVolumeBounds::Face
0155 (`OuterCylinder`, `InnerCylinder`, `PositiveDisc`, `NegativeDisc`),
0156 @ref Acts::CuboidVolumeBounds::Face (`NegativeXFace`, `PositiveXFace`, …) and
0157 likewise for trapezoid and diamond bounds.
0158
0159 Things to know before you use it:
0160
0161 - **It designates volume faces (portals) only.** Material goes onto
0162 `portal->surface()`. There is no blueprint equivalent for designating
0163 *sensitive* surfaces — if you need material on those, it has to come from the
0164 detector description itself.
0165 - **Cylinder phi planes are rejected.** `NegativePhiPlane` and
0166 `PositivePhiPlane` throw "Phi plane faces are not supported"
0167 (`Core/src/Geometry/MaterialDesignator.hpp`).
0168 - **Python only exposes the mapping form.** The bindings in
0169 `Python/Core/src/GeometryGen3.cpp` bind the two-axis `configureFace` overloads
0170 for cylinder and cuboid faces. The direct-`ISurfaceMaterial` overloads are
0171 C++ only.
0172 - **Do not designate a face that gets merged.** If a designated portal face has
0173 to be merged during container stacking, construction aborts: the material
0174 cannot be carried onto the larger merged surface. Move the designation to a
0175 face that is not merged, typically the enclosing container's face. Setting
0176 @ref Acts::BlueprintOptions::keepGoingOnMaterialMergeFailure downgrades this
0177 to a warning, but it is lossy — the material is discarded and the surface is
0178 tagged with a @ref Acts::MergedMaterialMarker. Treat it as a debugging aid,
0179 not a fix.
0180
0181 > [!warning]
0182 > Blueprint-built geometry ignores material decorators.
0183 > @ref Acts::Blueprint::construct passes a null decorator to the
0184 > @ref Acts::TrackingGeometry constructor, so the `--matconfig` /
0185 > `IMaterialDecorator` route in steps 1 and 3 has no effect on a Gen3 geometry.
0186 > Concretely, `getOpenDataDetector(gen3=True)` builds its decorator and then
0187 > drops it (`Python/Examples/python/odd.py`), and the Gen3 `OpenDataDetector`
0188 > config has no `materialDecorator` field at all. Designating in the blueprint
0189 > is currently the only route.
0190
0191 Once construction is done the two generations converge completely. The
0192 designated material sits on ordinary surfaces, so `hasMaterial()` is true,
0193 `trackingGeometry.extractMaterialSurfaces()` collects them, and
0194 @ref Acts::BinnedSurfaceMaterialAccumulator consumes `ProtoGridSurfaceMaterial`
0195 alongside `ProtoSurfaceMaterial`. Steps 2 to 5 below are unchanged — just omit
0196 `--matconfig` in step 3, since the geometry already carries the designation.
0197
0198 ## Step 2: record the material with Geant4
0199
0200 ```console
0201 python Examples/Scripts/Python/material_recording.py -n 1000 -t 1000 -o mydet_geant4
0202 ```
0203
0204 Shoots geantinos through the *detailed* geometry and records what they traverse,
0205 into `mydet_geant4.root`. `-n` is events, `-t` is tracks per event, so the above
0206 is a million tracks; `--eta-range` and `--phi-range` restrict the solid angle.
0207
0208 This step is independent of your surface choices — you only need to redo it if
0209 the detailed geometry changes, not when you retune binning. It is by far the
0210 slowest step, so record generously once and reuse the file.
0211
0212 ## Step 3: run the mapping
0213
0214 ```console
0215 python Examples/Scripts/Python/material_mapping.py \
0216 -n 1000000 -i mydet_geant4.root --matconfig geometry-map.json -o mydet_material
0217 ```
0218
0219 `--matconfig` loads your configuration through
0220 `acts.IMaterialDecorator.fromFile()`, which puts an
0221 @ref Acts::ProtoSurfaceMaterial (a binning specification with no content yet) on
0222 every surface you enabled. **On a Gen3 geometry, omit `--matconfig`** — the
0223 blueprint already designated the surfaces, and the decorator would be ignored
0224 anyway. The script then reads those surfaces back out:
0225
0226 @snippet{trimleft} examples/test_material_map.py Extract Material Surfaces
0227
0228 and hands the resulting list to @ref Acts::IntersectionMaterialAssigner and
0229 @ref Acts::BinnedSurfaceMaterialAccumulator. That list is the *only* geometry
0230 input the mapper gets, which is what makes it generation-agnostic.
0231
0232 Outputs:
0233
0234 | File | Contents |
0235 |---|---|
0236 | `mydet_material_map.json` | the material map, human-readable |
0237 | `mydet_material_map.root` | the same map, for production use |
0238 | `mydet_material_mapped.root` | recorded interactions that found a surface |
0239 | `mydet_material_unmapped.root` | recorded interactions that did not |
0240
0241 `_unmapped.root` is the one to look at when something is wrong. A large unmapped
0242 fraction means the material had nowhere to go — see
0243 @ref material_mapping_howto_troubleshooting "Troubleshooting".
0244
0245 ## Step 4: use the map
0246
0247 @snippet{trimleft} examples/test_material_map.py Load Material Map
0248
0249 Substitute your own detector and the map you just produced. This snippet comes
0250 from `docs/examples/test_material_map.py`, which runs as part of the pytest
0251 suite.
0252
0253 `.json`, `.cbor` and `.root` are all accepted. This is exactly how the ODD picks
0254 up `data/odd-material-maps.root` by default.
0255
0256 ## Step 5: validate
0257
0258 ```console
0259 python Examples/Scripts/Python/material_validation.py \
0260 -n 1000 -t 1000 -m mydet_material_map.root -o mydet_validated -p
0261 ```
0262
0263 This re-records material, now from your mapped map instead of from Geant4, so
0264 you can compare the two. `-p` additionally runs a real propagator with a
0265 navigator and writes `mydet_validated_propagated.root`.
0266
0267 Comparing the default and `-p` outputs is worth doing: the default collection is
0268 navigation-independent, so a difference between them is a *navigation* problem
0269 (material the navigator does not reach), not a mapping problem.
0270
0271 To compare against the Geant4 input:
0272
0273 ```console
0274 python Examples/Scripts/MaterialMapping/material_comparison.py
0275 root -l Examples/Scripts/MaterialMapping/Mat_map.C
0276 ```
0277
0278 `Examples/Scripts/MaterialMapping/material_mapping_check.py -i mydet_material_mapped.root`
0279 plots how far each interaction was moved to reach its assigned surface, which is
0280 the quickest way to spot material being attached to the wrong thing.
0281
0282 What you are aiming for looks like this — Geant4, mapped, validated and
0283 propagated profiles of `t_X0` against `eta`, with a ratio panel:
0284
0285 
0286
0287 ## Tuning
0288
0289 Iterating on binning does **not** require re-recording. Edit `config-map.json`,
0290 re-run `configureMap.py` and step 3 against the same recorded file.
0291
0292 Rules of thumb:
0293
0294 - Bins that receive no tracks stay empty. If your map has holes, either coarsen
0295 the binning or record more tracks.
0296 - Boundary and approach surfaces generally need less granularity than sensitive
0297 layers.
0298 - Check `_unmapped.root` after every change, not just at the end.
0299
0300 @anchor material_mapping_howto_limits
0301 ## Limitations you should know about
0302
0303 These are properties of the current navigation-less mapper, not of your setup.
0304 Both are cases where the legacy propagation-based mapper did more.
0305
0306 **Volume material is not produced.** @ref Acts::MaterialMapper retrieves volume
0307 assignments from the assignment finder and then discards them; only surface
0308 material is accumulated, and `finalizeMaps()` returns an empty volume map
0309 (`Core/src/Material/MaterialMapper.cpp`). If you need volume material, the
0310 deprecated @ref Acts::VolumeMaterialMapper is currently the only path.
0311
0312 **`mappingType` is not honoured.** The `"mappingType"` key round-trips through
0313 the JSON and is stored on the material, but the current assignment does plain
0314 nearest-intersection matching — its own comment says *"no pre/post matching"*
0315 (`Core/src/Material/MaterialInteractionAssignment.cpp`). `PreMapping`,
0316 `PostMapping` and `Sensor` are only interpreted by the deprecated
0317 @ref Acts::SurfaceMaterialMapper. To steer assignment today, use the
0318 `globalVetos`, `localVetos` and `reAssignments` hooks in
0319 @ref Acts::MaterialInteractionAssignment::Options.
0320
0321 @anchor material_mapping_howto_troubleshooting
0322 ## Troubleshooting
0323
0324 **Everything lands in `_unmapped.root`.** Nothing was designated. On Gen1,
0325 confirm that `geometry-map.json` actually contains `"mapMaterial": true`
0326 somewhere — if you forgot `processNonMaterial=True` in step 1a, or forgot to run
0327 `configureMap.py` in step 1d, the file will be syntactically fine and
0328 semantically empty. On Gen3, check that you are not passing `--matconfig` and
0329 expecting it to do something: blueprint geometry ignores decorators, so the
0330 designation has to be in the blueprint itself.
0331
0332 **Gen3 construction throws on a material merge.** A designated portal face is
0333 being merged during container stacking. Move the designation outward to the
0334 enclosing container's face rather than reaching for
0335 `keepGoingOnMaterialMergeFailure`, which discards the material.
0336
0337 **The map is full of empty bins.** Too fine a binning for the number of recorded
0338 tracks. Coarsen it, or record more.
0339
0340 **Material appears on the wrong surface.** Expected when candidate surfaces are
0341 close together: assignment picks the nearest intersection with no notion of
0342 "before" or "after". Use `material_mapping_check.py` to see the assignment
0343 distances, and the veto/re-assignment hooks to correct specific surfaces.
0344
0345 **Validation disagrees with Geant4, but only with `-p`.** That is navigation,
0346 not mapping. The mapped material is fine; the navigator is not finding all of
0347 it.
0348
0349 ## Worked example, and the tests that guard this
0350
0351 `Python/Examples/tests/test_material_mapping.py` runs the whole chain against
0352 the ODD and is the most reliable executable reference for it. The ODD itself
0353 ships a finished map at `data/odd-material-maps.root`.
0354
0355 The chain is additionally guarded by tests in `Python/Examples/tests`:
0356
0357 - `conftest.py` — the `material_recording_session` fixture generates reusable
0358 Geant4 material tracks.
0359 - `test_examples.py` — `test_material_recording` and `test_material_mapping`.
0360 - `root_file_hashes.txt` — reference hashes for the workflow outputs.
0361
0362 Those tests are the executable reference for expected output structure and for
0363 regression tracking. If this page and they disagree, they are right.