Architecture: Managers and Engines#
GATE 10 has two distinct kinds of classes which handle a simulation. Managers provide an interface to the user to set-up and configure a simulation and collect and organize user parameters in a structured way. Engines provide the interface with Geant4 and are responsible for creating all Geant4 objects. Managers and engines are divided in sub-managers and sub-engines responsible for certain logical parts of a simulation. Additionally, many objects in GATE are now implemented as classes which provide interfaces to the managers and engines.
The Simulation
class is the main manager with which the user
interacts. It collects general parameters, e.g. about verbosity and
visualization and it manages the way the simulation is run (in a
subprocess or not). Sub-managers are: VolumeManager
,
PhysicsManager
, ActorManager
, SourceManager
. These managers
can be thought of as bookkeepers. For example, the VolumeManager
keeps a dictionary with all the volumes added to a simulation, a
dictionary with all the parallel world volumes, etc. But it also
provides the user with methods to perform certain tasks,
e.g. VolumeManager.add_parallel_world()
.
The SimulationEngine
is the main driver of the Geant4 simulation:
every time the user calls sim.run()
, the Simulation
object (here
assumed to be called sim
) creates a new SimulationEngine
which
in turn creates all the sub-engines: VolumeEngine
,
PhysicsEngine
, SourceEngine
, ActorEngine
, ActionEngine
.
The method SimulationEngine.run_engine()
actually triggers the
construction and run of the Geant4 simulation. It takes the role of the
main.cc
in a pure Geant4 simulation. When the Geant4 simulation has
terminated, SimulationEngine.run_engine()
returns the simulation
output which is then collect by the Simulation
object and remains
accessible via sim.output
.
It is important to understand that the engines only exist while the GATE/Geant4 simulation is running, while the managers exist during the entire duration of the python interpreter session in which the user is setting up the simulation.
The managers and engines are explained in more detail below.
References among managers and engines#
Managers and engines frequently need to access attributes of other managers and engines. They therefore need references to each other. In GATE, these references follow a hierarchical pattern:
Sub-managers keep a references to the
Simulation
(the main manager). Sub-sub-managers keep references to the sub-manager above them. For example:PhysicsManager.simulation
refers to theSimulation
object which created it, andPhysicsListManager.physics_manager
refers to thePhysicsManager
which created it.Objects created through a manager keep a reference to the creating manager. For example: all volumes have an attribute
volume_manager
.In a similar fashion, sub-engines keep a references to the
SimulationEngine
from which they originate.The
SimulationEngine
itself keeps a reference to theSimulation
which created it.
This hierarchical network of references allows reaching objects from any
other object. For example: a Region
object is managed by the
PhysicsManager
, so from a region, a volume can be reached via:
my_volume = region.physics_manager.simulation.volume_manager.get_volume('my_volume')
Geant4 bindings#
This repository contains C++ source code that maps some (not all!)
Geant4 classes into one single Python module. It also contains
additional C++ classes that extends Geant4 functionalities (also mapped
to Python). At the end of the compilation process a single Python module
is available, named opengate_core
and is ready to use from the
Python side.
The source files are divided into two folders: g4_bindings
and
opengate_lib
. The first contains pure Geant4 Python bindings allow
to expose in Python a (small) part of Geant4 classes and functions. The
bindings are done with the
pybind11 library. The second
folder contains specific opengate functionalities.
How to add a Geant4 bindings ?#
If you want to expose another Geant4 class (or functions), you need to:
Create a
pyG4MyClass.cpp
With a function
init_G4MyClass
(see example in theg4_bindings
folder)In this function, indicate all functions/members that you want to expose.
Declare and call this init function in the
opengate_core.cpp
file.
Philosophy behind objects implemented as GateObject#
Generally, the idea is to encapsulate functionality into classes rather
than spreading out the code across managers and engines. The advantage
is that the code structure remains less cluttered. Good examples are the
Region
class and, although more complex, the volume classes. In the
following, we explain the rationale and design concept. For instructions
on how to implement or extend a class, see
here.
The GATE classes representing and a Geant4 object (or multiple Geant4
objects combined) are meant to do multiple things: 1) Be a storage for
user parameters. Exmample: the Region
class holds the user_info
user_limits
, production_cuts
, and em_switches
. 2) Provide
interface functions to manager classes (and the user) to configure the
object or inquire about it. Examples: Region.associate_volume()
,
Region.need_step_limiter()
3) Provide interface functions such as
initialize()
and close()
to the engines to handle the Geant4
objects. 4) Provide convenience functionality such as dumping as
dictionary (to_dictionary()
, from_dictionary()
), dump info about
the object (e.g. Region.dump_production_cuts()
), clone itself. 5)
Handle technical aspects such as pickling (for subprocesses) in a
unified way (via the method
``__getstate__()` <#implement-a-getstate-method-if-needed>`__)
The managers and engines, on the other hand, remain quite sleek and
clean. For example, if you look at the PhysicsEngine
class, you find
the method
def initialize_regions(self):
for region in self.physics_manager.regions.values():
region.initialize()
which really just iterates over the regions and initializes them.
The advantage of this becomes evident especially if there are multiple
variants of a class (via inheritance), such as for volumes. In this
case, the VolumeEngine
does not care about the specific type of
volume because it always calls the same interface. For example,
VolumeEngine.Construct()
(which is triggered by the G4RunManager,
not GATE) iterates over the volumes and calls volume.construct()
.
The volume object then takes care of taking the correct actions. If the
code inside each volume’s construct()
method were implemented inside
VolumeEngine.Construct()
, it would be cluttered with if statements
to pick what should be done.
NoteFor now, only a part of GATE implements objects based on the GateObject base class. Actors and Sources still need to be refactored.
How a class in GATE 10 is (usually) set up?#
Naming convention#
Use small letters and underscores for python variables. Do not use capital letters and camelcase.
Use capital letters and camel case for overloaded C++ variables if the class inherits from a base class implemented in C++.
All attributes pointing to Geant4 objects should have a “g4_” prepended for easy identification. Example:
self.g4_logical_volume
.Group the
g4_***
definitions in one block for better visual reference.
__init__()
method#
Define all attributes of the object in the
__init__()
method even if their value is set only later/elsewhere.If no value is set in
__init__()
, do:self.my_attribute = None
By defining all attributes in the
__init__()
method, other developers can easily inspect the class without reading through the entire class. Think of it as a C++ header file.If your class inherits from another class, and in particular from
GateObject
orDynamicGateObject
, include wild card arguments and keyword arguments in your__init__()
method:def __init__(self, your_specific_arguments, *args, your_specific_kwargs, **kwargs): super().__init__(*args, **kwargs) # ... YOUR CODE...
User info: handling parameters set by the user#
If your class handles user input, let it inherit from GateObject, or
DynamicGateObject if applicable. Define and configure user input via the
user_info_defaults
class attribute. See section XXX.
Important: User input defined and configured in the
user_info_defaults
dictionary should generally not be handled
manually in your __init__()
method. They are passed on to the
superclass inside the kwargs
dictionary. See section XXX for more
detail.
Initialization of Geant4 objects#
Implement an initialize()
method if Geant4 objects need to be
created by the SimulationEngine (or a sub-engine) when the simulation is
launched. The initialize()
method should not take any arguments, but
only rely on object attributes (self.xyz
) which were previously set.
Exception: G4RunManager has an initializatin sequence which GATE relies
on. In certain classes, the g4_XXX
componentes are initialized as
part of this sequence on the C++ side. Example: All volumes implement a
construct()
method which is called when the G4RunManager calls the
overloaded Construct()
method of the VolumeEngine.
Implement a close()
method if needed#
Explanation: If your class has attributes that point to Geant4 objects
which are deleted by the G4RunManager at the end of a simulation, your
class must get rid of these references when the SimulationEngine closes
down. This is achieved by a hierarchy of calls to a close() method,
starting from SimulationEngine.close()
. In your close()
method,
set all attributes pointing to Geant4 objects which the G4RunManafger
will delete to None
. If your class manages a list of other objects
which themselves need to call their close()
method, add a loop to
your close()
method and close down the list members. If you inherit
from another class, do not forget to call the close()
method from
the superclass via super().close()
. Take a look at
VolumeManager.close()
and the volumes classes or
PhysicsManager.close()
and the Region
class for examples.
Implement a __getstate__()
method if needed.#
Explanation: When a GATE simulation is run in a subprocess, all objects
need to be serialized so they can be sent to the subprocess, where they
are deserialized. The serialization is currently handled by the
pickle
module. If your class contains attributes which refer to
objects which cannot be pickled, the serialization will fail. This
typically concerns Geant4 objects. To make your class pickleable, you
should implement a __getstate__()
method. This is essentially a hook
called within the serialization pipeline which returns a representation
of your object (usually a dictionary). You should remove items which
cannot be pickled from this dictionary.
Example: Assume your class has an attribute self.g4_funny_object
referring to a Geant4 object. Your __getstate__()
method should do
something like this:
def __getstate__(self):
return_dict = self.__dict__
return_dict['g4_funny_object'] = None
return return_dict
If your class inherits from another one, e.g. from GateObject, you
should call the __getstate__()
method from the superclass:
def __getstate__(self):
return_dict = super().__getstate__()
return_dict['g4_funny_object'] = None
return return_dict
Important: The __getstate__()
method should not change your
object, but only modify the dictionary to be returned. Therefore, avoid
self.g4_funny_object = None
as this also alters your object.
Important: Do not use the close()
method in your
__getstate__()
method. The close()
method is part of another
mechanism and these mechanisms
should not be entangled. And: the close()
method would alter your
object and not only the returned dictionary representation.
Optional: Implement a __str__()
method#
You might consider implementing a __str__()
method which, by
construction, is required to return a string. If implemented, this
method is called when the user places your object inside a print()
statement: print(my_object)
. You could implement the __str__()
method to provide useful information about your object. If your object
inherits from another class, call the superclass:
def __str__(self):
s = super().__str__()
s += "*** Additional info: ***\n"
s += f"The object as an attribute 'xyz' of value {self.xyz}.\n"
return s
In particular, the GateObject superclass (and variants) implement a
__str__()
method which lists all user_info of the object.