Skip to content

Commit b6daa89

Browse files
authored
Merge pull request #835 from OpenGATE/debug_slow_dose_actor
Tests pass. Important to merge this as it fixes a bug.
2 parents dd46ee6 + 790067b commit b6daa89

19 files changed

Lines changed: 300 additions & 70 deletions
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
Physics
2+
=======
3+
4+
This chapter documents implementation details of GATE's physics handling for
5+
developers. It focuses on the Python-side orchestration around Geant4 physics
6+
objects, i.e. the code that turns user-facing settings into Geant4 regions,
7+
physics constructors, processes, and cuts.
8+
9+
Step Limiter
10+
------------
11+
12+
The step limiter is implemented through Geant4 user limits. On the GATE side,
13+
the relevant code is split across three places:
14+
15+
* ``opengate.managers.PhysicsManager`` exposes the user-facing API.
16+
* ``opengate.physics.Region`` stores per-region user limits and builds the
17+
corresponding Geant4 region objects.
18+
* ``opengate.physics.UserLimitsPhysics`` is a Geant4 physics constructor that
19+
adds the required Geant4 processes to the selected particles.
20+
21+
User-facing entry points
22+
~~~~~~~~~~~~~~~~~~~~~~~~
23+
24+
Users set the maximum step size either through the physics manager:
25+
26+
.. code-block:: python
27+
28+
sim.physics_manager.set_max_step_size(volume.name, 1 * gate.g4_units.mm)
29+
30+
or through the volume convenience method:
31+
32+
.. code-block:: python
33+
34+
volume.set_max_step_size(1 * gate.g4_units.mm)
35+
36+
Internally, both paths associate the volume with a ``Region`` object and store
37+
the value in ``region.user_limits["max_step_size"]``. If no region exists yet
38+
for the volume, ``PhysicsManager.find_or_create_region()`` creates one. The
39+
world volume is associated with Geant4's ``DefaultRegionForTheWorld``; other
40+
volumes use a GATE-created region named ``<volume_name>_region`` by default.
41+
42+
The particles to which user limits are applied are selected with:
43+
44+
.. code-block:: python
45+
46+
sim.physics_manager.user_limits_particles = ["proton", "GenericIon"]
47+
48+
This setting is a list-like user input. A single string is accepted by the
49+
setter hook and converted to a one-element list. The special values are:
50+
51+
* ``"all"``: apply user limits to all Geant4 particles known to the particle
52+
table.
53+
* ``"all_charged"``: add the step limiter to charged particles, following
54+
Geant4's ``G4StepLimiterPhysics`` behaviour.
55+
56+
Particle names are intentionally not limited to the production-cut aliases
57+
(``gamma``, ``electron``, ``positron``, ``proton``). Any Geant4 particle name is
58+
accepted, after applying the small GATE-to-Geant4 alias translation implemented
59+
by ``translate_particle_name_gate_to_geant4()``.
60+
61+
Region initialization
62+
~~~~~~~~~~~~~~~~~~~~~
63+
64+
During simulation initialization, every ``Region`` runs
65+
``Region.initialize_g4_user_limits()``. If none of the user-limit fields is set,
66+
no ``G4UserLimits`` object is created. Otherwise, GATE creates one and fills all
67+
limits:
68+
69+
* ``max_step_size`` maps to ``G4UserLimits.SetMaxAllowedStep()``.
70+
* ``max_track_length`` maps to ``SetUserMaxTrackLength()``.
71+
* ``max_time`` maps to ``SetUserMaxTime()``.
72+
* ``min_ekine`` maps to ``SetUserMinEkine()``.
73+
* ``min_range`` maps to ``SetUserMinRange()``.
74+
75+
Unset upper limits are replaced by ``FLOAT_MAX`` and unset lower limits by
76+
``0``. This lets one ``G4UserLimits`` object represent only the limits that the
77+
user actually requested.
78+
79+
After that, ``Region.initialize_g4_region()`` creates or finds the Geant4
80+
``G4Region``, attaches the ``G4UserLimits`` object to it, and adds the root
81+
logical volumes associated with the GATE region. This is the part that tells
82+
Geant4 where the user-limit values apply.
83+
84+
Registering the Geant4 processes
85+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
86+
87+
A ``G4UserLimits`` object alone is not sufficient. Geant4 also needs tracking
88+
processes attached to particles so that the limits are actually enforced.
89+
90+
The physics engine checks all regions in
91+
``PhysicsEngine.initialize_user_limits_physics()``:
92+
93+
.. code-block:: python
94+
95+
if region.need_step_limiter():
96+
need_step_limiter = True
97+
if region.need_user_special_cut():
98+
need_user_special_cut = True
99+
100+
If at least one region needs a maximum step size or another user special cut,
101+
GATE registers ``UserLimitsPhysics`` on the active Geant4 physics list.
102+
103+
``UserLimitsPhysics.ConstructProcess()`` then iterates over the full Geant4
104+
particle table and decides, particle by particle, whether to add:
105+
106+
* ``G4StepLimiter("StepLimiter")`` for ``max_step_size``.
107+
* ``G4UserSpecialCuts("UserSpecialCut")`` for the other user limits.
108+
109+
For explicit particle names and for ``"all"``, GATE adds both processes. For
110+
``"all_charged"``, GATE only adds ``G4StepLimiter`` to charged particles. This
111+
mirrors Geant4's default step-limiter constructor behaviour and avoids applying
112+
the other special cuts unless the user explicitly asked for the particle.
113+
114+
The created Geant4 process objects are stored in
115+
``UserLimitsPhysics.g4_step_limiter_storage`` and
116+
``UserLimitsPhysics.g4_special_user_cuts_storage``. This storage is important:
117+
the objects are created from Python via pybind11, and keeping references avoids
118+
their garbage collection after ``ConstructProcess()`` returns.
119+
120+
Validation and name translation
121+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
122+
123+
Before processes are added, ``UserLimitsPhysics`` validates all explicitly named
124+
particles against ``G4ParticleTable``. Unknown particles are rejected with a
125+
fatal error. The special selectors ``"all"`` and ``"all_charged"`` are removed
126+
before validation.
127+
128+
The translation helper ``translate_particle_name_gate_to_geant4()`` maps the
129+
historical GATE names used for production cuts to Geant4 names:
130+
131+
* ``electron`` -> ``e-``
132+
* ``positron`` -> ``e+``
133+
* ``gamma`` -> ``gamma``
134+
* ``proton`` -> ``proton``
135+
136+
Names not present in this alias table are passed through unchanged. This is what
137+
allows inputs such as ``"GenericIon"`` or other Geant4 particle names to work
138+
without extending the production-cut particle list.
139+
140+
Important distinction from production cuts
141+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
142+
143+
Production cuts and user limits have different particle-name scopes in GATE.
144+
Production cuts are still constrained to the particle aliases in
145+
``cut_particle_names`` because that mirrors Geant4 production-cut usage in GATE.
146+
User limits, including the step limiter, are more general: Geant4 can apply them
147+
to any particle with a process manager.
148+
149+
This distinction is why ``sim.physics_manager.user_limits_particles = "all"``
150+
means all Geant4 particles for user limits. Tests that need the historical
151+
GATE behaviour should explicitly request:
152+
153+
.. code-block:: python
154+
155+
sim.physics_manager.user_limits_particles = [
156+
"proton",
157+
"gamma",
158+
"electron",
159+
"positron",
160+
]
161+

docs/source/developer_guide/index.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ Developer guide
1818
developer_guide_dynamic_parametrisations
1919
developer_guide_auxiliary_attributes
2020

21+
.. toctree::
22+
:caption: Physics
23+
:maxdepth: 2
24+
25+
developer_guide_physics
26+
2127
.. toctree::
2228
:caption: Good to know
2329
:maxdepth: 2

docs/source/user_guide/user_guide_physics.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ Additionally, you need to tell GATE to which particles you want to apply the ste
131131

132132
.. code-block:: python
133133
134-
sim.physics_manager.set_user_limits_particles(['electron', 'positron'])
134+
sim.physics_manager.user_limits_particles = ['electron', 'positron']
135135
136136
There are other user limits like ''maximum track length'' and ''minimium kinetic energy'', that are used in analogy to the ''maximum step size''.
137137
You can also use Regions if your geometry is complex. Have a look at the section :ref:`user-limits-details-label` in the detailed part of this user guide for more info.

opengate/managers.py

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,13 @@ def _setter_hook_physics_list_name(self, physics_list_name):
573573
return physics_list_name
574574

575575

576+
def _setter_hook_user_limits_particles(self, particle_names):
577+
if not isinstance(particle_names, (list, set, tuple)):
578+
return list([particle_names])
579+
else:
580+
return particle_names
581+
582+
576583
class PhysicsManager(GateObject):
577584
"""
578585
Everything related to the physics (lists, cuts, etc.) should be here.
@@ -629,18 +636,11 @@ class PhysicsManager(GateObject):
629636
},
630637
),
631638
"user_limits_particles": (
632-
Box(
633-
[
634-
("all", False),
635-
("all_charged", True),
636-
("gamma", False),
637-
("electron", False),
638-
("positron", False),
639-
("proton", False),
640-
]
641-
),
639+
["all_charged"],
642640
{
643-
"doc": "Switch on (True) or off (False) UserLimits, e.g. step limiter, for individual particles. Default: Step limiter is applied to all charged particles (in accordance with G4 default)."
641+
"doc": "List of particles to which UserLimits, e.g. step limiter, are applied. Default: Step limiter "
642+
"is applied to all charged particles (in accordance with G4 default).",
643+
"setter_hook": _setter_hook_user_limits_particles,
644644
},
645645
),
646646
"em_parameters": (
@@ -879,8 +879,12 @@ def _normalize_volume_name(self, volume):
879879
fatal(f"Expected a volume name or a volume object, but received: {volume}")
880880

881881
def find_or_create_region(self, volume_name):
882+
if volume_name == self.simulation.world.name:
883+
region_name = "DefaultRegionForTheWorld"
884+
else:
885+
region_name = volume_name + "_region"
882886
if volume_name not in self.volumes_regions_lut:
883-
region = self.add_region(volume_name + "_region")
887+
region = self.add_region(region_name)
884888
region.associate_volume(volume_name)
885889
else:
886890
region = self.volumes_regions_lut[volume_name]
@@ -981,19 +985,29 @@ def set_track_structure_em_physics_in_region(
981985
region.track_structure_em_physics = track_structure_em_physics
982986

983987
def set_user_limits_particles(self, particle_names):
984-
if not isinstance(particle_names, (list, set, tuple)):
985-
particle_names = list([particle_names])
986-
for pn in list(particle_names):
987-
# try to get current value to check if particle_name is eligible
988-
try:
989-
_ = self.user_info.user_limits_particles[pn]
990-
except KeyError:
991-
fatal(
992-
f"Found unknown particle name '{pn}' in set_user_limits_particles(). Eligible names are "
993-
+ ", ".join(list(self.user_info.user_limits_particles.keys()))
994-
+ "."
995-
)
996-
self.user_info.user_limits_particles[pn] = True
988+
raise GateDeprecationError(
989+
"The function set_user_limits_particles has been removed. Set the particle(s) directly via: \n"
990+
"sim.physics_manager.user_limits_particles = XXX, \n"
991+
"e.g.\n"
992+
"sim.physics_manager.user_limits_particles = 'proton'\n"
993+
"or \n"
994+
"sim.physics_manager.user_limits_particles = ['gamma', 'electron']"
995+
)
996+
# if not isinstance(particle_names, (list, set, tuple)):
997+
# particle_names = list([particle_names])
998+
# self.user_info.user_limits_particles = particle_names
999+
1000+
# for pn in list(particle_names):
1001+
# # try to get current value to check if particle_name is eligible
1002+
# try:
1003+
# _ = self.user_info.user_limits_particles[pn]
1004+
# except KeyError:
1005+
# fatal(
1006+
# f"Found unknown particle name '{pn}' in set_user_limits_particles(). Eligible names are "
1007+
# + ", ".join(list(self.user_info.user_limits_particles.keys()))
1008+
# + "."
1009+
# )
1010+
# self.user_info.user_limits_particles[pn] = True
9971011

9981012
def freeze_config(self):
9991013
# Freeze the Python-side configuration before any SimulationEngine is

opengate/physics.py

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -74,42 +74,44 @@ def ConstructProcess(self):
7474
"""
7575
ui = self.physics_engine.user_info_physics_manager
7676

77-
particle_keys_to_consider = []
78-
# 'all' overrides individual settings
79-
if ui.user_limits_particles["all"] is True:
80-
particle_keys_to_consider = list(ui.user_limits_particles.keys())
81-
else:
82-
keys_to_exclude = ("all", "all_charged")
83-
particle_keys_to_consider = [
84-
p
85-
for p, v in ui.user_limits_particles.items()
86-
if v is True and p not in keys_to_exclude
87-
]
88-
89-
if len(particle_keys_to_consider) == 0:
77+
if len(ui.user_limits_particles) == 0:
9078
self.physics_engine.simulation_engine.simulation.warn_user(
91-
"user_limits_particles is False for all particles. No tracking cuts will be applied. Use sim.physics_manager.set_user_limits_particles()."
79+
"No particles selected to which UserLimits (incl. step limiter) will be applied. No tracking cuts will be applied. Use sim.physics_manager.set_user_limits_particles()."
9280
)
9381

94-
# translate to Geant4 particle names
82+
# process user input parameters
83+
flag_all = "all" in ui.user_limits_particles
84+
flag_all_charged = "all_charged" in ui.user_limits_particles
9585
particles_to_consider = [
96-
translate_particle_name_gate_to_geant4(k) for k in particle_keys_to_consider
86+
translate_particle_name_gate_to_geant4(p)
87+
for p in ui.user_limits_particles
88+
if p not in ["all", "all_charged"]
9789
]
9890

99-
for particle in g4.G4ParticleTable.GetParticleTable().GetParticleList():
91+
known_g4_particles = list(
92+
g4.G4ParticleTable.GetParticleTable().GetParticleList()
93+
)
94+
known_g4_particle_names = [p.GetParticleName() for p in known_g4_particles]
95+
96+
# check for consistency of particle names
97+
wrong_particle_names = [
98+
p for p in particles_to_consider if p not in known_g4_particle_names
99+
]
100+
if len(wrong_particle_names) > 0:
101+
fatal(
102+
f"UserLimits were requested for the following unknown particles: {wrong_particle_names}"
103+
)
104+
105+
for particle in known_g4_particles:
100106
add_step_limiter = False
101107
add_user_special_cuts = False
102108
p_name = str(particle.GetParticleName())
103109

104-
if p_name in particles_to_consider:
110+
if flag_all or p_name in particles_to_consider:
105111
add_step_limiter = True
106112
add_user_special_cuts = True
107-
108113
# this reproduces the logic of the Geant4's G4StepLimiterPhysics class
109-
if (
110-
ui.user_limits_particles["all_charged"] is True
111-
and particle.GetPDGCharge() != 0
112-
):
114+
elif flag_all_charged and particle.GetPDGCharge() != 0:
113115
add_step_limiter = True
114116

115117
if add_step_limiter is True or add_user_special_cuts is True:

opengate/tests/src/actors/test023_interaction_counter_auxiliary_attribute.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def run_simulation(
6969
sim.physics_manager.physics_list_name = "G4EmStandardPhysics_option4"
7070
if max_step_size is not None:
7171
sim.physics_manager.set_max_step_size(slab.name, max_step_size)
72-
sim.physics_manager.set_user_limits_particles("gamma")
72+
sim.physics_manager.user_limits_particles = "gamma"
7373
sim.run(start_new_process=True)
7474

7575
tree = uproot.open(phsp.get_output_path())["phsp"]

opengate/tests/src/actors/test065_dose_from_edep_ct.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,12 @@
117117
# physics
118118
sim.physics_manager.physics_list_name = "FTFP_INCLXX_EMZ"
119119
sim.physics_manager.set_production_cut("world", "all", 1000 * km)
120-
sim.physics_manager.user_limits_particles.all = True
120+
sim.physics_manager.user_limits_particles = [
121+
"proton",
122+
"gamma",
123+
"electron",
124+
"positron",
125+
]
121126

122127
# add dose actor
123128
dose_postprocess = sim.add_actor("DoseActor", "dose_postprocess")

opengate/tests/src/actors/test081_pgtle_stage1.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def voxel_to_mat_name(UH, mat_data): # from UH to material name
230230
sim.physics_manager.global_production_cuts.proton = 0.1 * mm
231231

232232
sim.physics_manager.set_max_step_size("ct", 0.1 * mm)
233-
sim.physics_manager.set_user_limits_particles(["proton"])
233+
sim.physics_manager.user_limits_particles = ["proton"]
234234

235235
# source of proton
236236
# FIXME to replace by a more realistic proton beam, see tests 044

opengate/tests/src/actors/test081_tle_1_geom.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def main(argv):
4646
sim.physics_manager.physics_list_name = "G4EmStandardPhysics_option3"
4747
sim.physics_manager.global_production_cuts.all = 1 * mm
4848
sim.physics_manager.set_max_step_size("waterbox", 1 * mm)
49-
sim.physics_manager.set_user_limits_particles("gamma")
49+
sim.physics_manager.user_limits_particles = "gamma"
5050

5151
s = f"/process/eLoss/CSDARange true"
5252
sim.g4_commands_before_init.append(s)

opengate/tests/src/actors/test081_tle_4_high_energy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
sim.physics_manager.physics_list_name = "G4EmStandardPhysics_option3"
4747
sim.physics_manager.global_production_cuts.all = 1 * mm
4848
sim.physics_manager.set_max_step_size("waterbox", 1 * mm)
49-
sim.physics_manager.set_user_limits_particles("gamma")
49+
sim.physics_manager.user_limits_particles = "gamma"
5050

5151
s = f"/process/eLoss/CSDARange true"
5252
sim.g4_commands_before_init.append(s)

0 commit comments

Comments
 (0)