From 782a051fef413f30f388b40187c1f4743ca1f880 Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Sun, 15 Sep 2024 16:00:27 +0200 Subject: [PATCH 01/18] hints from IDE --- opengate/actors/actoroutput.py | 153 +++++---------------------------- opengate/actors/arfactors.py | 6 +- opengate/actors/base.py | 2 +- opengate/base.py | 12 +++ 4 files changed, 37 insertions(+), 136 deletions(-) diff --git a/opengate/actors/actoroutput.py b/opengate/actors/actoroutput.py index 311fd2a43..d193035b8 100644 --- a/opengate/actors/actoroutput.py +++ b/opengate/actors/actoroutput.py @@ -1,6 +1,5 @@ -from pathlib import Path -from copy import deepcopy from box import Box +from typing import Optional from ..base import GateObject from ..utility import insert_suffix_before_extension, ensure_filename_is_str @@ -198,6 +197,10 @@ def _setter_hook_active(self, active): class ActorOutputBase(GateObject): + # hints for IDE + belongs_to: str + keep_data_in_memory: bool + _default_interface_class = BaseUserInterfaceToActorOutput default_suffix = None @@ -217,15 +220,7 @@ class ActorOutputBase(GateObject): "Otherwise, it is only stored on disk and needs to be re-loaded manually. " "Careful: Large data structures like a phase space need a lot of memory.", }, - ), - # "active": ( - # True, - # { - # "doc": "Should this output be calculated by the actor? " - # "Note: Output can be deactivated on in certain actors. ", - # "setter_hook": _setter_hook_active, - # }, - # ), + ) } @classmethod @@ -251,27 +246,6 @@ def __init__(self, *args, **kwargs): def __len__(self): return len(self.data_per_run) - # def __getitem__(self, which): - # return self.get_data(which, None) - # - # @property - # def active(self): - # return self._active - - # @property - # def write_to_disk(self): - # d = Box([(k, v["write_to_disk"]) for k, v in self.data_item_config.items()]) - # if len(d) > 1: - # return d - # elif len(d) == 1: - # return list(d.values())[0] - # else: - # fatal("Nothing defined in data_item_config. ") - # - # @write_to_disk.setter - # def write_to_disk(self, value): - # self.set_write_to_disk("all", value) - def set_write_to_disk(self, value, **kwargs): raise NotImplementedError @@ -333,6 +307,7 @@ def _compose_output_path(self, which, output_filename): f"of {type(self).__name__} called {self.name}" f"Valid arguments are a run index (int) or the term 'merged'. " ) + run_index = None # remove warning from IDE return insert_suffix_before_extension(full_data_path, f"run{run_index:04f}") def get_output_path(self, which="merged", **kwargs): @@ -395,6 +370,10 @@ def load_data(self, which): class MergeableActorOutput(ActorOutputBase): + # hints for IDE + merge_data_after_simulation: bool + keep_data_per_run: bool + user_info_defaults = { "merge_data_after_simulation": ( True, @@ -445,6 +424,9 @@ def end_of_simulation(self, **kwargs): class ActorOutputUsingDataItemContainer(MergeableActorOutput): + # hints for IDE + data_item_config: Optional[Box] + user_info_defaults = { "data_item_config": ( Box( @@ -479,20 +461,6 @@ def __init__(self, *args, **kwargs): f"No 'data_container_class' class attribute " f"specified for class {type(self)}." ) - # if type(data_container_class) is type: - # if DataItemContainer not in data_container_class.mro(): - # fatal(f"Illegal data container class {data_container_class}. ") - # self.data_container_class = data_container_class - # else: - # try: - # self.data_container_class = available_data_container_classes[ - # data_container_class - # ] - # except KeyError: - # fatal( - # f"Unknown data item class {data_container_class}. " - # f"Available classes are: {list(available_data_container_classes.keys())}" - # ) data_item_config = kwargs.pop("data_item_config", None) super().__init__(*args, **kwargs) if data_item_config is None: @@ -508,60 +476,6 @@ def __init__(self, *args, **kwargs): if "output_filename" not in v: v["output_filename"] = "auto" - # def initialize_output_filename(self, **kwargs): - # if self.get_output_filename(**kwargs) == 'auto': - # self.set_output_filename(self._generate_auto_output_filename(), **kwargs) - # - # for k, v in self.data_item_config.items(): - # if 'write_to_disk' in v and v['write_to_disk'] is True: - # if 'output_filename' not in v or v['output_filename'] in ['auto', '', None]: - # if len(self.data_item_config) > 0: - # item_suffix = k - # else: - # item_suffix = '' - # v['output_filename'] = f"{self.name}_from_{self.belongs_to_actor.type_name.lower()}_{self.belongs_to_actor.name}_{item_suffix}.{self.default_suffix}" - - # def get_output_path(self, **kwargs): - # item = kwargs.pop("item", "all") - # if item is None: - # return super().get_output_path(**kwargs) - # else: - # return super().get_output_path( - # output_filename=self.get_output_filename(item=item), **kwargs - # ) - # - # def get_output_filename(self, *args, item="all"): - # if item == "all": - # return Box( - # [ - # (k, str(self.compose_output_path_to_item(self.output_filename, k))) - # for k in self.data_item_config - # ] - # ) - # else: - # return str(self.compose_output_path_to_item(self.output_filename, item)) - - # def compose_output_path_to_item(self, output_path, item): - # """This method is intended to be called from an ActorOutput object which provides the path. - # It returns the amended path to the specific item, e.g. the numerator or denominator in a QuotientDataItem. - # Do not override this method. - # """ - # return insert_suffix_before_extension( - # output_path, self._get_suffix_for_item(item) - # ) - # # else: - # # return actor_output_path - - # def _get_suffix_for_item(self, identifier): - # if identifier in self.data_item_config: - # return self.data_item_config[identifier]["suffix"] - # else: - # fatal( - # f"No data item found with identifier {identifier} " - # f"in container class {self.data_container_class.__name__}. " - # f"Valid identifiers are: {list(self.data_item_config.keys())}." - # ) - def initialize_cpp_parameters(self): items = self._collect_item_identifiers("all") for h in items: @@ -597,14 +511,6 @@ def set_active(self, value, item=0): def get_active(self, item=0): items = self._collect_item_identifiers(item) return any([self.data_item_config[k]["active"] is True for k in items]) - # items = self._collect_item_identifiers(item) - # d = Box( - # [(k, self.data_item_config[k]["active"]) for k in items] - # ) - # if len(d) > 1: - # return d - # else: - # return list(d.values())[0] def set_output_filename(self, value, item=0): if item == "all": @@ -626,15 +532,6 @@ def get_output_filename(self, item=0): return self.data_item_config[item]["output_filename"] except KeyError: self._fatal_unknown_item(item) - # if f == "auto": - # if len(self.data_item_config) > 0: - # item_suffix = str(item) - # else: - # item_suffix = "" - # output_filename = f"{self.name}_from_{self.belongs_to_actor.type_name.lower()}_{self.belongs_to_actor.name}_{item_suffix}.{self.default_suffix}" - # else: - # output_filename = f - # return output_filename def get_item_suffix(self, item=0, **kwargs): if item == "all": @@ -743,6 +640,7 @@ def store_data(self, which, *data): f"Invalid argument 'which' in store_data() method of ActorOutput {self.name}. " f"Allowed values are: 'merged' or a valid run_index. " ) + run_index = None # avoid IDE warning self.data_per_run[run_index] = data_container def store_meta_data(self, which, **meta_data): @@ -776,6 +674,7 @@ def collect_data(self, which, return_identifier=False): ri = int(which) except ValueError: fatal(f"Invalid argument which in method collect_images(): {which}") + ri = None # avoid IDE warning data = [self.data_per_run[ri]] identifiers = [ri] if return_identifier is True: @@ -837,6 +736,7 @@ def get_image_properties(self, which, item=0): image_data_container = self.data_per_run[run_index] except KeyError: fatal(f"No data found for run index {run_index}.") + image_data_container = None # avoid IDE warning if image_data_container is not None: return image_data_container.get_image_properties()[item] except ValueError: @@ -856,39 +756,30 @@ def create_empty_image(self, run_index, size, spacing, origin=None, **kwargs): class ActorOutputSingleImage(ActorOutputImage): data_container_class = SingleItkImage - # def __init__(self, *args, **kwargs): - # super().__init__("SingleItkImage", *args, **kwargs) - # - class ActorOutputSingleMeanImage(ActorOutputImage): data_container_class = SingleMeanItkImage - # def __init__(self, *args, **kwargs): - # super().__init__("SingleMeanItkImage", *args, **kwargs) class ActorOutputSingleImageWithVariance(ActorOutputImage): data_container_class = SingleItkImageWithVariance - # def __init__(self, *args, **kwargs): - # super().__init__("SingleItkImageWithVariance", *args, **kwargs) - class ActorOutputQuotientImage(ActorOutputImage): data_container_class = QuotientItkImage - # def __init__(self, *args, **kwargs): - # super().__init__("QuotientItkImage", *args, **kwargs) class ActorOutputQuotientMeanImage(ActorOutputImage): data_container_class = QuotientMeanItkImage - # def __init__(self, *args, **kwargs): - # super().__init__("QuotientMeanItkImage", *args, **kwargs) - class ActorOutputRoot(ActorOutputBase): + # hints for IDE + output_filename: str + write_to_disk: bool + keep_data_in_memory: bool + user_info_defaults = { "output_filename": ( "auto", diff --git a/opengate/actors/arfactors.py b/opengate/actors/arfactors.py index 0f150e03b..87051d9e5 100644 --- a/opengate/actors/arfactors.py +++ b/opengate/actors/arfactors.py @@ -2,15 +2,13 @@ import numpy as np import itk import threading + import opengate_core as g4 from ..utility import g4_units, LazyModuleLoader -from ..exception import fatal, warning +from ..exception import fatal from .base import ActorBase - from .digitizers import ( DigitizerEnergyWindowsActor, - DigitizerBase, - DigitizerHitsCollectionActor, ) from .actoroutput import ActorOutputSingleImage, ActorOutputRoot from ..base import process_cls diff --git a/opengate/actors/base.py b/opengate/actors/base.py index 4eeb8ba91..485dcac40 100644 --- a/opengate/actors/base.py +++ b/opengate/actors/base.py @@ -2,7 +2,7 @@ from functools import wraps from ..definitions import __world_name__ -from ..exception import fatal, warning, GateImplementationError +from ..exception import fatal, GateImplementationError from ..base import GateObject from ..utility import insert_suffix_before_extension from .actoroutput import ActorOutputRoot diff --git a/opengate/base.py b/opengate/base.py index 6214207dc..c18b6fa14 100644 --- a/opengate/base.py +++ b/opengate/base.py @@ -1,5 +1,6 @@ import copy from pathlib import Path +from typing import Optional, List from box import Box import sys @@ -159,6 +160,8 @@ def add_properties_to_class(cls, user_info_defaults): "and the second item is a (possibly empty) dictionary of options.\n" ) fatal(s) + options= None # remove warning from IDE + default_value= None # remove warning from IDE if "deprecated" not in options: if not hasattr(cls, p_name): check_property_name(p_name) @@ -321,6 +324,10 @@ class GateObject: Some class attributes, e.g. inherited_user_info_defaults, are created as part of this processing. """ + # hints for IDE + name: str + inherited_user_info_defaults: dict + user_info_defaults = {"name": (None, {"required": True})} def __new__(cls, *args, **kwargs): @@ -522,6 +529,10 @@ def from_dictionary(self, d): class DynamicGateObject(GateObject): + + # hints for IDE + dynamic_params: Optional[List] + user_info_defaults = { "dynamic_params": ( None, @@ -763,6 +774,7 @@ def _get_user_info_options(user_info_name, object_type, class_module): ).inherited_user_info_defaults[user_info_name][1] except KeyError: fatal(f"Could not find user info {user_info_name} in {object_type}. ") + options = None # remove warning from IDE return options From fe0322826cdd4952c1ac320e23ec09d65cddda29 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 16 Sep 2024 08:58:52 +0200 Subject: [PATCH 02/18] add hints for IDE (actors) --- opengate/actors/base.py | 40 ++++++----------------------- opengate/actors/coincidences.py | 3 --- opengate/actors/dataitems.py | 44 +++----------------------------- opengate/actors/digitizers.py | 1 + opengate/actors/doseactors.py | 27 +++++++++++++++++--- opengate/actors/dynamicactors.py | 22 +++++++++++++++- opengate/actors/filters.py | 23 +++++++++++++++++ opengate/actors/miscactors.py | 25 ++++++++++++++++++ 8 files changed, 104 insertions(+), 81 deletions(-) diff --git a/opengate/actors/base.py b/opengate/actors/base.py index 485dcac40..e5425ba6a 100644 --- a/opengate/actors/base.py +++ b/opengate/actors/base.py @@ -75,6 +75,13 @@ def _with_check(self, *args): class ActorBase(GateObject): + + # hints for IDE + attached_to:str + filters: list + filters_boolean_operator: str + priority: int + user_info_defaults = { "attached_to": ( __world_name__, @@ -112,27 +119,7 @@ class ActorBase(GateObject): "in the list of all actions in the simulation. " "Low values mean 'early in the list', large values mean 'late in the list'. " }, - ), - # "keep_output_data": ( - # False, - # { - # "doc": "Should the output data be kept as part of this actor? " - # "If `True`, you can access the data directly after the simulation. " - # "If `False`, you need to re-read the data from disk. " - # }, - # ), - # "keep_data_per_run": ( - # False, - # { - # "doc": "In case the simulation has multiple runs, should separate results per run be kept?" - # }, - # ), - # "merge_data_from_runs": ( - # True, - # { - # "doc": "In case the simulation has multiple runs, should results from separate runs be merged?" - # }, - # ), + ) } # private list of property names for interfaces already defined @@ -200,17 +187,6 @@ def get_data(self, name=None, **kwargs): f" Example: my_actor.{list(self.interfaces_to_user_output.keys())[0]}.get_data(). " ) - # def _get_error_msg_output_filename(self): - # s = ( - # f"The shortcut attribute output_filename is not available for this actor " - # f"because it handles more than one output. You need to set the output_filename " - # f"parameter for each output individually: \n" - # ) - # for k in self.user_output: - # s += f"ACTOR.user_output.{k}.output_filename = ...\n" - # s += "... where ACTOR is your actor object." - # return s - # *** shortcut properties *** @property @shortcut_for_single_output_actor diff --git a/opengate/actors/coincidences.py b/opengate/actors/coincidences.py index 93382afca..da2a6ae43 100644 --- a/opengate/actors/coincidences.py +++ b/opengate/actors/coincidences.py @@ -1,7 +1,4 @@ import awkward as ak -import itertools -import collections -import opengate as gate from tqdm import tqdm from ..exception import fatal diff --git a/opengate/actors/dataitems.py b/opengate/actors/dataitems.py index 27e831840..5cc4a9600 100644 --- a/opengate/actors/dataitems.py +++ b/opengate/actors/dataitems.py @@ -130,6 +130,9 @@ class MeanValueDataItemMixin: overloaded methods take priority. """ + # hints for IDE + number_of_samples: int + def merge_with(self, other): result = (self * self.number_of_samples + other * other.number_of_samples) / ( self.number_of_samples + other.number_of_samples @@ -533,7 +536,6 @@ def inplace_merge_with(self, other): return self def merge_with(self, other): - # self._assert_data_is_not_none() data = [] for i in range(self._tuple_length): if ( @@ -559,36 +561,6 @@ def write(self, path, item, **kwargs): else: warning(f"Cannot write item {item} because it does not exist (=None).") - # if item is None: - # items_to_write = [ - # k - # for k, v in self.data_item_config.items() - # if v["write_to_disk"] is True - # ] - # else: - # items_to_write = [item] - # for k in items_to_write: - # full_path = self.belongs_to.compose_output_path_to_item(path, k) - # try: - # identifier = int(k) - # try: - # data_to_write = self.data[identifier] - # # self.data[i].write(full_path) - # except IndexError: - # data_to_write = None - # warning( - # f"No data for item number {identifier}. Cannot write this output" - # ) - # except ValueError: - # identifier = str(k) - # data_to_write = getattr(self, identifier) # .write(full_path) - # if data_to_write is not None: - # try: - # data_to_write.write(full_path) - # except NotImplementedError: - # warning(f"Cannot write output in data item {identifier}. ") - # continue - def __getattr__(self, item): # check if any of the data items has this attribute # exclude 'data' to avoid infinite recursion @@ -608,21 +580,11 @@ def __getattr__(self, item): f"because some contain it as a method and other as a property. " ) elif len(attributes_in_data) > 0: - # if len(attributes_in_data) != len(self.data): - # fatal( - # f"Cannot hand down request for property to data items " - # f"because not all of them contain it. " - # ) if len(attributes_in_data) == 1: return attributes_in_data[0] else: return attributes_in_data elif len(methods_in_data) > 0: - # if len(methods_in_data) != len(self.data): - # fatal( - # f"Cannot hand down request for method to data items " - # f"because not all of them contain it. " - # ) def hand_down(*args, **kwargs): return_values = [] diff --git a/opengate/actors/digitizers.py b/opengate/actors/digitizers.py index 3ea00b173..8acab48c5 100644 --- a/opengate/actors/digitizers.py +++ b/opengate/actors/digitizers.py @@ -901,6 +901,7 @@ def StartSimulationAction(self): f"No physical volume found for index {self.physical_volume_index} " f"in volume {self.attached_to_volume.name}" ) + pv = None # avoid warning from IDE align_image_with_physical_volume( self.attached_to_volume, self.user_output.projection.data_per_run[0].image ) diff --git a/opengate/actors/doseactors.py b/opengate/actors/doseactors.py index 34f0131e0..c49940bfc 100644 --- a/opengate/actors/doseactors.py +++ b/opengate/actors/doseactors.py @@ -1,4 +1,3 @@ -import itk import numpy as np from scipy.spatial.transform import Rotation @@ -12,7 +11,6 @@ from ..image import ( update_image_py_to_cpp, get_py_image_from_cpp_image, - itk_image_from_array, divide_itk_images, scale_itk_image, ) @@ -32,6 +30,17 @@ class VoxelDepositActor(ActorBase): that deposit quantities in a voxel grid, e.g. the DoseActor. """ + # hints for IDE + size: list + spacing: list + translation: list + rotation: list + repeated_volume_index: int + hit_type: str + output: str + img_coord_system: str + output_coordinate_system: str + user_info_defaults = { "size": ( [10, 10, 10], @@ -327,6 +336,10 @@ class DoseActor(VoxelDepositActor, g4.GateDoseActor): """ + # hints for IDE + use_more_ram: bool + score_in: str + user_info_defaults = { "use_more_ram": ( False, @@ -700,9 +713,12 @@ class LETActor(VoxelDepositActor, g4.GateLETActor): Options - LETd only for the moment - later: LETt, Q, fluence ... - """ + # hints for IDE + averaging_method: str + score_in: str + user_info_defaults = { "averaging_method": ( "dose_average", @@ -856,10 +872,13 @@ def EndSimulationAction(self): class FluenceActor(VoxelDepositActor, g4.GateFluenceActor): """ FluenceActor: compute a 3D map of fluence - FIXME: add scatter order and uncertainty """ + # hints for IDE + uncertainty: bool + scatter: bool + user_info_defaults = { "uncertainty": ( False, diff --git a/opengate/actors/dynamicactors.py b/opengate/actors/dynamicactors.py index f68d90f88..6b8f02083 100644 --- a/opengate/actors/dynamicactors.py +++ b/opengate/actors/dynamicactors.py @@ -1,5 +1,6 @@ -import opengate_core as g4 +from typing import Optional +import opengate_core as g4 from ..definitions import __world_name__ from ..base import GateObject from ..geometry.utility import rot_np_as_g4, vec_np_as_g4 @@ -72,6 +73,10 @@ def _setter_hook_attached_to(self, value): class GeometryChanger(GateObject): + + # hints for IDE + attached_to: Optional[str] + user_info_defaults = { "attached_to": ( None, @@ -128,6 +133,11 @@ def apply_change(self, run_id): class VolumeImageChanger(GeometryChanger): + + # hints for IDE + images: Optional[list] + label_image: Optional[dict] + user_info_defaults = { "images": ( None, @@ -151,6 +161,11 @@ def apply_change(self, run_id): class VolumeTranslationChanger(GeometryChanger): + + # hints for IDE + translations: Optional[list] + repetition_index: int + user_info_defaults = { "translations": ( None, @@ -196,6 +211,11 @@ def apply_change(self, run_id): class VolumeRotationChanger(GeometryChanger): + + # hints for IDE + rotations: Optional[list] + repetition_index: int + user_info_defaults = { "rotations": ( None, diff --git a/opengate/actors/filters.py b/opengate/actors/filters.py index 217df66a9..4d36faa03 100644 --- a/opengate/actors/filters.py +++ b/opengate/actors/filters.py @@ -1,4 +1,5 @@ import sys +from typing import Optional import opengate_core as g4 from ..base import GateObject @@ -10,6 +11,9 @@ class FilterBase(GateObject): A filter to be attached to an actor. """ + # hints for IDE + policy: str + user_info_defaults = { "policy": ( "accept", @@ -35,6 +39,10 @@ def __setstate__(self, state): class ParticleFilter(FilterBase, g4.GateParticleFilter): + + # hints for IDE + particle: str + user_info_defaults = { "particle": ( "", @@ -53,6 +61,11 @@ def __initcpp__(self): class KineticEnergyFilter(FilterBase, g4.GateKineticEnergyFilter): + + # hints for IDE + energy_min: float + energy_max: float + user_info_defaults = { "energy_min": ( 0, @@ -77,6 +90,10 @@ def __initcpp__(self): class TrackCreatorProcessFilter(FilterBase, g4.GateTrackCreatorProcessFilter): + + # hints for IDE + process_name: str + user_info_defaults = { "process_name": ( "none", @@ -95,6 +112,12 @@ def __initcpp__(self): class ThresholdAttributeFilter(FilterBase, g4.GateThresholdAttributeFilter): + + # hints for IDE + value_min: float + value_max: float + attribute: Optional[str] + user_info_defaults = { "value_min": ( 0, diff --git a/opengate/actors/miscactors.py b/opengate/actors/miscactors.py index 72a2dd7e8..620d1d793 100644 --- a/opengate/actors/miscactors.py +++ b/opengate/actors/miscactors.py @@ -11,6 +11,11 @@ class ActorOutputStatisticsActor(ActorOutputBase): """This is a hand-crafted ActorOutput specifically for the SimulationStatisticsActor.""" + # hints for IDE + encoder: str + output_filename: str + write_to_disk: bool + user_info_defaults = { "encoder": ( "json", @@ -174,6 +179,9 @@ class SimulationStatisticsActor(ActorBase, g4.GateSimulationStatisticsActor): Store statistics about a simulation run. """ + # hints for IDE + track_types_flag:bool + user_info_defaults = { "track_types_flag": ( False, @@ -286,6 +294,12 @@ def _setter_hook_particles(self, value): class SplittingActorBase(ActorBase): + # hints for IDE + splitting_factor:int + bias_primary_only:bool + bias_only_once:bool + particles:list + user_info_defaults = { "splitting_factor": ( 1, @@ -319,6 +333,14 @@ class SplittingActorBase(ActorBase): class ComptSplittingActor(SplittingActorBase, g4.GateOptrComptSplittingActor): + # hints for IDE + weight_threshold:float + min_weight_of_particle:float + russian_roulette:bool + rotation_vector_director:bool + vector_director:list + max_theta:float + user_info_defaults = { "weight_threshold": ( 0, @@ -375,6 +397,9 @@ def initialize(self): class BremSplittingActor(SplittingActorBase, g4.GateBOptrBremSplittingActor): + # hints for IDE + processes:list + user_info_defaults = { "processes": ( ["eBrem"], From fa34bac376d2089ed9d59e730f0b475ce8dbbf08 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 16 Sep 2024 16:19:16 +0200 Subject: [PATCH 03/18] scale density if the dose grid is different of the image grid --- opengate/actors/doseactors.py | 10 +- opengate/image.py | 57 +++++++++ opengate/tests/src/test008_dose_actor_2.py | 132 +++++++++++++++++++++ 3 files changed, 196 insertions(+), 3 deletions(-) create mode 100755 opengate/tests/src/test008_dose_actor_2.py diff --git a/opengate/actors/doseactors.py b/opengate/actors/doseactors.py index 34f0131e0..44f74db14 100644 --- a/opengate/actors/doseactors.py +++ b/opengate/actors/doseactors.py @@ -1,4 +1,3 @@ -import itk import numpy as np from scipy.spatial.transform import Rotation @@ -12,9 +11,11 @@ from ..image import ( update_image_py_to_cpp, get_py_image_from_cpp_image, - itk_image_from_array, divide_itk_images, scale_itk_image, + get_info_from_image, + images_have_same_domain, + resample_itk_image_like ) from ..geometry.utility import get_transform_world_to_local from ..base import process_cls @@ -511,9 +512,12 @@ def compute_dose_from_edep_img(self, input_image, density_image=None): # for dose to water, divide by density of water and not density of material scaled_image = scale_itk_image(input_image, 1 / (1.0 * gcm3)) else: + density_image = vol.create_density_image() + if images_have_same_domain(input_image, density_image) is False: + density_image = resample_itk_image_like(density_image, input_image, 0, linear=True) scaled_image = divide_itk_images( img1_numerator=input_image, - img2_denominator=vol.create_density_image(), + img2_denominator=density_image, filterVal=0, replaceFilteredVal=0, ) diff --git a/opengate/image.py b/opengate/image.py index 97ae1ea5f..3bcea1caa 100644 --- a/opengate/image.py +++ b/opengate/image.py @@ -2,6 +2,7 @@ import numpy as np from box import Box from scipy.spatial.transform import Rotation +import math import opengate_core as g4 from .exception import fatal @@ -455,3 +456,59 @@ def write_itk_image(img, file_path): # TODO: check if filepath exists # TODO: add metadata to file header itk.imwrite(img, str(file_path)) + + +def images_have_same_domain(image1, image2, tolerance=1e-5): + # Check if the sizes and origins of the images are the same, + # and if the spacing values are close within the given tolerance + img1_info = get_info_from_image(image1) + img2_info = get_info_from_image(image2) + is_same = ( + len(img1_info.size) == len(img2_info.size) + and all(i == j for i, j in zip(img1_info.size, img2_info.size)) + and images_have_same_spacing(image1, image2, tolerance) + and all( + math.isclose(i, j, rel_tol=tolerance) + for i, j in zip(image1.GetOrigin(), image2.GetOrigin()) + ) + ) + return is_same + + +def images_have_same_spacing(image1, image2, tolerance=1e-5): + # Check if the spacing values are close within the given tolerance + is_same = all( + math.isclose(i, j, rel_tol=tolerance) + for i, j in zip(image1.GetSpacing(), image2.GetSpacing()) + ) + return is_same + + +def resample_itk_image_like(img, like_img, default_pixel_value, linear=True): + # Create a resampler object + ResampleFilterType = itk.ResampleImageFilter.New + + resampler = ResampleFilterType(Input=img) + + # Set the resampler parameters from like_img + resampler.SetSize(itk.size(like_img)) + resampler.SetOutputSpacing(like_img.GetSpacing()) + resampler.SetOutputOrigin(like_img.GetOrigin()) + resampler.SetOutputDirection(like_img.GetDirection()) + + # Set the default pixel value + resampler.SetDefaultPixelValue(default_pixel_value) + + # Use the identity transform - we only resample in place + # identity_transform = itk.IdentityTransform.New() + # resampler.SetTransform(identity_transform) + + # Set the interpolation method to Linear if required + if linear: + resampler.SetInterpolator(itk.LinearInterpolateImageFunction.New(img)) + + # Execute the resampling + resampler.Update() + resampled_img = resampler.GetOutput() + + return resampled_img diff --git a/opengate/tests/src/test008_dose_actor_2.py b/opengate/tests/src/test008_dose_actor_2.py new file mode 100755 index 000000000..29ea88ebc --- /dev/null +++ b/opengate/tests/src/test008_dose_actor_2.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import opengate as gate +from opengate.tests import utility + + +if __name__ == "__main__": + paths = utility.get_default_test_paths(__file__, "", "test008") + + # create the simulation + sim = gate.Simulation() + + # options + sim.visu = False + sim.visu_type = "vrml" + sim.output_dir = paths.output + sim.progress_bar = True + sim.random_seed = 42321 + + # units + m = gate.g4_units.m + cm = gate.g4_units.cm + cm3 = gate.g4_units.cm3 + keV = gate.g4_units.keV + MeV = gate.g4_units.MeV + mm = gate.g4_units.mm + Bq = gate.g4_units.Bq + gcm3 = gate.g4_units.g / cm3 + + # world + world = sim.world + world.size = [1 * m, 1 * m, 1 * m] + world.material = "G4_AIR" + + # pat + patient = sim.add_volume("Image", "patient") + patient.image = paths.data / "patient-4mm.mhd" + print(f'Reading image {patient.image}') + patient.material = "G4_AIR" # material used by default + patient.translation = [0, 0, 272 * mm] + f1 = paths.data / "Schneider2000MaterialsTable.txt" + f2 = paths.data / "Schneider2000DensitiesTable.txt" + tol = 0.1 * gcm3 + ( + patient.voxel_materials, + materials, + ) = gate.geometry.materials.HounsfieldUnit_to_material(sim, tol, f1, f2) + print(f"tol = {tol/gcm3} g/cm3") + print(f"mat : {len(patient.voxel_materials)} materials") + + # physics (opt1 is faster than opt4, but less accurate) + sim.physics_manager.physics_list_name = "G4EmStandardPhysics_option1" + sim.physics_manager.set_production_cut("world", "all", 10 * mm) + sim.physics_manager.set_production_cut("patient", "all", 0.1 * mm) + + # source + source = sim.add_source("GenericSource", "mysource") + source.particle = "proton" + source.energy.mono = 150 * MeV + source.position.type = "disc" + source.position.radius = 3 * mm + source.position.translation = [0, 0, 0] + source.direction.type = "momentum" + source.direction.momentum = [0, 0, 1] + source.n = 200 + + # stats + stats = sim.add_actor("SimulationStatisticsActor", "stats") + stats.track_types_flag = True + stats.output_filename = "stats.txt" + + # dose actor 1: depth edep + doseactor = sim.add_actor("DoseActor", "depth") + doseactor.attached_to = "patient" + doseactor.output_filename = "depth.mhd" + doseactor.spacing = [5 * mm, 5 * mm, 5 * mm] + doseactor.size = [50, 50, 50] + doseactor.output_coordinate_system = "attached_to_image" + doseactor.dose.active = True + doseactor.dose_uncertainty.active = True + doseactor.density.active = True + + # run + sim.run() + + # print stat + print(stats) + + is_ok = True + print("\nDifference for EDEP") + is_ok = ( + utility.assert_images( + paths.output_ref / "depth_dose.mhd", + doseactor.dose.get_output_path(), + stats, + tolerance=25, + ignore_value=0, + sum_tolerance=1, + ) + and is_ok + ) + + print("\nDifference for uncertainty") + is_ok = ( + utility.assert_images( + paths.output_ref / "depth_dose_uncertainty.mhd", + doseactor.dose_uncertainty.get_output_path(), + stats, + tolerance=5, + ignore_value=1, + sum_tolerance=1, + ) + and is_ok + ) + + """print("\nDifference for density") + is_ok = ( + utility.assert_images( + paths.output_ref / "depth_density.mhd", + doseactor.density.get_output_path(), + stats, + tolerance=5, + ignore_value=1, + sum_tolerance=1, + ) + and is_ok + )""" + + + utility.test_ok(is_ok) + From 4f35f31a69a49e7c01aef92981db03f819c740a5 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 16 Sep 2024 16:21:20 +0200 Subject: [PATCH 04/18] update data --- opengate/tests/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opengate/tests/data b/opengate/tests/data index ceaa6287b..714c1390a 160000 --- a/opengate/tests/data +++ b/opengate/tests/data @@ -1 +1 @@ -Subproject commit ceaa6287b9482996d0c8cd7cec40756f63674d1b +Subproject commit 714c1390a3af3f70504bc263d237fd7c29e5eb41 From 9db3ac01af194dc6d41e6bde5669a049af17294d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:27:30 +0000 Subject: [PATCH 05/18] [pre-commit.ci] Automatic python and c++ formatting --- opengate/actors/doseactors.py | 6 ++++-- opengate/image.py | 14 +++++++------- opengate/tests/src/test008_dose_actor_2.py | 4 +--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/opengate/actors/doseactors.py b/opengate/actors/doseactors.py index 44f74db14..f0ac594ef 100644 --- a/opengate/actors/doseactors.py +++ b/opengate/actors/doseactors.py @@ -15,7 +15,7 @@ scale_itk_image, get_info_from_image, images_have_same_domain, - resample_itk_image_like + resample_itk_image_like, ) from ..geometry.utility import get_transform_world_to_local from ..base import process_cls @@ -514,7 +514,9 @@ def compute_dose_from_edep_img(self, input_image, density_image=None): else: density_image = vol.create_density_image() if images_have_same_domain(input_image, density_image) is False: - density_image = resample_itk_image_like(density_image, input_image, 0, linear=True) + density_image = resample_itk_image_like( + density_image, input_image, 0, linear=True + ) scaled_image = divide_itk_images( img1_numerator=input_image, img2_denominator=density_image, diff --git a/opengate/image.py b/opengate/image.py index 3bcea1caa..010aadb24 100644 --- a/opengate/image.py +++ b/opengate/image.py @@ -464,13 +464,13 @@ def images_have_same_domain(image1, image2, tolerance=1e-5): img1_info = get_info_from_image(image1) img2_info = get_info_from_image(image2) is_same = ( - len(img1_info.size) == len(img2_info.size) - and all(i == j for i, j in zip(img1_info.size, img2_info.size)) - and images_have_same_spacing(image1, image2, tolerance) - and all( - math.isclose(i, j, rel_tol=tolerance) - for i, j in zip(image1.GetOrigin(), image2.GetOrigin()) - ) + len(img1_info.size) == len(img2_info.size) + and all(i == j for i, j in zip(img1_info.size, img2_info.size)) + and images_have_same_spacing(image1, image2, tolerance) + and all( + math.isclose(i, j, rel_tol=tolerance) + for i, j in zip(image1.GetOrigin(), image2.GetOrigin()) + ) ) return is_same diff --git a/opengate/tests/src/test008_dose_actor_2.py b/opengate/tests/src/test008_dose_actor_2.py index 29ea88ebc..a0b210068 100755 --- a/opengate/tests/src/test008_dose_actor_2.py +++ b/opengate/tests/src/test008_dose_actor_2.py @@ -36,7 +36,7 @@ # pat patient = sim.add_volume("Image", "patient") patient.image = paths.data / "patient-4mm.mhd" - print(f'Reading image {patient.image}') + print(f"Reading image {patient.image}") patient.material = "G4_AIR" # material used by default patient.translation = [0, 0, 272 * mm] f1 = paths.data / "Schneider2000MaterialsTable.txt" @@ -127,6 +127,4 @@ and is_ok )""" - utility.test_ok(is_ok) - From 9c1f6e9d1729011ab96261f1237c6a4906cb93c4 Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 17 Sep 2024 08:05:31 +0200 Subject: [PATCH 06/18] correct argv for qt visu --- .../opengate_lib/GateSourceManager.cpp | 48 +++++++++---------- .../opengate_lib/GateSourceManager.h | 3 +- opengate/tests/src/test004_simple_visu_qt.py | 15 ++---- 3 files changed, 28 insertions(+), 38 deletions(-) diff --git a/core/opengate_core/opengate_lib/GateSourceManager.cpp b/core/opengate_core/opengate_lib/GateSourceManager.cpp index 5ecb0ed9a..4193660ee 100644 --- a/core/opengate_core/opengate_lib/GateSourceManager.cpp +++ b/core/opengate_core/opengate_lib/GateSourceManager.cpp @@ -34,10 +34,9 @@ bool fUserStoppingCritReached = false; GateSourceManager::GateSourceManager() { fUIEx = nullptr; - fVisEx = nullptr; fVisualizationVerboseFlag = false; fVisualizationFlag = false; - fVisualizationTypeFlag = "qt"; + fVisualizationType = "qt"; fVisualizationFile = "g4writertest.gdml"; fVerboseLevel = 0; fUserEventInformationFlag = false; @@ -49,10 +48,13 @@ GateSourceManager::GateSourceManager() { l.fCurrentSimulationTime = 0; l.fNextActiveSource = nullptr; l.fNextSimulationTime = 0; + fExpectedNumberOfEvents = 0; + fUserStoppingCritReached = false; + fProgressBarStep = 1000; + fCurrentEvent = 0; } GateSourceManager::~GateSourceManager() { - delete fVisEx; // fUIEx is already deleted if (fProgressBarFlag) { if (!G4Threading::IsMultithreadedApplication() || @@ -72,13 +74,13 @@ void GateSourceManager::Initialize(TimeIntervals simulation_times, fOptions = options; fVisualizationFlag = DictGetBool(options, "visu"); fVisualizationVerboseFlag = DictGetBool(options, "visu_verbose"); - fVisualizationTypeFlag = DictGetStr(options, "visu_type"); + fVisualizationType = DictGetStr(options, "visu_type"); fVisualizationFile = DictGetStr(options, "visu_filename"); - if (fVisualizationTypeFlag == "vrml" || - fVisualizationTypeFlag == "vrml_file_only") + if (fVisualizationType == "vrml" || + fVisualizationType == "vrml_file_only") fVisCommands = DictGetVecStr(options, "visu_commands_vrml"); - else if (fVisualizationTypeFlag == "gdml" || - fVisualizationTypeFlag == "gdml_file_only") + else if (fVisualizationType == "gdml" || + fVisualizationType == "gdml_file_only") fVisCommands = DictGetVecStr(options, "visu_commands_gdml"); else fVisCommands = DictGetVecStr(options, "visu_commands"); @@ -134,7 +136,7 @@ void GateSourceManager::StartMasterThread() { InitializeVisualization(); auto *uim = G4UImanager::GetUIpointer(); uim->ApplyCommand(run); - // std::cout << "Apply Beam On" << std::endl; + bool exit_sim_on_next_run = false; for (auto &actor : fActors) { int ret = actor->EndOfRunActionMasterThread(run_id); @@ -151,7 +153,7 @@ void GateSourceManager::StartMasterThread() { } } - // progress bar + // progress bar (only thread 0) if (fProgressBarFlag) { if (G4Threading::IsMultithreadedApplication() && G4Threading::G4GetThreadId() != 0) @@ -280,6 +282,7 @@ void GateSourceManager::GeneratePrimaries(G4Event *event) { auto *vertex = new G4PrimaryVertex(p, l.fCurrentSimulationTime); vertex->SetPrimary(particle); event->AddPrimaryVertex(vertex); + // (allocated memory is leaked) } else { // shoot particle l.fNextActiveSource->GeneratePrimaries(event, l.fCurrentSimulationTime); @@ -329,15 +332,15 @@ void GateSourceManager::GeneratePrimaries(G4Event *event) { } void GateSourceManager::InitializeVisualization() { - if (!fVisualizationFlag || (fVisualizationTypeFlag == "gdml") || - (fVisualizationTypeFlag == "gdml_file_only")) + if (!fVisualizationFlag || (fVisualizationType == "gdml") || + (fVisualizationType == "gdml_file_only")) return; - char *argv[1]; // ok on osx - // char **argv = new char*[1]; // not ok on osx - if (fVisualizationTypeFlag == "qt") { - fUIEx = new G4UIExecutive(1, argv, fVisualizationTypeFlag); - // fUIEx = new G4UIExecutive(1, argv, "qt"); // FIXME + char** argv = new char*[1]; // Allocate 1 element + argv[0] = nullptr; // Properly indicate no arguments + + if (fVisualizationType == "qt") { + fUIEx = new G4UIExecutive(1, argv, fVisualizationType); // FIXME does not always work on Linux ? only OSX for the moment fUIEx->SetVerbose(fVisualizationVerboseFlag); } @@ -368,7 +371,7 @@ void GateSourceManager::InitializeVisualization() { void GateSourceManager::StartVisualization() const { #ifdef USE_GDML - if (fVisualizationTypeFlag == "gdml") { + if (fVisualizationType == "gdml") { G4GDMLParser parser; parser.SetRegionExport(true); parser.Write(fVisualizationFile, @@ -378,18 +381,13 @@ void GateSourceManager::StartVisualization() const { ->GetLogicalVolume()); } #else - if (fVisualizationTypeFlag == "gdml") { + if (fVisualizationType == "gdml") { std::cout << "Error: GDML is not activated with Geant4" << std::endl; return; } #endif - // if (!fVisualizationFlag || (fVisualizationTypeFlag == "vrml") || - // (fVisualizationTypeFlag == "vrml_file_only") || - // (fVisualizationTypeFlag == "gdml") || - // (fVisualizationTypeFlag == "gdml_file_only")) - // return; - if (fVisualizationFlag && fVisualizationTypeFlag == "qt") { + if (fVisualizationFlag && fVisualizationType == "qt") { fUIEx->SessionStart(); delete fUIEx; } diff --git a/core/opengate_core/opengate_lib/GateSourceManager.h b/core/opengate_core/opengate_lib/GateSourceManager.h index 45f677bac..72828268e 100644 --- a/core/opengate_core/opengate_lib/GateSourceManager.h +++ b/core/opengate_core/opengate_lib/GateSourceManager.h @@ -91,10 +91,9 @@ class GateSourceManager : public G4VUserPrimaryGeneratorAction { bool fVisualizationFlag; bool fVisualizationVerboseFlag; - std::string fVisualizationTypeFlag; + std::string fVisualizationType; std::string fVisualizationFile; G4UIExecutive *fUIEx; - G4VisExecutive *fVisEx; std::vector fVisCommands; UIsessionSilent fSilent; diff --git a/opengate/tests/src/test004_simple_visu_qt.py b/opengate/tests/src/test004_simple_visu_qt.py index 944109085..693efa3bd 100755 --- a/opengate/tests/src/test004_simple_visu_qt.py +++ b/opengate/tests/src/test004_simple_visu_qt.py @@ -18,7 +18,7 @@ sim.g4_verbose_level = 1 sim.visu = True sim.visu_type = "qt" - sim.visu_verbose = False + sim.visu_verbose = True sim.number_of_threads = 1 sim.random_engine = "MersenneTwister" sim.random_seed = "auto" @@ -51,18 +51,11 @@ sim.run_timing_intervals = [[0, 0.5 * sec], [0.5 * sec, 1.0 * sec]] # add stat actor - sim.add_actor("SimulationStatisticsActor", "Stats") + stats = sim.add_actor("SimulationStatisticsActor", "Stats") # start simulation # sim.g4_commands_after_init.append("/run/verbose 1") sim.run() - stats = sim.get_actor("Stats") - stats.counts.run_count = 1 - - # gate_test4_simulation_stats_actor - # Gate mac/main.mac - stats_ref = utility.read_stat_file(paths.gate_output / "stat.txt") - is_ok = utility.assert_stats(stats, stats_ref, tolerance=0.03) - - utility.test_ok(is_ok) + print(stats) + utility.test_ok(True) From e99b1deb88cd9d1f1514f985b58274c169c6f05e Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 17 Sep 2024 10:54:04 +0200 Subject: [PATCH 07/18] cannot always predict the nb of events (AA) --- opengate/engines.py | 12 +++++++++++- opengate/sources/generic.py | 22 ++++++++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/opengate/engines.py b/opengate/engines.py index 77ce6d46c..7412b21cf 100644 --- a/opengate/engines.py +++ b/opengate/engines.py @@ -182,6 +182,12 @@ def start(self): for source in self.sources: source.prepare_output() + def can_predict_expected_number_of_event(self): + can_predict = True + for source in self.sources: + can_predict = can_predict and source.can_predict_number_of_events() + return can_predict + class PhysicsEngine(EngineBase): """ @@ -1158,7 +1164,11 @@ def start_and_stop(self): s = "(in a new process) " s2 = "" if self.simulation.progress_bar: - s2 = f"(around {self.source_engine.expected_number_of_events} events expected)" + n = self.source_engine.expected_number_of_events + if self.source_engine.can_predict_expected_number_of_event(): + s2 = f"(around {n} events expected)" + else: + s2 = f"(cannot predict the number of events, max is {n}, e.g. acceptance_angle is enabled)" log.info("-" * 80 + f"\nSimulation: START {s}{s2}") # actor: start simulation (only the master thread) diff --git a/opengate/sources/generic.py b/opengate/sources/generic.py index b38658ead..fc24be478 100644 --- a/opengate/sources/generic.py +++ b/opengate/sources/generic.py @@ -276,18 +276,8 @@ def add_to_source_manager(self, source_manager): def prepare_output(self): pass - def get_estimated_number_of_events(self, run_timing_interval): - """# by default, all event have the same time, so we check that - # this time is included into the given time interval - if ( - run_timing_interval[0] - <= self.user_info.start_time - <= run_timing_interval[1] - ): - return self.user_info.n - return 0""" - fatal(f"Not implemented yet: get_estimated_number_of_events") - exit() + def can_predict_number_of_events(self): + return True class GenericSource(SourceBase): @@ -532,6 +522,14 @@ def update_tac_activity(self): ui.activity = ui.tac_activities[0] self.g4_source.SetTAC(ui.tac_times, ui.tac_activities) + def can_predict_number_of_events(self): + aa = self.user_info.direction.acceptance_angle + if aa.intersection_flag or aa.normal_flag: + if aa.skip_policy == "ZeroEnergy": + return True + return False + return True + class TemplateSource(SourceBase): """ From a058b95c08b033480703f3a077d357b9ece50051 Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 17 Sep 2024 10:55:02 +0200 Subject: [PATCH 08/18] deprecated was not detected --- opengate/base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/opengate/base.py b/opengate/base.py index 6214207dc..03be8e914 100644 --- a/opengate/base.py +++ b/opengate/base.py @@ -1,6 +1,5 @@ import copy from pathlib import Path - from box import Box import sys @@ -140,6 +139,7 @@ def digest_user_info_defaults(cls): def add_properties_to_class(cls, user_info_defaults): """Add user_info defaults as properties to class if not yet present.""" + for p_name, default_value_and_options in user_info_defaults.items(): _ok = False if ( @@ -148,8 +148,6 @@ def add_properties_to_class(cls, user_info_defaults): ): default_value = default_value_and_options[0] options = default_value_and_options[1] - # _ok = True - # if not _ok: else: s = ( f"*** DEVELOPER WARNING ***" @@ -428,7 +426,7 @@ def __setattr__(self, key, value): if ( key in self.inherited_user_info_defaults and "deprecated" in self.inherited_user_info_defaults[key][1] - and self.inherited_user_info_defaults[key][1]["deprecated"] is True + #and self.inherited_user_info_defaults[key][1]["deprecated"] is True ): raise GateDeprecationError( self.inherited_user_info_defaults[key][1]["deprecated"] From ef1d2fda066d49bfe4526b25b9ae56a02a41743d Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 17 Sep 2024 10:55:07 +0200 Subject: [PATCH 09/18] deprecated was not detected --- opengate/tests/src/test078_check_userinfo.py | 59 ++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100755 opengate/tests/src/test078_check_userinfo.py diff --git a/opengate/tests/src/test078_check_userinfo.py b/opengate/tests/src/test078_check_userinfo.py new file mode 100755 index 000000000..4d7846771 --- /dev/null +++ b/opengate/tests/src/test078_check_userinfo.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from opengate.tests.utility import ( + get_default_test_paths, + test_ok, print_test, +) +from opengate.utility import g4_units +from opengate.managers import Simulation + +if __name__ == "__main__": + paths = get_default_test_paths(__file__,) + + sim = Simulation() + + m = g4_units.m + cm = g4_units.cm + keV = g4_units.keV + mm = g4_units.mm + um = g4_units.um + Bq = g4_units.Bq + + world = sim.world + world.size = [3 * m, 3 * m, 3 * m] + world.material = "G4_AIR" + + waterbox = sim.add_volume("Box", "Waterbox") + waterbox.size = [40 * cm, 40 * cm, 40 * cm] + waterbox.translation = [0 * cm, 0 * cm, 25 * cm] + waterbox.material = "G4_WATER" + + source = sim.add_source("GenericSource", "Default") + source.particle = "gamma" + source.energy.mono = 80 * keV + source.direction.type = "momentum" + source.direction.momentum = [0, 0, 1] + source.n = 20 + + # test wrong attributes + is_ok = True + stats = sim.add_actor("SimulationStatisticsActor", "Stats") + stats.track_types_flag = True + try: + stats.mother = "nothing" + is_ok = False and is_ok + except: + pass + print_test(is_ok, f"Try to set deprecated attribute 'mother', it should raise an exception") + + """try: + stats.TOTO = "nothing" + is_ok = False and is_ok + except: + pass + print_test(is_ok, f"Try to set wring attribute 'TOTO', it should raise an exception")""" + + sim.run() + + test_ok(is_ok) From 4061c2d4cf1e3977d0c6a62ee4a116435964deff Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 17 Sep 2024 11:14:50 +0200 Subject: [PATCH 10/18] typo --- opengate/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/opengate/base.py b/opengate/base.py index 672e50305..9e2faffec 100644 --- a/opengate/base.py +++ b/opengate/base.py @@ -158,8 +158,8 @@ def add_properties_to_class(cls, user_info_defaults): "and the second item is a (possibly empty) dictionary of options.\n" ) fatal(s) - options= None # remove warning from IDE - default_value= None # remove warning from IDE + options = None # remove warning from IDE + default_value = None # remove warning from IDE if "deprecated" not in options: if not hasattr(cls, p_name): check_property_name(p_name) @@ -433,7 +433,7 @@ def __setattr__(self, key, value): if ( key in self.inherited_user_info_defaults and "deprecated" in self.inherited_user_info_defaults[key][1] - #and self.inherited_user_info_defaults[key][1]["deprecated"] is True + # and self.inherited_user_info_defaults[key][1]["deprecated"] is True ): raise GateDeprecationError( self.inherited_user_info_defaults[key][1]["deprecated"] @@ -456,7 +456,7 @@ def __add_to_simulation__(self): """Hook method which can be called by managers. Specific classes can use this to implement actions to be taken when an object is being added to the simulation, - e.g. adding a certain actor implies switching on certein physics options. + e.g. adding a certain actor implies switching on certain physics options. """ pass @@ -772,7 +772,7 @@ def _get_user_info_options(user_info_name, object_type, class_module): ).inherited_user_info_defaults[user_info_name][1] except KeyError: fatal(f"Could not find user info {user_info_name} in {object_type}. ") - options = None # remove warning from IDE + options = None # remove warning from IDE return options From f0ee8423d36099ad61415bbbfaa5076d4c38cde1 Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 17 Sep 2024 11:16:28 +0200 Subject: [PATCH 11/18] typo --- opengate/actors/actoroutput.py | 15 +++++---------- opengate/actors/base.py | 5 +++-- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/opengate/actors/actoroutput.py b/opengate/actors/actoroutput.py index d193035b8..deedb86cd 100644 --- a/opengate/actors/actoroutput.py +++ b/opengate/actors/actoroutput.py @@ -220,7 +220,7 @@ class ActorOutputBase(GateObject): "Otherwise, it is only stored on disk and needs to be re-loaded manually. " "Careful: Large data structures like a phase space need a lot of memory.", }, - ) + ), } @classmethod @@ -307,7 +307,7 @@ def _compose_output_path(self, which, output_filename): f"of {type(self).__name__} called {self.name}" f"Valid arguments are a run index (int) or the term 'merged'. " ) - run_index = None # remove warning from IDE + run_index = None # remove warning from IDE return insert_suffix_before_extension(full_data_path, f"run{run_index:04f}") def get_output_path(self, which="merged", **kwargs): @@ -415,11 +415,6 @@ def end_of_simulation(self, **kwargs): f"and/or write_data() method. " f"A developer needs to fix this. " ) - # if self.merge_data_after_simulation is True: - # self.merge_data_from_runs() - # if self.keep_data_per_run is False: - # for k in self.data_per_run: - # self.data_per_run[k] = None class ActorOutputUsingDataItemContainer(MergeableActorOutput): @@ -640,7 +635,7 @@ def store_data(self, which, *data): f"Invalid argument 'which' in store_data() method of ActorOutput {self.name}. " f"Allowed values are: 'merged' or a valid run_index. " ) - run_index = None # avoid IDE warning + run_index = None # avoid IDE warning self.data_per_run[run_index] = data_container def store_meta_data(self, which, **meta_data): @@ -674,7 +669,7 @@ def collect_data(self, which, return_identifier=False): ri = int(which) except ValueError: fatal(f"Invalid argument which in method collect_images(): {which}") - ri = None # avoid IDE warning + ri = None # avoid IDE warning data = [self.data_per_run[ri]] identifiers = [ri] if return_identifier is True: @@ -736,7 +731,7 @@ def get_image_properties(self, which, item=0): image_data_container = self.data_per_run[run_index] except KeyError: fatal(f"No data found for run index {run_index}.") - image_data_container = None # avoid IDE warning + image_data_container = None # avoid IDE warning if image_data_container is not None: return image_data_container.get_image_properties()[item] except ValueError: diff --git a/opengate/actors/base.py b/opengate/actors/base.py index e5425ba6a..a87e5367a 100644 --- a/opengate/actors/base.py +++ b/opengate/actors/base.py @@ -77,7 +77,7 @@ def _with_check(self, *args): class ActorBase(GateObject): # hints for IDE - attached_to:str + attached_to: str filters: list filters_boolean_operator: str priority: int @@ -119,7 +119,7 @@ class ActorBase(GateObject): "in the list of all actions in the simulation. " "Low values mean 'early in the list', large values mean 'late in the list'. " }, - ) + ), } # private list of property names for interfaces already defined @@ -279,6 +279,7 @@ def initialize(self): # But it does not hurt to populate the info in C++ regardless of the actor # The output path can also be (re-)set by the specific actor in # StartSimulation or BeginOfRunActionMasterThread, if needed + # for k, v in self.user_output.items(): # if len(v.data_write_config) > 1: # for h, w in v.data_write_config.items(): From 58c700c90a8b94dc2da2bd1ae6513c6b47ddb02c Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 17 Sep 2024 11:19:37 +0200 Subject: [PATCH 12/18] re-run precommit black --- opengate/actors/digitizers.py | 4 +-- opengate/actors/miscactors.py | 26 ++++++++++---------- opengate/tests/src/test078_check_userinfo.py | 14 ++++++++--- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/opengate/actors/digitizers.py b/opengate/actors/digitizers.py index 8acab48c5..99a19ad3d 100644 --- a/opengate/actors/digitizers.py +++ b/opengate/actors/digitizers.py @@ -901,7 +901,7 @@ def StartSimulationAction(self): f"No physical volume found for index {self.physical_volume_index} " f"in volume {self.attached_to_volume.name}" ) - pv = None # avoid warning from IDE + pv = None # avoid warning from IDE align_image_with_physical_volume( self.attached_to_volume, self.user_output.projection.data_per_run[0].image ) @@ -1009,7 +1009,7 @@ class PhaseSpaceActor(DigitizerBase, g4.GatePhaseSpaceActor): "debug": ( False, { - "doc": "FIXME", + "doc": "print debug info", }, ), "steps_to_store": ( diff --git a/opengate/actors/miscactors.py b/opengate/actors/miscactors.py index 620d1d793..4cc3dba3e 100644 --- a/opengate/actors/miscactors.py +++ b/opengate/actors/miscactors.py @@ -180,7 +180,7 @@ class SimulationStatisticsActor(ActorBase, g4.GateSimulationStatisticsActor): """ # hints for IDE - track_types_flag:bool + track_types_flag: bool user_info_defaults = { "track_types_flag": ( @@ -256,7 +256,7 @@ def EndSimulationAction(self): it requires "trampoline functions" on the cpp side. - # feasible but very slow ! + # it is feasible but very slow ! def SteppingAction(self, step, touchable): g4.GateSimulationStatisticsActor.SteppingAction(self, step, touchable) do_something() @@ -295,10 +295,10 @@ def _setter_hook_particles(self, value): class SplittingActorBase(ActorBase): # hints for IDE - splitting_factor:int - bias_primary_only:bool - bias_only_once:bool - particles:list + splitting_factor: int + bias_primary_only: bool + bias_only_once: bool + particles: list user_info_defaults = { "splitting_factor": ( @@ -334,12 +334,12 @@ class SplittingActorBase(ActorBase): class ComptSplittingActor(SplittingActorBase, g4.GateOptrComptSplittingActor): # hints for IDE - weight_threshold:float - min_weight_of_particle:float - russian_roulette:bool - rotation_vector_director:bool - vector_director:list - max_theta:float + weight_threshold: float + min_weight_of_particle: float + russian_roulette: bool + rotation_vector_director: bool + vector_director: list + max_theta: float user_info_defaults = { "weight_threshold": ( @@ -398,7 +398,7 @@ def initialize(self): class BremSplittingActor(SplittingActorBase, g4.GateBOptrBremSplittingActor): # hints for IDE - processes:list + processes: list user_info_defaults = { "processes": ( diff --git a/opengate/tests/src/test078_check_userinfo.py b/opengate/tests/src/test078_check_userinfo.py index 4d7846771..6c901ffa2 100755 --- a/opengate/tests/src/test078_check_userinfo.py +++ b/opengate/tests/src/test078_check_userinfo.py @@ -3,13 +3,16 @@ from opengate.tests.utility import ( get_default_test_paths, - test_ok, print_test, + test_ok, + print_test, ) from opengate.utility import g4_units from opengate.managers import Simulation if __name__ == "__main__": - paths = get_default_test_paths(__file__,) + paths = get_default_test_paths( + __file__, + ) sim = Simulation() @@ -45,14 +48,17 @@ is_ok = False and is_ok except: pass - print_test(is_ok, f"Try to set deprecated attribute 'mother', it should raise an exception") + print_test( + is_ok, f"Try to set deprecated attribute 'mother', it should raise an exception" + ) """try: stats.TOTO = "nothing" is_ok = False and is_ok except: pass - print_test(is_ok, f"Try to set wring attribute 'TOTO', it should raise an exception")""" + print_test(is_ok, f"Try to set wring attribute 'TOTO', it should raise an exception") + """ sim.run() From 769797ae735db6cc5d32c696f150a88232626f9c Mon Sep 17 00:00:00 2001 From: Thomas BAUDIER Date: Tue, 17 Sep 2024 14:17:31 +0200 Subject: [PATCH 13/18] linter --- core/opengate_core/opengate_lib/GateSourceManager.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/opengate_core/opengate_lib/GateSourceManager.cpp b/core/opengate_core/opengate_lib/GateSourceManager.cpp index 4193660ee..b05d46459 100644 --- a/core/opengate_core/opengate_lib/GateSourceManager.cpp +++ b/core/opengate_core/opengate_lib/GateSourceManager.cpp @@ -76,8 +76,7 @@ void GateSourceManager::Initialize(TimeIntervals simulation_times, fVisualizationVerboseFlag = DictGetBool(options, "visu_verbose"); fVisualizationType = DictGetStr(options, "visu_type"); fVisualizationFile = DictGetStr(options, "visu_filename"); - if (fVisualizationType == "vrml" || - fVisualizationType == "vrml_file_only") + if (fVisualizationType == "vrml" || fVisualizationType == "vrml_file_only") fVisCommands = DictGetVecStr(options, "visu_commands_vrml"); else if (fVisualizationType == "gdml" || fVisualizationType == "gdml_file_only") @@ -336,8 +335,8 @@ void GateSourceManager::InitializeVisualization() { (fVisualizationType == "gdml_file_only")) return; - char** argv = new char*[1]; // Allocate 1 element - argv[0] = nullptr; // Properly indicate no arguments + char **argv = new char *[1]; // Allocate 1 element + argv[0] = nullptr; // Properly indicate no arguments if (fVisualizationType == "qt") { fUIEx = new G4UIExecutive(1, argv, fVisualizationType); From ec66a423aafa2a892945f874e5216c196e33603b Mon Sep 17 00:00:00 2001 From: Thomas BAUDIER Date: Wed, 18 Sep 2024 10:18:19 +0200 Subject: [PATCH 14/18] Add explanation for readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ea17e821..0a17e8d1b 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Once installed, you can run all tests: opengate_tests ```` -**WARNING** The first time you run this command, the test data will be downloaded. If the download fails (on some systems), try to add the following command before running opengate_tests: +**WARNING** The first time you run this command, the geant4 data and the test data will be downloaded. If the download fails (on some systems), try to add the following command before running opengate_tests: ```` export GIT_SSL_NO_VERIFY=1 ```` From 1e650302cdcac9894cdb6a95ee7ac1bb47ff1e18 Mon Sep 17 00:00:00 2001 From: Thomas BAUDIER Date: Wed, 18 Sep 2024 10:28:49 +0200 Subject: [PATCH 15/18] Remove pre-commit end-of-file-fixer hook end-of-file-fixer is ok locally but not on pre-commit.ci and the website fails to push the modif --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc81ff4b8..d5e1691d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black rev: 24.8.0 From b5d7b869b663d9aba241dba903d58d97bbdd251c Mon Sep 17 00:00:00 2001 From: David Date: Thu, 19 Sep 2024 08:28:27 +0200 Subject: [PATCH 16/18] correct volume mother --- opengate/contrib/spect/siemens_intevo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opengate/contrib/spect/siemens_intevo.py b/opengate/contrib/spect/siemens_intevo.py index c25e7e02a..2681d5bdd 100644 --- a/opengate/contrib/spect/siemens_intevo.py +++ b/opengate/contrib/spect/siemens_intevo.py @@ -946,7 +946,7 @@ def create_simu_for_arf_training_dataset( sim.add_parallel_world("arf_world") plane_size = [533 * mm, 387 * mm] arf_plane = add_detection_plane_for_arf(sim, plane_size, colli_type, radius) - arf_plane.attached_to = "arf_world" + arf_plane.mother = "arf_world" # sources s1 = sim.add_source("GenericSource", "source") From baa48c5c769bf435bc27b5518c407872e1f784f3 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 19 Sep 2024 08:31:28 +0200 Subject: [PATCH 17/18] statsactor: write to disk if filename is given --- opengate/actors/miscactors.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/opengate/actors/miscactors.py b/opengate/actors/miscactors.py index 4cc3dba3e..4be894845 100644 --- a/opengate/actors/miscactors.py +++ b/opengate/actors/miscactors.py @@ -208,6 +208,16 @@ def __str__(self): def counts(self): return self.user_output.stats.merged_data + @ActorBase.output_filename.setter + def output_filename(self, val): + # special behavior: + # By default write_to_disk is False. + # However, if user set the output_filename while it is the default (auto) + # we set write_to_disk to True. + if self.output_filename == "auto": + self.write_to_disk = True + ActorBase.output_filename.fset(self, val) + def store_output_data(self, output_name, run_index, *data): raise NotImplementedError From cc8344b0e1018d93ed1d25835af3f2875582cf52 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 19 Sep 2024 08:31:53 +0200 Subject: [PATCH 18/18] increase the progressbar update for large number of events --- core/opengate_core/opengate_lib/GateSourceManager.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/opengate_core/opengate_lib/GateSourceManager.cpp b/core/opengate_core/opengate_lib/GateSourceManager.cpp index b05d46459..9717edc92 100644 --- a/core/opengate_core/opengate_lib/GateSourceManager.cpp +++ b/core/opengate_core/opengate_lib/GateSourceManager.cpp @@ -193,6 +193,8 @@ void GateSourceManager::ComputeExpectedNumberOfEvents() { source->GetExpectedNumberOfEvents(fSimulationTimes); } fProgressBarStep = (long)round((double)fExpectedNumberOfEvents / 100.0); + if (fExpectedNumberOfEvents > 1e7) + fProgressBarStep = (long)round((double)fExpectedNumberOfEvents / 1000.0); } long int GateSourceManager::GetExpectedNumberOfEvents() const {