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
11#pragma once
12
13#include <G4FieldManager.hh>
14#include <G4LogicalVolumeStore.hh>
15#include <G4ProductionCuts.hh>
16#include <G4Region.hh>
17#include <G4SDManager.hh>
18#include <G4String.hh>
19#include <G4VUserDetectorConstruction.hh>
20#include <stdexcept>
21#include <string>
22#include <utility>
23#include <vector>
24
25#include "FieldService/G4MagFieldAdapter.h"
26#include "FieldService/IFieldSource.h"
27#include "geant4_sim_core.hpp"
28#include "geometry_source.hpp"
29
30namespace SHiP::g4 {
31
32enum class SDMode { scoring, crossing };
33
34class ConfigurableDetectorConstruction : public G4VUserDetectorConstruction {
35 IGeometrySource const*
36 source_; // non-owning; Job-layer product outlives the G4 run
37 ship::IFieldSource const* field_source_; // non-owning; may have no regions
38 SDMode sd_mode_;
39 ship::Energy ke_threshold_; // used by CrossingSD
40 std::vector<std::pair<std::string, ship::Length>>
41 regions_; // pattern -> production cut
42
43 public:
45 IGeometrySource const& source, ship::IFieldSource const& field_source,
46 SDMode sd_mode = SDMode::scoring,
47 ship::Energy ke_threshold = ship::Energy::zero(),
48 std::vector<std::pair<std::string, ship::Length>> regions = {})
49 : source_{&source},
50 field_source_{&field_source},
51 sd_mode_{sd_mode},
52 ke_threshold_{ke_threshold},
53 regions_{std::move(regions)} {}
54
55 // Called once, on ship::geometry_thread() (the run manager is initialised
56 // there — the process's single Geant4 geometry-creating thread)
57 G4VPhysicalVolume* Construct() override {
58 auto* world = source_->construct();
59
60 // Create the configured production-cut regions here, not in
61 // ConstructSDandField: G4Region is a split class, so regions must be
62 // created on the process's single geometry-creating thread — this one —
63 // while ConstructSDandField runs on worker threads (#80). Construct()
64 // is also the canonical Geant4 place: the regions exist before any
65 // worker builds its per-region physics tables.
66 for (auto const& [pattern, cut] : regions_) {
67 auto* region = new G4Region(pattern);
68 auto* cuts = new G4ProductionCuts();
69 cuts->SetProductionCut(aegir::clhep::g4(cut));
70 region->SetProductionCuts(cuts);
71 bool matched = false;
72 for (auto* lv : *G4LogicalVolumeStore::GetInstance()) {
73 if (G4StrUtil::contains(lv->GetName(), std::string_view{pattern})) {
74 region->AddRootLogicalVolume(lv);
75 matched = true;
76 }
77 }
78 if (!matched)
79 throw std::runtime_error("Production-cut region '" + pattern +
80 "' matches no logical volumes");
81 }
82
83 return world;
84 }
85
86 void ConstructSDandField() override {
87 auto const& sv_names = source_->sensitiveVolumes();
88 DetectorIdMap detector_ids;
89
90 for (auto* lv : *G4LogicalVolumeStore::GetInstance()) {
91 for (int i = 0; i < static_cast<int>(sv_names.size()); ++i) {
92 if (G4StrUtil::contains(lv->GetName(), std::string_view{sv_names[i]})) {
93 detector_ids.emplace(lv, i);
94 break;
95 }
96 }
97 }
98
99 G4VSensitiveDetector* sd = nullptr;
100 if (sd_mode_ == SDMode::crossing) {
101 sd = new CrossingSD("CrossingSD", detector_ids, ke_threshold_);
102 } else {
103 sd = new ScoringSD("ScoringSD", detector_ids);
104 }
105 G4SDManager::GetSDMpointer()->AddNewDetector(sd);
106
107 for (auto [lv, _] : detector_ids) {
108 lv->SetSensitiveDetector(sd);
109 }
110
111 // Install per-magnet G4FieldManagers on every logical volume whose name
112 // contains the configured pattern. Outside these volumes Geant4 never
113 // invokes the field, so drift regions stay cost-free. An unmatched
114 // pattern is treated as a hard configuration error (wrong field setup is
115 // a silent physics bug otherwise).
116 for (auto const& fr : field_source_->regions()) {
117 auto* adapter = new ship::G4MagFieldAdapter(fr.field);
118 auto* fmgr = new G4FieldManager(adapter);
119 bool matched = false;
120 for (auto* lv : *G4LogicalVolumeStore::GetInstance()) {
121 if (G4StrUtil::contains(lv->GetName(),
122 std::string_view{fr.volume_pattern})) {
123 lv->SetFieldManager(fmgr, /*forceToAllDaughters=*/true);
124 matched = true;
125 }
126 }
127 if (!matched)
128 throw std::runtime_error("Field region '" + fr.name +
129 "': volume_pattern '" + fr.volume_pattern +
130 "' matches no logical volumes");
131 }
132 }
133};
134
135} // 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:34
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={})
Definition detector_construction.hpp:44
G4VPhysicalVolume * Construct() override
Definition detector_construction.hpp:57
void ConstructSDandField() override
Definition detector_construction.hpp:86
Definition geant4_sim_core.hpp:76
Definition geant4_sim_core.hpp:53
Definition detector_construction.hpp:30
SDMode
Definition detector_construction.hpp:32
std::unordered_map< G4LogicalVolume *, int > DetectorIdMap
Definition geant4_sim_core.hpp:33
G4double g4(ship::Length l)
Definition clhep_bridge.hpp:51