Geant4 integration: what is in, what is missing, what to do next¶
A survey of the Geant4 Application Developer's
Guide
against src/HEP/Geant/, and a plan for the rest.
The guide's two relevant chapters are Detector Definition and Response and Tracking and Physics. Their subsections are the checklist below.
What is integrated¶
| Area | Objects | Tests |
|---|---|---|
| Solids | 30+, including Boolean, twisted, polycone/polyhedra, tessellated | SolidTest, NewSolidsTest, BooleanSolidTest |
| Volumes | LogicalVolume, PhysicalVolume |
SolidTest, HierarchyTest |
| Repeated placement | ReplicaVolume, ParameterisedVolume, AssemblyVolume |
VolumesTest |
| Regions & cuts | Region with per-particle production cuts |
VolumesTest, RegionSerializeTest |
| Materials | NIST catalogue, Element, MaterialComponent, compounds |
MaterialTest, CompoundMaterialTest |
| EM field | MagneticField, ElectricField, accuracy controls |
FieldTest |
| User limits | UserLimits |
FieldTest |
| Hits | SensitiveDetector, Hit |
SensitiveTest, SimulationReadoutTest |
| Scoring | ScoringDetector, Scorer (9 quantities), 3 filters |
ScoringTest |
| Parallel geometry | ParallelWorld, ReflectedVolume |
ParallelTest |
| Exception safety | Guard.h — fatal G4Exception becomes a refusal |
example 07 |
| Remote operation | scene on another node, driven over a socket | RemoteSceneTest |
| Physics lists | Physics — 36 reference lists, 12 EM variants, step limiter, optical flag |
PhysicsTest, SimulationReadoutTest |
| Primary generators | cosmic (SkyPlane, Cylinder, QuadMesh), plus ParticleGun and IsotropicSource |
SourcesTest, GunSimulationTest |
| Scoring meshes | ScoringMesh — box or cylinder grid, any Scorer |
ScoringMeshTest |
| Track filtering | TrackFilter — a G4UserStackingAction from the object tree |
TrackFilterTest |
| Run and tracking | RunSummary timing and clearing, TrackRecorder one line per track |
ActionsTest, example 15 |
| Optical materials | OpticalProperty on a Material, plus Birks quenching |
OpticalTest, example 11 |
| Optical surfaces | OpticalSurface skins and BorderSurface interfaces |
OpticalTest, example 11 |
| Digitization | Digitizer — channels, gate, threshold, resolution, counts |
DigitizerTest, example 12 |
| Isotopes | Isotope on an Element — enriched and depleted mixes |
IsotopeTest, example 13 |
| Voxel phantoms | VoxelPhantom — a VoxImage as nested-parameterised geometry |
PhantomTest, example 16 |
| Probes | PlaneProbe — a placed surface streaming crossings to a recipe |
ProbeTest, example 17 |
| Division | DivisionVolume — slicing a mother by count, width or both |
VolumesTest, example 14 |
GDML import exists on the Detectors side (GDMLSolidTest), not as a Geant
object.
What is missing¶
Ordered by what it costs to not have.
1. ~~Physics lists~~ — done¶
Physics offers the 36 reference lists and 12 EM variants this build has, read
from G4PhysListFactory, plus the step limiter (without which a UserLimits
MaxStep is silently ignored) and an optical flag. A Scene runs with the one
it is given and keeps the built-in default otherwise. A name the build does not
have is refused rather than substituted.
Still open here: optical physics is registrable but inert until materials can declare optical properties (§4), and hadronic alternatives beyond the reference lists are not exposed.
2. ~~Primary generators~~ — done¶
ParticleGun (named particle, energy, aimed direction, multiplicity, optional
cone spread) and IsotropicSource (General Particle Source reduced to point,
sphere or box radiating isotropically) sit beside the three cosmic emitters as
further EmitterPrimary subclasses, so a Scene's Emitter port takes any of
them. The cosmic generators are untouched and GunSimulationTest drives a
scene with both in turn.
Still open here: GPS's full surface — spectra other than monoenergetic, angular
distributions other than isotropic, multiple sources with intensities — is not
exposed. Nor is G4ParticleGun's use for ion species.
3. User actions — stacking done, the rest open¶
TrackFilter is a G4UserStackingAction reachable from the object tree: a
Scene has a TrackFilter port, and the filter says which tracks are followed
(PrimariesOnly, MinSecondaryEnergy, ParticleFilter,
DropNeutralSecondaries). Ten 4 GeV muons through a metre of iron drop from
~15 800 recorded hits to ~1 200 with secondaries off, which is the cheapest
lever a study has.
Two decisions worth keeping. The policy is a pure function --
Classify(TrackFacts) over four numbers -- so it is testable exhaustively
without a beam, and the G4 side is only what reads those numbers off a
G4Track. And the stacking action reads the scene's filter per track rather
than binding to one: Geant4 builds a worker's actions once when the thread
starts, so a bound action would freeze whichever filter happened to be attached
at that moment. Attaching, exchanging or detaching between runs all work.
RunSummary and TrackRecorder take the other two places. The summary reports
the last run's event count, duration and rate -- read on the master, where
Geant4 has merged the workers -- and clears the attached readouts when a run
begins, which is what stops a second run appending to the first. The recorder
writes one line per track: both ends, both directions, both energies, the path
length and ScatteringAngle(). Half the muons through 300 mm of lead scatter
49 mrad on average against 0.7 for those through air beside it, which is the
measurement this framework exists to make.
Two Geant4 constraints shaped this. One run action per thread, so the summary
shares the one that re-clones the emitter; and BuildForMaster needs its own,
since under the tasking manager Build runs on workers only.
Still open: there is no general Action base -- these are purpose-built
objects rather than a family -- and the tracking action is bound to its
recorder when a worker's actions are built, so detaching one mid-process does
not stop an already-built worker recording. DetectorSteppingAction stands
beside all of this, unchanged.
4. Optical materials and Birks quenching¶
G4MaterialPropertiesTable is absent, so no refractive index, absorption
length, or scintillation yield can be given — optical physics would have
nothing to work with. Birks quenching likewise. Both matter for scintillator
detectors, which is what the existing DetectorChamber models.
5. ~~Scoring meshes~~ — done¶
ScoringMesh is a box or cylinder grid registered with G4ScoringManager,
taking any of the existing Scorer objects. It takes no volumes: it covers
whatever is inside it. Bins are read back with ReadScores(scorerName), keyed
by Geant4's flattened bin index, and ToVoxImage() turns them into the image
type the rest of this library reconstructs into.
The bin ordering was listed here as undetermined, and it was not: the mesh was
registering plain primitive scorers. A plain scorer indexes a hit by the copy
number of the innermost replica -- the last axis alone -- so every bin sharing
a z merged and a 4 x 4 x 8 mesh reported eight numbers rather than 128. That
is what made the keys look meaningless. Scorer::MakeG4MeshScorer builds the
3D variants, which read all three depths and produce i*nj*nk + j*nk + k;
BinOf() inverts it, and the dose-map example now shows a pencil beam lighting
four of sixteen columns with nothing off-axis.
Two things to know before relying on it.
A mesh's geometry is committed the moment it is registered with
G4ScoringManager: a size, centre or segment count set afterwards is accepted
without complaint and silently ignored, leaving Geant4's 10 × 10 × 10 default.
ScoringMesh therefore configures before registering, and property changes
announce rather than commit — Update() is what commits. This was found the
hard way: the first version registered first, so every mesh ran at the default
resolution while the tests, which compared against the object's own property
rather than the mesh, reported success.
Mesh bins do not map onto a VoxImage, and a conversion is not shippable
today. G4ScoringBox::GetIndex is private and the flattening is documented
nowhere, so the mapping has to be measured — and measuring it showed the score
map's keys do not vary with the transverse bin at all. A pencil beam fired down
two different columns of a 3 × 5 × 7 mesh produced the same keys, 0..6, in
both. G4ScoringBox carries an fSegmentDirection and appears to score along
one axis in this configuration. Until that is understood, any voxel mapping
would be a guess that produces a scrambled map looking entirely plausible,
which is worse than not having one. This is the next thing to resolve here.
6. ~~Digitization~~ — done, though not through G4VDigitizerModule¶
Digitizer reads the hits of the SensitiveDetectors attached to it and
produces Digi readings: grouped into channels (per copy, per volume, or all
together, and never across events), integrated over a gate, smeared with a
sqrt(E) resolution, cut at a discriminator threshold and quantised into counts
with a saturation range. Each digi carries the true value beside the reported
one, because the difference between them is the instrument.
It is deliberately not a G4VDigitizerModule. Geant4's machinery reads
G4VHitsCollections out of G4HCofThisEvent; this library's sensitive detector
keeps flat Hit records instead, which is what lets them serialise, cross a
socket and be reached from the object tree. Running the digitisation off those
also makes Digitize(hits) a pure function, checkable by handing it three hits.
Hit gained an eventId for this: without it there was no way to tell two
muons apart after the fact, and a readout that merged them would report energy
no channel ever saw at once.
PerEvent drives it from the event loop: each event's hits are taken from the
detectors as that event ends and become digis there and then. The hits are
consumed -- the digis are the record -- which is what keeps a long run from
holding every step it took. Safe under several workers, and tested that way:
DigitizerThreadedTest runs 40 events across 4 workers and requires exactly 40
digis with 40 distinct event ids.
GetDigisForEvent(id) joins a run's readings to its events. That is
deliberately a lookup rather than a collection hanging off GeantEvent: a digi
already carries the event that made it, GeantEvent does not persist its own
muon path, so nothing would travel with it that does not travel already -- and
every consumer would pay for digis whether it wanted them or not.
7. Smaller gaps¶
- ~~Isotopes.~~ Done:
Isotopeattaches to anElement's ports, abundances are checked to add to 1 before Geant4 sees them, and an impossible nucleus is refused rather than fatal. Isotopes beat the NIST flag. - ~~
G4PVDivision.~~ Done:DivisionVolume, by count, by width, or both, with an offset. Geant4's width-only constructor is unusable in 11.2 -- it hands zero replicas toG4PVReplica::CheckAndSetParametersand aborts -- so the count is worked out from the mother's bounding extent instead. - ~~
G4VNestedParameterisation.~~ Done:VoxelPhantomturns aVoxImageinto three levels of repetition -- slices, columns, voxels -- where only the innermost is parameterised and a voxel's material comes from the copy numbers of all three. An 8-cubed phantom costs three divisions rather than 512 placements, and a 256-cubed one costs the same three. A value becomes a material by falling into one of the bands betweenValueMinandValueMax, one per attachedMaterial. - Track and step user information (
G4VUserTrackInformation). Largely answered byTrackRecorder: what a study would have attached to a track is usually what it wanted to read back at the end, and that is now a record. - Object persistency (Geant's own; uLib has
.gcx, which is better suited). - Track error propagation, exotic physics — specialist, low priority.
The architectural debt — lifted¶
Readouts are built per worker thread now, and a scene runs multithreaded.
Geant4 keeps a logical volume's sensitive detector, field manager and user
limits in a split class — per-thread storage filled for each worker by
G4VUserDetectorConstruction::ConstructSDandField(). This library used to
attach them from the object tree, on the thread that owns it, which reaches a
worker only when the worker is that thread. A scene therefore ran serial, and
before that it ran multithreaded and silently collected nothing.
SceneDetectorConstruction::ConstructSDandField() now walks the document and
has each readout build its own G4 object for the calling thread. That is
the part that matters: a G4VSensitiveDetector belongs to a G4SDManager,
that manager is thread-local, and registering one name twice is a fatal
exception — so a shared detector re-attached per thread cannot work, and does
not. SimulationReadoutTest now collects hits across six worker tasks.
Consequences worth knowing:
SensitiveDetector::GetG4SensitiveDetector()andScoringDetector::GetG4Detector()return null and always will. There is no single G4 object to hand back.IsAttached()answers what this side can answer: configured, with volumes.- Hit recording takes a lock, because every worker's detector writes to one list. That is the one place the readout serialises; tracking dominates for anything but a pathological detector.
- Detector names are uniqued against a process-wide set rather than against
G4SDManager, which no longer holds anything on the master.
One thing is still serial: a scene holding a ScoringMesh. A mesh is a
parallel world, and registering one after the master has built its process
tables leaves the workers with different ones — Geant4 aborts with "size of
G4ProcessVector is inconsistent between master and worker". Scene has a
Multithreaded property, on by default, and the mesh test and example turn it
off.
Investigated and still open, with what was learnt. A parallel world needs two
things under MT, not one: the geometry, which G4ScoringManager builds, and a
process on every particle that steps through it. Registering
G4ParallelWorldPhysics(mesh->GetWorldName()) on the physics list before the
kernel builds its tables does clear the process-vector abort -- and the run
then dies later, in G4ScoringManager::Merge at worker termination, because
the worker's thread-local scoring manager has no meshes to merge from while the
master's has one. So the remaining piece is getting the meshes duplicated onto
each worker, which is what G4WorkerRunManager::ConstructScoringWorlds does
from G4MTRunManager::GetMasterScoringManager(). Forcing G4RunManagerType::MT
instead of the tasking manager does not help; it fails earlier and worse. The
half-fix was not kept: turning a clean abort at startup into a crash at the end
of a run is worse for anyone who ignores the documentation.
Suggested order¶
- ~~Physics lists~~ — done.
- ~~Primary generators~~ — done.
- ~~Scoring meshes~~ — done. A mesh-to-
VoxImageconversion remains. - ~~Multithreaded readout~~ — done, except for scenes holding a
ScoringMesh, which still needMultithreadedoff. - ~~Stacking action~~ — done:
TrackFilter, §3. - ~~Optical materials~~ — done:
OpticalPropertyand Birks, §4. - ~~Digitization~~ — done:
Digitizer, §6. - ~~Isotopes and division~~ — done, §7.
- ~~Optical surfaces~~ — done:
OpticalSurface, §4. - ~~Border surfaces~~ and ~~per-event digis~~ — done, §4 and §6.
- ~~Run and tracking actions~~ — done:
RunSummaryandTrackRecorder, §3. - ~~Mesh bins to a
VoxImage~~ — done, §5: the ordering was never the problem, the scorers were. - ~~Optical physics configuration~~ — done, §4.
- ~~Nested parameterisation~~ — done:
VoxelPhantom, §7. WithToVoxImagebeside it the loop is closed: an image becomes geometry, muons cross it, and the measurement finds the object again -- example 16 recovers a lead block at 83 mrad against a 0.5 mrad background, in the columns it was drawn in. - ~~Digis joined to events~~ — done as a lookup, §6, for the reason given there.
- What is left, with what each is worth:
ScoringMeshunder the tasking run manager. The one real gap. Investigated in §5 above, half understood, deliberately not half-fixed. A scene with a mesh runs serially and says so.- WLS time profile and per-process verbosity (§4). Two settings on
G4OpticalParametersnobody has needed yet; the ones that decide what optical physics costs are exposed. - A general
Actionbase (§3). Listed originally as the shape user actions should take, and on the evidence of building four of them it is not worth having:TrackFilter,RunSummary,TrackRecorderand the digitizer's event hook share no state and no behaviour, only the fact that Geant4 calls them. A base class would add a layer and remove nothing. - Track user information (§7). Largely answered by
TrackRecorder, which is what anybody would have attached it for.
Each step should arrive the way the existing ones did: objects with discoverable
properties, a test that runs rather than only wires, and an example under
src/HEP/Geant/examples/ that writes a scene.