Skip to content

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: Isotope attaches to an Element'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 to G4PVReplica::CheckAndSetParameters and aborts -- so the count is worked out from the mother's bounding extent instead.
  • ~~G4VNestedParameterisation.~~ Done: VoxelPhantom turns a VoxImage into 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 between ValueMin and ValueMax, one per attached Material.
  • Track and step user information (G4VUserTrackInformation). Largely answered by TrackRecorder: 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() and ScoringDetector::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

  1. ~~Physics lists~~ — done.
  2. ~~Primary generators~~ — done.
  3. ~~Scoring meshes~~ — done. A mesh-to-VoxImage conversion remains.
  4. ~~Multithreaded readout~~ — done, except for scenes holding a ScoringMesh, which still need Multithreaded off.
  5. ~~Stacking action~~ — done: TrackFilter, §3.
  6. ~~Optical materials~~ — done: OpticalProperty and Birks, §4.
  7. ~~Digitization~~ — done: Digitizer, §6.
  8. ~~Isotopes and division~~ — done, §7.
  9. ~~Optical surfaces~~ — done: OpticalSurface, §4.
  10. ~~Border surfaces~~ and ~~per-event digis~~ — done, §4 and §6.
  11. ~~Run and tracking actions~~ — done: RunSummary and TrackRecorder, §3.
  12. ~~Mesh bins to a VoxImage~~ — done, §5: the ordering was never the problem, the scorers were.
  13. ~~Optical physics configuration~~ — done, §4.
  14. ~~Nested parameterisation~~ — done: VoxelPhantom, §7. With ToVoxImage beside 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.
  15. ~~Digis joined to events~~ — done as a lookup, §6, for the reason given there.
  16. What is left, with what each is worth:
    • ScoringMesh under 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 G4OpticalParameters nobody has needed yet; the ones that decide what optical physics costs are exposed.
    • A general Action base (§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, TrackRecorder and 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.