Back to home page

EIC code displayed by LXR

 
 

    


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 ![Overlay and ratio for material profiles.](material/overlay_profile_ratio_v_eta_t_X0.png)
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.