aegir
Phlex-based simulation framework for the SHiP experiment.
Loading...
Searching...
No Matches
detector_construction.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 CERN for the benefit of the SHiP Collaboration
2//
3// SPDX-License-Identifier: LGPL-3.0-or-later
4
5// detector_construction.hpp — Bridges IGeometrySource to
6// G4VUserDetectorConstruction
7//
8// Delegates Construct() to IGeometrySource::construct() and assigns the
9// configured SD type to volumes matching IGeometrySource::sensitiveVolumes().
10// Registers the configured scoring planes as a parallel world
11// (ScoringParallelWorld, see scoring_planes.hpp).
12
13#pragma once
14
15#include <G4FieldManager.hh>
16#include <G4LogicalVolumeStore.hh>
17#include <G4ProductionCuts.hh>
18#include <G4Region.hh>
19#include <G4SDManager.hh>
20#include <G4String.hh>
21#include <G4VUserDetectorConstruction.hh>
22#include <stdexcept>
23#include <string>
24#include <utility>
25#include <vector>
26
27#include "FieldService/G4MagFieldAdapter.h"
28#include "FieldService/IFieldSource.h"
29#include "geant4_sim_core.hpp"
30#include "geometry_source.hpp"
31#include "scoring_planes.hpp"
32
33namespace SHiP::g4 {
34
35enum class SDMode { scoring, crossing };
36
37class ConfigurableDetectorConstruction : public G4VUserDetectorConstruction {
38 IGeometrySource const*
39 source_; // non-owning; Job-layer product outlives the G4 run
40 ship::IFieldSource const* field_source_; // non-owning; may have no regions
41 SDMode sd_mode_;
42 ship::Energy ke_threshold_; // used by CrossingSD
43 std::vector<std::pair<std::string, ship::Length>>
44 regions_; // pattern -> production cut
45 // The scoring planes' parallel world, or null without planes. Owned by
46 // G4VUserDetectorConstruction once registered (leaked with it, like the
47 // rest of the Geant4 setup).
48 ScoringParallelWorld* scoring_world_ = nullptr;
49
50 // The detector geometry's logical volumes: the volume store without the
51 // scoring planes' parallel world, which the sensitive-volume and field
52 // patterns must not match.
53 [[nodiscard]] std::vector<G4LogicalVolume*> detector_volumes() const {
54 std::vector<G4LogicalVolume*> volumes;
55 for (auto* lv : *G4LogicalVolumeStore::GetInstance()) {
56 if (scoring_world_ == nullptr || !scoring_world_->contains(lv)) {
57 volumes.push_back(lv);
58 }
59 }
60 return volumes;
61 }
62
63 public:
65 IGeometrySource const& source, ship::IFieldSource const& field_source,
66 SDMode sd_mode = SDMode::scoring,
67 ship::Energy ke_threshold = ship::Energy::zero(),
68 std::vector<std::pair<std::string, ship::Length>> regions = {},
69 std::vector<ScoringPlaneConfig> scoring_planes = {})
70 : source_{&source},
71 field_source_{&field_source},
72 sd_mode_{sd_mode},
73 ke_threshold_{ke_threshold},
74 regions_{std::move(regions)} {
75 // Detector hits carry their sensitive-volume pattern's index as detector
76 // id; keep those below the scoring planes' id so the two cannot collide.
77 if (std::cmp_greater(source.sensitiveVolumes().size(),
79 throw std::invalid_argument(
80 "geant4_module: at most " + std::to_string(kScoringPlaneDetectorId) +
81 " sensitive_volumes patterns are supported (detector id " +
82 std::to_string(kScoringPlaneDetectorId) +
83 " is reserved for scoring planes)");
84 }
85 if (!scoring_planes.empty()) {
86 scoring_world_ = new ScoringParallelWorld(std::move(scoring_planes));
87 RegisterParallelWorld(scoring_world_);
88 }
89 }
90
93 [[nodiscard]] bool has_scoring_planes() const {
94 return scoring_world_ != nullptr;
95 }
96
97 // Called once, on ship::geometry_thread() (the run manager is initialised
98 // there — the process's single Geant4 geometry-creating thread)
99 G4VPhysicalVolume* Construct() override {
100 auto* world = source_->construct();
101
102 // Create the configured production-cut regions here, not in
103 // ConstructSDandField: G4Region is a split class, so regions must be
104 // created on the process's single geometry-creating thread — this one —
105 // while ConstructSDandField runs on worker threads (#80). Construct()
106 // is also the canonical Geant4 place: the regions exist before any
107 // worker builds its per-region physics tables.
108 for (auto const& [pattern, cut] : regions_) {
109 auto* region = new G4Region(pattern);
110 auto* cuts = new G4ProductionCuts();
111 cuts->SetProductionCut(aegir::clhep::g4(cut));
112 region->SetProductionCuts(cuts);
113 bool matched = false;
114 for (auto* lv : *G4LogicalVolumeStore::GetInstance()) {
115 if (G4StrUtil::contains(lv->GetName(), std::string_view{pattern})) {
116 region->AddRootLogicalVolume(lv);
117 matched = true;
118 }
119 }
120 if (!matched) {
121 throw std::runtime_error("Production-cut region '" + pattern +
122 "' matches no logical volumes");
123 }
124 }
125
126 return world;
127 }
128
129 void ConstructSDandField() override {
130 auto const& sv_names = source_->sensitiveVolumes();
131 DetectorIdMap detector_ids;
132
133 auto const volumes = detector_volumes();
134 for (auto* lv : volumes) {
135 for (int i = 0; std::cmp_less(i, sv_names.size()); ++i) {
136 if (G4StrUtil::contains(lv->GetName(), std::string_view{sv_names[i]})) {
137 detector_ids.emplace(lv, i);
138 break;
139 }
140 }
141 }
142
143 G4VSensitiveDetector* sd = nullptr;
144 if (sd_mode_ == SDMode::crossing) {
145 sd = new CrossingSD("CrossingSD", detector_ids, ke_threshold_);
146 } else {
147 sd = new ScoringSD("ScoringSD", detector_ids);
148 }
149 G4SDManager::GetSDMpointer()->AddNewDetector(sd);
150
151 for (auto [lv, _] : detector_ids) {
152 lv->SetSensitiveDetector(sd);
153 }
154
155 // Install per-magnet G4FieldManagers on every logical volume whose name
156 // contains the configured pattern. Outside these volumes Geant4 never
157 // invokes the field, so drift regions stay cost-free. An unmatched
158 // pattern is treated as a hard configuration error (wrong field setup is
159 // a silent physics bug otherwise).
160 for (auto const& fr : field_source_->regions()) {
161 auto* adapter = new ship::G4MagFieldAdapter(fr.field);
162 auto* fmgr = new G4FieldManager(adapter);
163 bool matched = false;
164 for (auto* lv : volumes) {
165 if (G4StrUtil::contains(lv->GetName(),
166 std::string_view{fr.volume_pattern})) {
167 lv->SetFieldManager(fmgr, /*forceToAllDaughters=*/true);
168 matched = true;
169 }
170 }
171 if (!matched) {
172 throw std::runtime_error("Field region '" + fr.name +
173 "': volume_pattern '" + fr.volume_pattern +
174 "' matches no logical volumes");
175 }
176 }
177 }
178};
179
180} // namespace SHiP::g4
Definition geometry_source.hpp:20
virtual G4VPhysicalVolume * construct() const =0
Build or return the cached G4 world volume.
virtual std::vector< std::string > const & sensitiveVolumes() const =0
Volume names to assign sensitive detectors to.
Definition detector_construction.hpp:37
G4VPhysicalVolume * Construct() override
Definition detector_construction.hpp:99
ConfigurableDetectorConstruction(IGeometrySource const &source, ship::IFieldSource const &field_source, SDMode sd_mode=SDMode::scoring, ship::Energy ke_threshold=ship::Energy::zero(), std::vector< std::pair< std::string, ship::Length > > regions={}, std::vector< ScoringPlaneConfig > scoring_planes={})
Definition detector_construction.hpp:64
bool has_scoring_planes() const
Whether scoring planes are configured, i.e.
Definition detector_construction.hpp:93
void ConstructSDandField() override
Definition detector_construction.hpp:129
Definition geant4_sim_core.hpp:86
The parallel world holding the scoring planes.
Definition scoring_planes.hpp:515
bool contains(G4LogicalVolume *lv) const
Whether lv belongs to this world (a plane or the world volume), so that detector-geometry patterns (s...
Definition scoring_planes.hpp:543
Definition geant4_sim_core.hpp:61
Definition detector_construction.hpp:33
constexpr std::int32_t kScoringPlaneDetectorId
SimHit::detectorId of every scoring-plane hit.
Definition scoring_plane_id.hpp:21
SDMode
Definition detector_construction.hpp:35
std::unordered_map< G4LogicalVolume *, int > DetectorIdMap
Definition geant4_sim_core.hpp:33
G4double g4(ship::Length l)
Definition clhep_bridge.hpp:51