Skip to content

Commit dd46ee6

Browse files
authored
Merge pull request #1053 from gertvanhoey/deadtime
Dead time actor
2 parents ce43354 + 65dc6a5 commit dd46ee6

11 files changed

Lines changed: 683 additions & 0 deletions

core/opengate_core/opengate_core.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,8 @@ void init_GateHitsCollectionActor(py::module &);
493493

494494
void init_GateHitsAdderActor(py::module &);
495495

496+
void init_GateDigitizerDeadTimeActor(py::module &);
497+
496498
void init_GateDigitizerPileupActor(py::module &);
497499

498500
void init_GateDigitizerReadoutActor(py::module &m);
@@ -834,6 +836,7 @@ PYBIND11_MODULE(opengate_core, m) {
834836
init_GateDigiAttributeManager(m);
835837
init_GateVDigiAttribute(m);
836838
init_GateHitsAdderActor(m);
839+
init_GateDigitizerDeadTimeActor(m);
837840
init_GateDigitizerPileupActor(m);
838841
init_GateDigitizerReadoutActor(m);
839842
init_GateDigitizerBlurringActor(m);
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/* --------------------------------------------------
2+
Copyright (C): OpenGATE Collaboration
3+
This software is distributed under the terms
4+
of the GNU Lesser General Public Licence (LGPL)
5+
See LICENSE.md for further details
6+
-------------------------------------------------- */
7+
8+
#include "GateDigitizerDeadTimeActor.h"
9+
#include "../GateHelpersDict.h"
10+
#include "GateHelpers.h"
11+
#include "GateHelpersDigitizer.h"
12+
#include <memory>
13+
14+
GateDigitizerDeadTimeActor::GateDigitizerDeadTimeActor(py::dict &user_info)
15+
: GateVDigitizerWithOutputActor(user_info, true) {
16+
fActions.insert("EndOfEventAction");
17+
fActions.insert("EndOfRunAction");
18+
}
19+
20+
GateDigitizerDeadTimeActor::~GateDigitizerDeadTimeActor() = default;
21+
22+
void GateDigitizerDeadTimeActor::InitializeUserInfo(py::dict &user_info) {
23+
24+
GateVDigitizerWithOutputActor::InitializeUserInfo(user_info);
25+
if (py::len(user_info) > 0 && user_info.contains("dead_time")) {
26+
fDeadTime = DictGetDouble(user_info, "dead_time"); // nanoseconds
27+
}
28+
if (py::len(user_info) > 0 && user_info.contains("policy")) {
29+
const auto policy_str = DictGetStr(user_info, "policy");
30+
if (policy_str == "NonParalyzable") {
31+
fPolicy = DeadTimePolicy::NonParalyzable;
32+
} else if (policy_str == "Paralyzable") {
33+
fPolicy = DeadTimePolicy::Paralyzable;
34+
} else {
35+
Fatal("Unknown dead time policy '" + policy_str + "'");
36+
}
37+
}
38+
39+
if (py::len(user_info) > 0 && user_info.contains("sorting_time")) {
40+
fSortingTime = DictGetDouble(user_info, "sorting_time"); // nanoseconds
41+
}
42+
fGroupVolumeDepth = -1;
43+
fInputDigiCollectionName = DictGetStr(user_info, "input_digi_collection");
44+
}
45+
46+
void GateDigitizerDeadTimeActor::BeginOfRunActionMasterThread(int run_id) {
47+
48+
fTimeSorter = std::make_unique<GateTimeSorter>(fOutputDigiCollectionName);
49+
fTimeSorter->Init(fInputDigiCollection);
50+
fTimeSorter->SetSortingWindow(fSortingTime);
51+
fTimeSorter->SetMaxSize(fClearEveryNEvents);
52+
53+
auto &outputIter = fTimeSorter->OutputIterator();
54+
outputIter.TrackAttribute("GlobalTime", &fTimeSorterOutputTime);
55+
outputIter.TrackAttribute("PreStepUniqueVolumeID", &fTimeSorterOutputVolID);
56+
57+
fillerOut = std::make_unique<GateDigiAttributesFiller>(
58+
fTimeSorter->OutputCollection(), fOutputDigiCollection,
59+
fTimeSorter->OutputCollection()->GetDigiAttributeNames());
60+
61+
fVolumeEndOfDeadTimeInterval.clear();
62+
}
63+
64+
void GateDigitizerDeadTimeActor::SetGroupVolumeDepth(const int depth) {
65+
fGroupVolumeDepth = depth;
66+
}
67+
68+
void GateDigitizerDeadTimeActor::DigitInitialize(
69+
const std::vector<std::string> &attributes_not_in_filler) {
70+
71+
auto a = attributes_not_in_filler;
72+
GateVDigitizerWithOutputActor::DigitInitialize(a);
73+
74+
fOutputDigiCollection->RootInitializeTupleForWorker();
75+
}
76+
77+
void GateDigitizerDeadTimeActor::EndOfEventAction(const G4Event *) {
78+
79+
fTimeSorter->OnEndOfEventAction([this]() { ProcessTimeSortedDigis(); });
80+
}
81+
82+
void GateDigitizerDeadTimeActor::EndOfRunAction(const G4Run *) {
83+
84+
fTimeSorter->OnEndOfRunAction(
85+
[this]() { fOutputDigiCollection->FillToRootIfNeeded(true); },
86+
[this]() { ProcessTimeSortedDigis(); });
87+
}
88+
89+
void GateDigitizerDeadTimeActor::ProcessTimeSortedDigis() {
90+
91+
auto &iter = fTimeSorter->OutputIterator();
92+
iter.GoToBegin();
93+
while (!iter.IsAtEnd()) {
94+
95+
const auto volHash =
96+
fTimeSorterOutputVolID->get()->GetIdUpToDepthAsHash(fGroupVolumeDepth);
97+
std::optional<double> endTime{};
98+
auto it = fVolumeEndOfDeadTimeInterval.find(volHash);
99+
if (it != fVolumeEndOfDeadTimeInterval.end()) {
100+
endTime = it->second;
101+
}
102+
103+
const auto currentTime = *fTimeSorterOutputTime;
104+
if (!endTime.has_value() || currentTime > *endTime) {
105+
// Digi goes to the output.
106+
fillerOut->Fill(iter.fIndex);
107+
// Update end of dead time interval.
108+
fVolumeEndOfDeadTimeInterval[volHash] = currentTime + fDeadTime;
109+
} else {
110+
// Digi is dropped because it arrived during a dead time interval.
111+
if (fPolicy == DeadTimePolicy::NonParalyzable) {
112+
// End of dead time interval does not change.
113+
} else if (fPolicy == DeadTimePolicy::Paralyzable) {
114+
// End of dead time interval is updated.
115+
fVolumeEndOfDeadTimeInterval[volHash] = currentTime + fDeadTime;
116+
} else {
117+
Fatal("Unknown dead time policy");
118+
}
119+
}
120+
121+
iter++;
122+
}
123+
fTimeSorter->MarkOutputAsProcessed();
124+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* --------------------------------------------------
2+
Copyright (C): OpenGATE Collaboration
3+
This software is distributed under the terms
4+
of the GNU Lesser General Public Licence (LGPL)
5+
See LICENSE.md for further details
6+
-------------------------------------------------- */
7+
8+
#ifndef GateDigitizerDeadTimeActor_h
9+
#define GateDigitizerDeadTimeActor_h
10+
11+
#include "GateDigiCollection.h"
12+
#include "GateTimeSorter.h"
13+
#include "GateVDigitizerWithOutputActor.h"
14+
#include <G4Cache.hh>
15+
#include <G4Navigator.hh>
16+
#include <map>
17+
#include <memory>
18+
#include <pybind11/stl.h>
19+
#include <queue>
20+
21+
namespace py = pybind11;
22+
23+
/*
24+
* Digitizer module for dead time.
25+
*/
26+
27+
class GateDigitizerDeadTimeActor : public GateVDigitizerWithOutputActor {
28+
29+
public:
30+
explicit GateDigitizerDeadTimeActor(py::dict &user_info);
31+
32+
~GateDigitizerDeadTimeActor() override;
33+
34+
void InitializeUserInfo(py::dict &user_info) override;
35+
36+
void BeginOfRunActionMasterThread(int run_id) override;
37+
38+
void EndOfEventAction(const G4Event *event) override;
39+
40+
void EndOfRunAction(const G4Run *run) override;
41+
42+
void SetGroupVolumeDepth(int depth);
43+
44+
protected:
45+
enum class DeadTimePolicy {
46+
NonParalyzable,
47+
Paralyzable,
48+
};
49+
50+
void DigitInitialize(
51+
const std::vector<std::string> &attributes_not_in_filler) override;
52+
53+
// User parameters
54+
double fDeadTime;
55+
DeadTimePolicy fPolicy;
56+
double fSortingTime;
57+
int fGroupVolumeDepth;
58+
59+
std::unique_ptr<GateTimeSorter> fTimeSorter;
60+
std::map<uint64_t, double> fVolumeEndOfDeadTimeInterval;
61+
62+
// Tracking pointers used by GateTimeSorter output iterator.
63+
GateUniqueVolumeID::Pointer *fTimeSorterOutputVolID{};
64+
double *fTimeSorterOutputTime{};
65+
66+
std::unique_ptr<GateDigiAttributesFiller> fillerOut;
67+
68+
void ProcessTimeSortedDigis();
69+
};
70+
71+
#endif // GateDigitizerDeadTimeActor_h
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* --------------------------------------------------
2+
Copyright (C): OpenGATE Collaboration
3+
This software is distributed under the terms
4+
of the GNU Lesser General Public Licence (LGPL)
5+
See LICENSE.md for further details
6+
-------------------------------------------------- */
7+
8+
#include <pybind11/pybind11.h>
9+
#include <pybind11/stl.h>
10+
11+
namespace py = pybind11;
12+
13+
#include "GateDigitizerDeadTimeActor.h"
14+
15+
void init_GateDigitizerDeadTimeActor(py::module &m) {
16+
17+
py::class_<GateDigitizerDeadTimeActor,
18+
std::unique_ptr<GateDigitizerDeadTimeActor, py::nodelete>,
19+
GateVDigitizerWithOutputActor>(m, "GateDigitizerDeadTimeActor")
20+
.def(py::init<py::dict &>())
21+
.def("SetGroupVolumeDepth",
22+
&GateDigitizerDeadTimeActor::SetGroupVolumeDepth);
23+
}

docs/source/user_guide/user_guide_reference_actors.rst

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,49 @@ Reference
878878
.. autoclass:: opengate.actors.digitizers.DigitizerEfficiencyActor
879879

880880

881+
DigitizerDeadTimeActor
882+
----------------------
883+
884+
Description
885+
~~~~~~~~~~~
886+
887+
Dead time is the period following the detection of a single during which a detector cannot register a new event.
888+
The :class:`~.opengate.actors.digitizers.DigitizerDeadTimeActor` simulates this effect by dropping singles that arrive within the dead time interval
889+
after a previously accepted single, on a per-volume basis (set by `group_volume`).
890+
891+
Two policies are available, controlled by the `policy` parameter:
892+
893+
* **NonParalyzable** (default): the dead time interval is fixed in duration, starting from each accepted single.
894+
Singles arriving during that interval are discarded, but do not extend it. This models, for example,
895+
a detector that keeps accepting events after the dead time has elapsed regardless of what happened during the interval.
896+
* **Paralyzable**: every single that arrives during the current dead time interval extends it by `dead_time` from the time of that single.
897+
If the rate is very high, the detector can become fully paralyzed because each new single keeps resetting the interval.
898+
899+
The actor internally time-sorts its input digis using a buffer window controlled by `sorting_time` before applying the dead time logic.
900+
This is necessary to correctly handle events from different threads or runs that may arrive out of order.
901+
902+
.. code-block:: python
903+
904+
ns = gate.g4_units.ns
905+
906+
dt = sim.add_actor("DigitizerDeadTimeActor", "Singles_after_deadtime")
907+
dt.attached_to = crystal.name
908+
dt.authorize_repeated_volumes = True
909+
dt.input_digi_collection = "Singles"
910+
dt.group_volume = crystal.name
911+
dt.dead_time = 1.0 * ns
912+
dt.policy = "NonParalyzable" # or "Paralyzable"
913+
dt.output_filename = "singles.root"
914+
915+
Refer to `test100 <https://github.com/OpenGATE/opengate/blob/master/opengate/tests/src/actors/test100_deadtime_actor.py>`_ for an example.
916+
The multi-threaded variant is in `test100_deadtime_actor_mt <https://github.com/OpenGATE/opengate/blob/master/opengate/tests/src/actors/test100_deadtime_actor_mt.py>`_.
917+
918+
Reference
919+
~~~~~~~~~
920+
921+
.. autoclass:: opengate.actors.digitizers.DigitizerDeadTimeActor
922+
923+
881924
DigitizerPileupActor
882925
--------------------
883926

opengate/actors/digitizers.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,93 @@ def EndSimulationAction(self):
538538
g4.GateDigitizerBlurringActor.EndSimulationAction(self)
539539

540540

541+
class DigitizerDeadTimeActor(DigitizerWithRootOutput, g4.GateDigitizerDeadTimeActor):
542+
"""
543+
Dititizer module for simulating dead time
544+
(drops singles that occur briefly after a previous one, in the same volume).
545+
"""
546+
547+
user_info_defaults = {
548+
"input_digi_collection": (
549+
"Singles",
550+
{
551+
"doc": "Digi collection to be used as input.",
552+
},
553+
),
554+
"dead_time": (
555+
0,
556+
{
557+
"doc": "Time interval after one digi during which following digis are dropped",
558+
},
559+
),
560+
"policy": (
561+
"NonParalyzable",
562+
{
563+
"doc": "Policy controlling whether the dead time interval is extended when new digis occur. "
564+
+ "NonParalyzable: the dead time interval is fixed and does not change when new digis occur during that interval. "
565+
+ "Paralyzable: when a new digi arrives during the current dead time interval, the interval is extended to last until dead_time after the new digi. ",
566+
"allowed_values": (
567+
"NonParalyzable",
568+
"Paralyzable",
569+
),
570+
},
571+
),
572+
"group_volume": (
573+
None,
574+
{
575+
"doc": "Name of the volume in which the dead time effect takes place",
576+
},
577+
),
578+
"clear_every": (
579+
1e5,
580+
{
581+
"doc": "The memory consumed by the actor is minimized after having processed the specified amount of digis",
582+
},
583+
),
584+
"sorting_time": (
585+
1e3,
586+
{
587+
"doc": "Time interval during which digis are buffered for time-sorting",
588+
},
589+
),
590+
"skip_attributes": (
591+
[],
592+
{
593+
"doc": "Attributes to be omitted from the output.",
594+
},
595+
),
596+
}
597+
598+
def __init__(self, *args, **kwargs):
599+
DigitizerBase.__init__(self, *args, **kwargs)
600+
self.__initcpp__()
601+
602+
def __initcpp__(self):
603+
g4.GateDigitizerDeadTimeActor.__init__(self, self.user_info)
604+
self.AddActions({"StartSimulationAction", "EndSimulationAction"})
605+
606+
def initialize(self):
607+
DigitizerBase.initialize(self)
608+
self.InitializeUserInfo(self.user_info)
609+
self.InitializeCpp()
610+
611+
def set_group_by_depth(self):
612+
depth = -1
613+
if self.user_info.group_volume is not None:
614+
depth = self.simulation.volume_manager.get_volume(
615+
self.user_info.group_volume
616+
).volume_depth_in_tree
617+
self.SetGroupVolumeDepth(depth)
618+
619+
def StartSimulationAction(self):
620+
DigitizerBase.StartSimulationAction(self)
621+
self.set_group_by_depth()
622+
g4.GateDigitizerDeadTimeActor.StartSimulationAction(self)
623+
624+
def EndSimulationAction(self):
625+
g4.GateDigitizerDeadTimeActor.EndSimulationAction(self)
626+
627+
541628
class DigitizerPileupActor(DigitizerWithRootOutput, g4.GateDigitizerPileupActor):
542629
"""
543630
Dititizer module for simulating pile-up
@@ -1581,6 +1668,7 @@ def volume_name(self, value):
15811668
process_cls(DigitizerWithRootOutput)
15821669
process_cls(DigitizerAdderActor)
15831670
process_cls(DigitizerBlurringActor)
1671+
process_cls(DigitizerDeadTimeActor)
15841672
process_cls(DigitizerPileupActor)
15851673
process_cls(DigitizerSpatialBlurringActor)
15861674
process_cls(DigitizerEfficiencyActor)

0 commit comments

Comments
 (0)