Organizing Simulation Scripts
Table of Contents
Geometry functions
In a small toy script you can define all geometry, sources, actors, physics and output in one long file. As soon as you move to realistic scanners, phantoms or treatment setups, this becomes hard to read and even harder to reuse. A simple way to improve GATE simulations is to encapsulate pieces of geometry in Python functions.
The idea is always the same. The main script creates the simulation object and the world volume. Then, for each logical detector or phantom component, it calls a function that adds volumes to the simulation and returns references to them.
A typical function to build geometry has three ingredients. First, it takes the simulation as an argument, usually named sim. Second, it has arguments for the key dimensions, materials and positions so that you do not hard code values inside. Third, it returns the important volumes or IDs that other parts of the simulation will need.
For example, consider a simple PET ring. Instead of writing all volumes directly in the main script, you can define:
def build_pet_ring(sim, inner_radius, crystal_size, n_crystals, material):
# create a mother volume for the ring
ring = sim.add_volume("Tubs", "ring")
ring.rmin = inner_radius
ring.rmax = inner_radius + crystal_size[0]
ring.dz = crystal_size[2] / 2
ring.phi_start = 0 * deg
ring.phi_total = 360 * deg
ring.material = "Air"
ring.mother = "world"
# create a crystal prototype
crystal = sim.add_volume("Box", "crystal")
crystal.size = crystal_size
crystal.material = material
crystal.mother = ring.name
# repeat crystals around the ring
sim.repeat_volume(
volume=crystal,
axis="phi",
n=n_crystals
)
return ring, crystal
The exact syntax may change with GATE versions, but the pattern remains. The main script calls build_pet_ring and receives back the ring and a prototype crystal. Other functions can use these objects to attach actors, set identifiers or modify materials without knowing how the ring has been built internally.
You should use similar functions for repeating modules, building collimators, creating water or image phantoms, and placing shielding. For example, a build_lead_shield(sim, thickness, opening_radius) function can be reused across many projects where the shield specifications change, but the basic shape remains.
Another important benefit of geometry functions is testing. You can write a small script that imports only your geometry module, creates a very short simulation and uses visualization to check the geometry. Because the geometry is in a separate function, you can test and fix it once and then reuse it in multiple simulations. When you later refactor or extend your geometry, the main scripts remain clean, with only a few function calls instead of hundreds of individual volume definitions.
Finally, geometry functions encourage the use of configuration variables. Instead of writing numbers directly in the function, you pass them as arguments from a separate configuration file or a dictionary. This way a single change to scanner dimensions or field of view propagates automatically to all geometry functions that use those parameters.
Source functions
Sources benefit from the same modular design. In practice, you will often need slightly different versions of the same conceptual source. For instance a point F-18 source for PET quality control, a uniform Tc-99m distribution in a cylinder for SPECT, or a rectangular photon beam for radiotherapy tests. If each of these is built directly in the main script, you will quickly copy and edit very similar code blocks and risk inconsistencies.
A cleaner approach is to define source builder functions. These functions should accept the simulation object and a group of source parameters, such as particle type, energy, position and activity. They then create the GATE source object, configure it and return the source name or object reference.
Here is a minimal example for a PET source:
def add_f18_point_source(sim, name, position, activity):
src = sim.add_source("GenericSource", name)
src.particle = "e+"
src.position.type = "point"
src.position.translation = position
src.activity = activity
src.energy.type = "mono"
src.energy.mono = 0.635 * MeV
return srcA main script that sets up several acquisitions can then call:
src_center = add_f18_point_source(sim, "center", [0, 0, 0], 1 * MBq)
src_offaxis = add_f18_point_source(sim, "offaxis", [20 * mm, 0, 0], 1 * MBq)
The advantage is not just code reuse. If at some point you decide to represent the F-18 spectrum with a more realistic energy distribution, you change the definition only in add_f18_point_source. All simulations that use this function get the updated source model without further edits.
For more complex problems you may define higher level functions, such as add_pet_nema_phantom_sources or add_uniform_cylinder_source. These may internally call several lower level functions or create multiple GATE sources at once. You can store all source functions in a dedicated file, for instance sources.py, and import them into different projects. This keeps your main simulation scripts focused on the scenario definition rather than technical details of source configuration.
It is also helpful to design source functions that accept a configuration dictionary. For example:
def add_generic_source(sim, cfg):
src = sim.add_source("GenericSource", cfg["name"])
src.particle = cfg["particle"]
# configure position, energy and activity using cfg entries
return srcWith this pattern, you can define different sources in a separate JSON, YAML or Python dictionary file, and simply loop over the configurations. This avoids multiple similar code fragments and makes it easier to run parameter studies where only a few source parameters change between simulations.
Actor functions
Actors are naturally modular objects, and functions are an effective way to keep their configuration organized. In a typical GATE project you will use several actors for statistics, dose, energy deposition, phase space or hits. The details of these actors, such as the volume on which they operate, the resolution of dose grids or the filters that they apply, tend to be project specific but follow consistent patterns.
An actor function should again receive the simulation object and a set of parameters, create and configure the actor, and return it. For example, for a generic simulation statistics actor:
def add_simulation_statistics(sim, name="stats"):
act = sim.add_actor("SimulationStatisticsActor", name)
act.track_types_flag = True
act.energy_flag = True
act.verbose_level = 1
return act
Any main script can now call add_simulation_statistics(sim) and be sure that statistics are collected in a consistent way across projects.
Dose and energy deposition actors are particularly well suited to functions because they require many grid parameters. Instead of repeating a complicated block each time, you can define:
def add_dose_actor(sim, name, output, size, spacing, translation, mother):
act = sim.add_actor("DoseActor", name)
act.mother = mother
act.output = output
act.size = size
act.spacing = spacing
act.translation = translation
act.save_true_dose = True
act.save_uncertainty = True
return actWhen you later change the voxel size or decide to record an extra attribute, you modify this single function. All simulations using it will automatically apply the new settings.
For detector simulations, a similar pattern works with hits, singles and coincidence related actors. A function add_crystal_energy_deposition_actor or add_phase_space_actor_at_isocenter makes it clear in the main script what information is recorded and where. The technical details of which attributes are saved, what filters are used and how output files are named remain encapsulated in the actor function.
You can also combine actor functions with geometry functions. For example, after building a water phantom with build_water_phantom, you can immediately call a dedicated function attach_phantom_dose_actors(sim, phantom_volume, cfg) that uses the phantom dimensions to set up an appropriate dose grid. This reduces the chance of mismatches between geometry and scoring volumes.
Organizing actor creation in one or more Python modules has a further benefit for analysis and reproducibility. If you always use the same actor functions for a class of problems, you know that dose maps, statistics or phase space files have been produced with consistent settings. When you publish results or compare simulations, the configuration is documented in one central place.
Views: 12
KAHIBARO