From 4e2fc60b4b5ab2e299412af5ac252483f5133c63 Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 21 Jan 2025 12:30:45 +0100 Subject: [PATCH 01/13] get step voxel position in helper --- .../opengate_lib/GateDoseActor.cpp | 36 +---------------- .../opengate_lib/GateDoseActor.h | 3 -- .../opengate_lib/GateHelpersImage.h | 10 ++++- .../opengate_lib/GateHelpersImage.txx | 39 +++++++++++++++++++ 4 files changed, 49 insertions(+), 39 deletions(-) diff --git a/core/opengate_core/opengate_lib/GateDoseActor.cpp b/core/opengate_core/opengate_lib/GateDoseActor.cpp index 765b745dc..c769b5c96 100644 --- a/core/opengate_core/opengate_lib/GateDoseActor.cpp +++ b/core/opengate_core/opengate_lib/GateDoseActor.cpp @@ -127,39 +127,6 @@ void GateDoseActor::BeginOfEventAction(const G4Event *event) { NbOfEvent++; } -void GateDoseActor::GetVoxelPosition(G4Step *step, G4ThreeVector &position, - bool &isInside, - Image3DType::IndexType &index) const { - auto preGlobal = step->GetPreStepPoint()->GetPosition(); - auto postGlobal = step->GetPostStepPoint()->GetPosition(); - auto touchable = step->GetPreStepPoint()->GetTouchable(); - - // consider random position between pre and post - if (fHitType == "pre") { - position = preGlobal; - } - if (fHitType == "random") { - auto x = G4UniformRand(); - auto direction = postGlobal - preGlobal; - position = preGlobal + x * direction; - } - if (fHitType == "middle") { - auto direction = postGlobal - preGlobal; - position = preGlobal + 0.5 * direction; - } - - auto localPosition = - touchable->GetHistory()->GetTransform(0).TransformPoint(position); - - // convert G4ThreeVector to itk PointType - Image3DType::PointType point; - point[0] = localPosition[0]; - point[1] = localPosition[1]; - point[2] = localPosition[2]; - - isInside = cpp_edep_image->TransformPhysicalPointToIndex(point, index); -} - void GateDoseActor::SteppingAction(G4Step *step) { auto event_id = G4RunManager::GetRunManager()->GetCurrentEvent()->GetEventID(); @@ -173,7 +140,8 @@ void GateDoseActor::SteppingAction(G4Step *step) { G4ThreeVector position; bool isInside; Image3DType::IndexType index; - GetVoxelPosition(step, position, isInside, index); + GetStepVoxelPosition(step, fHitType, cpp_edep_image, position, + isInside, index); if (isInside) { diff --git a/core/opengate_core/opengate_lib/GateDoseActor.h b/core/opengate_core/opengate_lib/GateDoseActor.h index 67b326cc1..73b55d9e1 100644 --- a/core/opengate_core/opengate_lib/GateDoseActor.h +++ b/core/opengate_core/opengate_lib/GateDoseActor.h @@ -115,9 +115,6 @@ class GateDoseActor : public GateVActor { void PrepareLocalDataForRun(threadLocalT &data, int numberOfVoxels); - void GetVoxelPosition(G4Step *step, G4ThreeVector &position, bool &isInside, - Image3DType::IndexType &index) const; - // Option: indicate we must convert to dose to water bool fToWaterFlag{}; diff --git a/core/opengate_core/opengate_lib/GateHelpersImage.h b/core/opengate_core/opengate_lib/GateHelpersImage.h index 974ed0590..ae45ee571 100644 --- a/core/opengate_core/opengate_lib/GateHelpersImage.h +++ b/core/opengate_core/opengate_lib/GateHelpersImage.h @@ -21,8 +21,14 @@ void ImageAddValue(typename ImageType::Pointer image, template void AttachImageToVolume(typename ImageType::Pointer image, std::string volumeName, - G4ThreeVector initial_translation = G4ThreeVector(), - G4RotationMatrix img_rotation = G4RotationMatrix()); + G4ThreeVector image_offset = G4ThreeVector(), + G4RotationMatrix volume_rotation = G4RotationMatrix()); + +template +void GetStepVoxelPosition(G4Step *step, std::string hitType, + typename ImageType::Pointer cpp_image, + G4ThreeVector &position, bool &isInside, + typename ImageType::IndexType &index); #include "GateHelpersImage.txx" diff --git a/core/opengate_core/opengate_lib/GateHelpersImage.txx b/core/opengate_core/opengate_lib/GateHelpersImage.txx index 152667bab..742aac220 100644 --- a/core/opengate_core/opengate_lib/GateHelpersImage.txx +++ b/core/opengate_core/opengate_lib/GateHelpersImage.txx @@ -7,6 +7,7 @@ #include "G4LogicalVolume.hh" #include "GateHelpersGeometry.h" +#include "G4RandomTools.hh" template void ImageAddValue(typename ImageType::Pointer image, @@ -53,3 +54,41 @@ void AttachImageToVolume(typename ImageType::Pointer image, image->SetOrigin(o); image->SetDirection(dir); } + +template +void GetStepVoxelPosition(G4Step *step, + std::string hitType, + typename ImageType::Pointer cpp_image, + G4ThreeVector &position, + bool &isInside, + typename ImageType::IndexType &index) +{ + auto preGlobal = step->GetPreStepPoint()->GetPosition(); + auto postGlobal = step->GetPostStepPoint()->GetPosition(); + auto touchable = step->GetPreStepPoint()->GetTouchable(); + + // consider random position between pre and post + if (hitType == "pre") { + position = preGlobal; + } + if (hitType == "random") { + auto x = G4UniformRand(); + auto direction = postGlobal - preGlobal; + position = preGlobal + x * direction; + } + if (hitType == "middle") { + auto direction = postGlobal - preGlobal; + position = preGlobal + 0.5 * direction; + } + + auto localPosition = + touchable->GetHistory()->GetTransform(0).TransformPoint(position); + + // convert G4ThreeVector to itk PointType + typename ImageType::PointType point; + point[0] = localPosition[0]; + point[1] = localPosition[1]; + point[2] = localPosition[2]; + + isInside = cpp_image->TransformPhysicalPointToIndex(point, index); +} From abcfc5ec298c01b5978615323bb476a5cf71d00b Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 21 Jan 2025 12:30:58 +0100 Subject: [PATCH 02/13] get step voxel position in helper --- core/opengate_core/opengate_lib/GateTLEDoseActor.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/opengate_core/opengate_lib/GateTLEDoseActor.cpp b/core/opengate_core/opengate_lib/GateTLEDoseActor.cpp index 4bee2de08..f2b06c111 100644 --- a/core/opengate_core/opengate_lib/GateTLEDoseActor.cpp +++ b/core/opengate_core/opengate_lib/GateTLEDoseActor.cpp @@ -148,7 +148,8 @@ void GateTLEDoseActor::SteppingAction(G4Step *step) { G4ThreeVector position; bool isInside; Image3DType::IndexType index; - GetVoxelPosition(step, position, isInside, index); + GetStepVoxelPosition(step, fHitType, cpp_edep_image, position, + isInside, index); auto event_id = G4RunManager::GetRunManager()->GetCurrentEvent()->GetEventID(); if (isInside) { From 0801e46d2490270da81c45376d1648b3c0685da2 Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 21 Jan 2025 12:33:12 +0100 Subject: [PATCH 03/13] allow for 4D images to be push/pull c++/python --- .../opengate_lib/helpers_itk_image_py.h | 56 ++++++++++++------- .../opengate_lib/pyGateItkImage.cpp | 2 + 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/core/opengate_core/opengate_lib/helpers_itk_image_py.h b/core/opengate_core/opengate_lib/helpers_itk_image_py.h index 801b78025..eef349f79 100644 --- a/core/opengate_core/opengate_lib/helpers_itk_image_py.h +++ b/core/opengate_core/opengate_lib/helpers_itk_image_py.h @@ -52,20 +52,18 @@ inline void set_region(TImagePointer &img, size) { using RegionType = typename TImagePointer::ObjectType::RegionType; typename RegionType::IndexType itk_index; - const auto *data_index = - static_cast(np_array_ptr_after_check_dim_and_shape(index)); - itk_index[0] = data_index[0]; - itk_index[1] = data_index[1]; - itk_index[2] = data_index[2]; + const auto *data_index = static_cast( + np_array_ptr_after_check_dim_and_shape(index, img->ImageDimension)); + for (int i = 0; i < img->ImageDimension; i++) + itk_index[i] = data_index[i]; typename RegionType::SizeType itk_size; - const auto *data_size = - static_cast(np_array_ptr_after_check_dim_and_shape(size)); + const auto *data_size = static_cast( + np_array_ptr_after_check_dim_and_shape(size, img->ImageDimension)); if (data_size[0] < 0 || data_size[1] < 0 || data_size[2] < 0) { throw std::runtime_error("In set_regions, input size cannot be negative."); } - itk_size[0] = data_size[0]; - itk_size[1] = data_size[1]; - itk_size[2] = data_size[2]; + for (int i = 0; i < img->ImageDimension; i++) + itk_size[i] = data_size[i]; RegionType itk_region(itk_index, itk_size); img->SetRegions(itk_region); img->Allocate(); @@ -95,9 +93,10 @@ void declare_itk_image_ptr(pybind11::module &m, const std::string &typestr) { [](TImagePointer &img, py::array_t size) { py::array_t - zero_index(3); + zero_index(img->ImageDimension); int *raw = static_cast(zero_index.request().ptr); - raw[0] = raw[1] = raw[2] = 0; + for (int i = 0; i < img->ImageDimension; i++) + raw[i] = 0; return set_region(img, zero_index, size); }) .def( @@ -118,7 +117,8 @@ void declare_itk_image_ptr(pybind11::module &m, const std::string &typestr) { py::array_t spacing) { const auto *data = static_cast( - np_array_ptr_after_check_dim_and_shape(spacing)); + np_array_ptr_after_check_dim_and_shape( + spacing, img->ImageDimension)); img->SetSpacing(data); }) .def("origin", @@ -131,7 +131,8 @@ void declare_itk_image_ptr(pybind11::module &m, const std::string &typestr) { py::array_t origin) { const auto *data = static_cast( - np_array_ptr_after_check_dim_and_shape(origin)); + np_array_ptr_after_check_dim_and_shape( + origin, img->ImageDimension)); img->SetOrigin(data); }) .def("direction", @@ -173,13 +174,26 @@ void declare_itk_image_ptr(pybind11::module &m, const std::string &typestr) { "to_pyarray", [](const TImagePointer &img, const std::string &contiguous) { const auto size = img->GetLargestPossibleRegion().GetSize(); - const auto shape = - (contiguous == "F") - ? std::vector{size[2], size[1], size[0]} - : std::vector{size[0], size[1], size[2]}; - return py::array( - py::dtype::of(), - shape, img->GetBufferPointer()); + if (img->ImageDimension == 3) { + const auto shape = + (contiguous == "F") + ? std::vector{size[2], size[1], size[0]} + : std::vector{size[0], size[1], size[2]}; + return py::array( + py::dtype::of< + typename TImagePointer::ObjectType::PixelType>(), + shape, img->GetBufferPointer()); + } + if (img->ImageDimension == 4) { + const auto shape = + (contiguous == "F") + ? std::vector{size[3], size[2], size[1], size[0]} + : std::vector{size[0], size[1], size[2], size[3]}; + return py::array( + py::dtype::of< + typename TImagePointer::ObjectType::PixelType>(), + shape, img->GetBufferPointer()); + } }, py::arg("contig") = "F") // TODO: Create a view (non-copy) of the data diff --git a/core/opengate_core/opengate_lib/pyGateItkImage.cpp b/core/opengate_core/opengate_lib/pyGateItkImage.cpp index 0a97f53d9..f4c8b5af3 100644 --- a/core/opengate_core/opengate_lib/pyGateItkImage.cpp +++ b/core/opengate_core/opengate_lib/pyGateItkImage.cpp @@ -19,10 +19,12 @@ using IUC3P = itk::Image::Pointer; using IUS3P = itk::Image::Pointer; using IF3P = itk::Image::Pointer; using ID3P = itk::Image::Pointer; +using ID4P = itk::Image::Pointer; void init_itk_image(py::module &m) { declare_itk_image_ptr(m, "IUC3P"); declare_itk_image_ptr(m, "IUS3P"); declare_itk_image_ptr(m, "IF3P"); declare_itk_image_ptr(m, "ID3P"); + declare_itk_image_ptr(m, "ID4P"); } From 9742a4682401f9098d33077d344f9de402835ecb Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 21 Jan 2025 12:33:53 +0100 Subject: [PATCH 04/13] vpgTLE actor template --- .../GateVoxelizedPromptGammaTLEActor.cpp | 64 +++++++++++++++++++ .../GateVoxelizedPromptGammaTLEActor.h | 47 ++++++++++++++ .../pyGateVoxelizedPromptGammaTLEActor.cpp | 45 +++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.cpp create mode 100644 core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.h create mode 100644 core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaTLEActor.cpp diff --git a/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.cpp b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.cpp new file mode 100644 index 000000000..0ea9f364f --- /dev/null +++ b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.cpp @@ -0,0 +1,64 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + ------------------------------------ -------------- */ + +#include "G4EmCalculator.hh" +#include "G4ParticleDefinition.hh" +#include "G4RandomTools.hh" +#include "G4RunManager.hh" +#include "G4Threading.hh" + +#include "GateHelpers.h" +#include "GateHelpersDict.h" +#include "GateHelpersImage.h" +#include "GateMaterialMuHandler.h" +#include "GateVoxelizedPromptGammaTLEActor.h" + +#include +#include +#include +#include + +GateVoxelizedPromptGammaTLEActor::GateVoxelizedPromptGammaTLEActor( + py::dict &user_info) + : GateVActor(user_info, false) { + fMultiThreadReady = false; +} + +void GateVoxelizedPromptGammaTLEActor::InitializeUserInfo(py::dict &user_info) { + GateVActor::InitializeUserInfo(user_info); + // retrieve the python param here +} + +void GateVoxelizedPromptGammaTLEActor::InitializeCpp() { + GateVActor::InitializeCpp(); + // Create the image pointers + // (the size and allocation will be performed on the py side) + cpp_image = ImageType::New(); +} + +void GateVoxelizedPromptGammaTLEActor::BeginOfRunActionMasterThread( + int run_id) { + std::cout << "Begin of run " << run_id << std::endl; + std::cout << "Image size " << cpp_image->GetLargestPossibleRegion().GetSize() + << std::endl; +} + +void GateVoxelizedPromptGammaTLEActor::BeginOfEventAction( + const G4Event *event) {} + +void GateVoxelizedPromptGammaTLEActor::PreUserTrackingAction( + const G4Track *track) {} + +void GateVoxelizedPromptGammaTLEActor::SteppingAction(G4Step *step) { + // Get the voxel index + G4ThreeVector position; + bool isInside; + ImageType::IndexType index; + GetStepVoxelPosition(step, "random", cpp_image, position, isInside, + index); + // if IsInside ... +} diff --git a/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.h b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.h new file mode 100644 index 000000000..4998b7ce0 --- /dev/null +++ b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.h @@ -0,0 +1,47 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + -------------------------------------------------- */ + +#ifndef GateVoxelizedPromptGammaTLEActor_h +#define GateVoxelizedPromptGammaTLEActor_h + +#include "GateDoseActor.h" +#include "GateMaterialMuHandler.h" + +#include "G4Cache.hh" +#include "G4EmCalculator.hh" +#include "G4NistManager.hh" +#include "G4VPrimitiveScorer.hh" + +#include + +namespace py = pybind11; + +class GateVoxelizedPromptGammaTLEActor : public GateVActor { + +public: + // Constructor + explicit GateVoxelizedPromptGammaTLEActor(py::dict &user_info); + + void InitializeUserInfo(py::dict &user_info) override; + + void InitializeCpp() override; + + void BeginOfRunActionMasterThread(int run_id) override; + + void BeginOfEventAction(const G4Event *event) override; + + void PreUserTrackingAction(const G4Track *track) override; + + // Main function called every step in attached volume + void SteppingAction(G4Step *) override; + + // Image type + typedef itk::Image ImageType; + ImageType::Pointer cpp_image; +}; + +#endif // GateVoxelizedPromptGammaTLEActor_h diff --git a/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaTLEActor.cpp b/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaTLEActor.cpp new file mode 100644 index 000000000..356893d0c --- /dev/null +++ b/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaTLEActor.cpp @@ -0,0 +1,45 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + -------------------------------------------------- */ + +#include +#include + +namespace py = pybind11; + +#include "GateVoxelizedPromptGammaTLEActor.h" + +class PyGateVoxelizedPromptGammaTLEActor + : public GateVoxelizedPromptGammaTLEActor { +public: + // Inherit the constructors + using GateVoxelizedPromptGammaTLEActor::GateVoxelizedPromptGammaTLEActor; + + void BeginOfRunActionMasterThread(int run_id) override { + PYBIND11_OVERLOAD(void, GateVoxelizedPromptGammaTLEActor, + BeginOfRunActionMasterThread, run_id); + } + + int EndOfRunActionMasterThread(int run_id) override { + PYBIND11_OVERLOAD(int, GateVoxelizedPromptGammaTLEActor, + EndOfRunActionMasterThread, run_id); + } +}; + +void init_GateVoxelizedPromptGammaTLEActor(py::module &m) { + py::class_, + GateVActor>(m, "GateVoxelizedPromptGammaTLEActor") + .def(py::init()) + .def("InitializeUserInfo", + &GateVoxelizedPromptGammaTLEActor::InitializeUserInfo) + .def("BeginOfRunActionMasterThread", + &GateVoxelizedPromptGammaTLEActor::BeginOfRunActionMasterThread) + .def("EndOfRunActionMasterThread", + &GateVoxelizedPromptGammaTLEActor::EndOfRunActionMasterThread) + .def_readwrite("cpp_image", &GateVoxelizedPromptGammaTLEActor::cpp_image); +} From eca252a1807535ac7ba21900ab72764423a78a2e Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 21 Jan 2025 12:33:59 +0100 Subject: [PATCH 05/13] vpgTLE actor template --- core/opengate_core/opengate_core.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/opengate_core/opengate_core.cpp b/core/opengate_core/opengate_core.cpp index e9ab29218..8f911dd66 100644 --- a/core/opengate_core/opengate_core.cpp +++ b/core/opengate_core/opengate_core.cpp @@ -306,6 +306,8 @@ void init_GateDoseActor(py::module &m); void init_GateTLEDoseActor(py::module &m); +void init_GateVoxelizedPromptGammaTLEActor(py::module &m); + void init_GateFluenceActor(py::module &m); void init_GateLETActor(py::module &m); @@ -585,6 +587,7 @@ PYBIND11_MODULE(opengate_core, m) { init_GateTrackingAction(m); init_GateDoseActor(m); init_GateTLEDoseActor(m); + init_GateVoxelizedPromptGammaTLEActor(m); init_GateFluenceActor(m); init_GateLETActor(m); init_GateProductionAndStoppingActor(m); From f215c6f5cbb646393f8cd6119785ad72fb298ad6 Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 21 Jan 2025 12:40:22 +0100 Subject: [PATCH 06/13] first template vpgTLE actor --- opengate/actors/actoroutput.py | 16 ++++ opengate/actors/dataitems.py | 12 ++- opengate/actors/doseactors.py | 58 ++----------- opengate/actors/tleactors.py | 152 +++++++++++++++++++++++++++++++++ opengate/image.py | 27 ++++++ opengate/managers.py | 3 +- 6 files changed, 215 insertions(+), 53 deletions(-) create mode 100644 opengate/actors/tleactors.py diff --git a/opengate/actors/actoroutput.py b/opengate/actors/actoroutput.py index 7b03e79e9..5037f4aba 100644 --- a/opengate/actors/actoroutput.py +++ b/opengate/actors/actoroutput.py @@ -6,6 +6,7 @@ import opengate_core as g4 from ..base import GateObject, process_cls +from ..image import create_3d_image_of_histogram from ..utility import insert_suffix_before_extension, ensure_filename_is_str from ..exception import warning, fatal, GateImplementationError from .dataitems import ( @@ -845,11 +846,25 @@ def create_empty_image(self, run_index, size, spacing, origin=None, **kwargs): ) +class ActorOutputImageOfHistogram(ActorOutputImage): + def create_image_of_histograms( + self, run_index, size, spacing, bins, origin=None, **kwargs + ): + if run_index not in self.data_per_run: + self.data_per_run[run_index] = self.data_container_class(belongs_to=self) + img = create_3d_image_of_histogram(size, spacing, bins, origin, **kwargs) + self.data_per_run[run_index].set_data(img) + + # concrete classes usable in Actors: class ActorOutputSingleImage(ActorOutputImage): data_container_class = SingleItkImage +class ActorOutputSingleImageOfHistogram(ActorOutputImageOfHistogram): + data_container_class = SingleItkImage + + class ActorOutputSingleMeanImage(ActorOutputImage): data_container_class = SingleMeanItkImage @@ -960,6 +975,7 @@ def initialize_cpp_parameters(self): process_cls(ActorOutputUsingDataItemContainer) process_cls(ActorOutputImage) process_cls(ActorOutputSingleImage) +process_cls(ActorOutputSingleImageOfHistogram) process_cls(ActorOutputSingleMeanImage) process_cls(ActorOutputSingleImageWithVariance) process_cls(ActorOutputQuotientImage) diff --git a/opengate/actors/dataitems.py b/opengate/actors/dataitems.py index 8da8d1e42..b0d6d5567 100644 --- a/opengate/actors/dataitems.py +++ b/opengate/actors/dataitems.py @@ -289,7 +289,17 @@ def set_image_properties(self, **properties): if "origin" in properties and properties["origin"] is not None: self.data.SetOrigin(properties["origin"]) if "rotation" in properties and properties["rotation"] is not None: - self.data.SetDirection(properties["rotation"]) + r = properties["rotation"] + if self.data.GetImageDimension() == 4: + # for 4D image, the rotation is enhanced + r = np.pad( + r, + pad_width=((0, 1), (0, 1)), + mode="constant", + constant_values=0, + ) + r[3][3] = 1.0 + self.data.SetDirection(r) def get_image_properties(self): return get_info_from_image(self.data) diff --git a/opengate/actors/doseactors.py b/opengate/actors/doseactors.py index 5e8bf6f92..3173c11f7 100644 --- a/opengate/actors/doseactors.py +++ b/opengate/actors/doseactors.py @@ -172,7 +172,10 @@ def _update_output_coordinate_system(self, which_output, run_index): origin = -size * spacing / 2.0 + spacing / 2.0 translation = np.array(self.translation) - origin_local = Rotation.from_matrix(self.rotation).apply(origin) + translation + # the slicing "[:3]" is required for 4D images (3D image of histograms) + origin_local = ( + Rotation.from_matrix(self.rotation).apply(origin[:3]) + translation[:3] + ) # image centered at (0,0,0), no rotation if self.output_coordinate_system is None: @@ -184,6 +187,9 @@ def _update_output_coordinate_system(self, which_output, run_index): # image centered at self.translation and rotated by self.rotation, # i.e. in the reference frame of the volume to which the actor is attached. elif self.output_coordinate_system in ("local",): + if size.shape != origin_local.shape: + # special case for 4D images + origin_local = np.append(origin_local, [0]) self.user_output[which_output].set_image_properties( run_index, origin=origin_local.tolist(), rotation=self.rotation ) @@ -705,55 +711,6 @@ def EndSimulationAction(self): VoxelDepositActor.EndSimulationAction(self) -class TLEDoseActor(DoseActor, g4.GateTLEDoseActor): - """TLE = Track Length Estimator""" - - energy_min: float - energy_max: float - database: str - - user_info_defaults = { - "energy_min": ( - 0.0, - {"doc": "Kill the gamma if below this energy"}, - ), - "energy_max": ( - 1.0 * g4_units.MeV, - { - "doc": "Above this energy, do not perform TLE (TLE is only relevant for low energy gamma)" - }, - ), - "database": ( - "EPDL", - { - "doc": "which database to use", - "allowed_values": ("EPDL", "NIST"), # "simulated" does not work - }, - ), - } - - def __initcpp__(self): - g4.GateTLEDoseActor.__init__(self, self.user_info) - self.AddActions( - { - "BeginOfRunActionMasterThread", - "EndOfRunActionMasterThread", - "BeginOfRunAction", - "EndOfRunAction", - "BeginOfEventAction", - "SteppingAction", - "PreUserTrackingAction", - } - ) - - def initialize(self, *args): - if self.score_in != "material": - fatal( - f"TLEDoseActor cannot score in {self.score_in}, only 'material' is allowed." - ) - super().initialize(args) - - def _setter_hook_score_in_let_actor(self, value): if value.lower() in ("g4_water", "g4water"): """Assuming a misspelling of G4_WATER and correcting it to correct spelling; Note that this is rather dangerous operation.""" @@ -1038,7 +995,6 @@ def EndSimulationAction(self): process_cls(VoxelDepositActor) process_cls(DoseActor) -process_cls(TLEDoseActor) process_cls(LETActor) process_cls(FluenceActor) process_cls(ProductionAndStoppingActor) diff --git a/opengate/actors/tleactors.py b/opengate/actors/tleactors.py new file mode 100644 index 000000000..0d0de91e6 --- /dev/null +++ b/opengate/actors/tleactors.py @@ -0,0 +1,152 @@ +import opengate_core as g4 +from ..exception import fatal +from ..utility import g4_units +from ..base import process_cls +from .actoroutput import ( + ActorOutputSingleImageOfHistogram, + ActorOutputImage, + ActorOutputSingleImage, +) +from .doseactors import DoseActor, VoxelDepositActor + + +class TLEDoseActor(DoseActor, g4.GateTLEDoseActor): + """ + TLE = Track Length Estimator + + The Track Length Estimator (TLE) is a variance reduction technique used in Monte Carlo simulations to accelerate dose computation, particularly for photon transport. It calculates the dose contribution by integrating the track length of particles within a voxel, reducing the number of particles required for accurate results. TLE is efficient for photon simulations as it avoids the need to explicitly simulate individual interactions within each voxel. + + The main limitation of TLE is its inability to account for dose delocalization, as it assumes energy deposition occurs along the particle's track within a voxel. TLE is typically restricted to photons with energies below 1-2 MeV for standard voxel sizes, as higher energies may result in larger dose spread beyond the voxel boundaries, reducing accuracy. + + Two databases can be used: EPDL (Evaluated Photon Data Library): Provides detailed photon interaction cross-sections, including photoelectric, Compton scattering, and pair production, for accurate modeling across a wide energy range. NIST (National Institute of Standards and Technology) XCOM database: Supplies photon attenuation and energy absorption coefficients, widely used for material property definitions and dose computations. + + """ + + energy_min: float + energy_max: float + database: str + + user_info_defaults = { + "energy_min": ( + 0.0, + {"doc": "Kill the gamma if below this energy"}, + ), + "energy_max": ( + 1.0 * g4_units.MeV, + { + "doc": "Above this energy, do not perform TLE (TLE is only relevant for low energy gamma)" + }, + ), + "database": ( + "EPDL", + { + "doc": "which database to use", + "allowed_values": ("EPDL", "NIST"), # "simulated" does not work + }, + ), + } + + def __initcpp__(self): + g4.GateTLEDoseActor.__init__(self, self.user_info) + self.AddActions( + { + "BeginOfRunActionMasterThread", + "EndOfRunActionMasterThread", + "BeginOfRunAction", + "EndOfRunAction", + "BeginOfEventAction", + "SteppingAction", + "PreUserTrackingAction", + } + ) + + def initialize(self, *args): + if self.score_in != "material": + fatal( + f"TLEDoseActor cannot score in {self.score_in}, only 'material' is allowed." + ) + super().initialize(args) + + +class VoxelizedPromptGammaTLEActor( + VoxelDepositActor, g4.GateVoxelizedPromptGammaTLEActor +): + """ + FIXME doc todo + """ + + user_info_defaults = { + "stage_0_database": ( + None, + { + "doc": "TODO", + }, + ), + "bins": (100, {"doc": "TODO"}), + } + + user_output_config = { + "vpg": { + # FIXME change to something like ImageOfHisto ? + "actor_output_class": ActorOutputSingleImageOfHistogram, + "active": True, + }, + } + + def __init__(self, *args, **kwargs) -> None: + VoxelDepositActor.__init__(self, *args, **kwargs) + self.__initcpp__() + + def __initcpp__(self): + g4.GateVoxelizedPromptGammaTLEActor.__init__(self, self.user_info) + self.AddActions( + { + "BeginOfRunActionMasterThread", + "BeginOfEventAction", + "SteppingAction", + "PreUserTrackingAction", + "EndOfRunActionMasterThread", + } + ) + + def initialize(self, *args): + self.check_user_input() + VoxelDepositActor.initialize(self) + + # C++ side + self.InitializeUserInfo(self.user_info) + self.InitializeCpp() + + def prepare_output_for_run(self, output_name, run_index, **kwargs): + ## need to override because create image is different for img of histo + self._assert_output_exists(output_name) + self.user_output[output_name].create_image_of_histograms( + run_index, + self.size, + self.spacing, + self.bins, + origin=self.translation, + **kwargs, + ) + + def BeginOfRunActionMasterThread(self, run_index): + self.prepare_output_for_run("vpg", run_index) + self.push_to_cpp_image("vpg", run_index, self.cpp_image) + g4.GateVoxelizedPromptGammaTLEActor.BeginOfRunActionMasterThread( + self, run_index + ) + + def EndOfRunActionMasterThread(self, run_index): + print("end of run action master thread", run_index) + self.fetch_from_cpp_image("vpg", run_index, self.cpp_image) + self._update_output_coordinate_system("vpg", run_index) + VoxelDepositActor.EndOfRunActionMasterThread(self, run_index) + return 0 + + def EndSimulationAction(self): + g4.GateVoxelizedPromptGammaTLEActor.EndSimulationAction(self) + VoxelDepositActor.EndSimulationAction(self) + + +process_cls(TLEDoseActor) +process_cls(VoxelizedPromptGammaTLEActor) diff --git a/opengate/image.py b/opengate/image.py index dcb088f57..36040922d 100644 --- a/opengate/image.py +++ b/opengate/image.py @@ -59,6 +59,33 @@ def create_3d_image( return img +def create_3d_image_of_histogram( + size, spacing, bins, origin=None, pixel_type="float", allocate=True +): + size.append(bins) + spacing.append(1.0) + if origin is not None: + origin.append(0.0) + return create_4d_image(size, spacing, origin, pixel_type, allocate) + + +def create_4d_image(size, spacing, origin=None, pixel_type="float", allocate=True): + dim = 4 + pixel_type = itk.ctype(pixel_type) + image_type = itk.Image[pixel_type, dim] + img = image_type.New() + region = itk.ImageRegion[dim]() + region.SetSize([int(s) for s in size]) + region.SetIndex([0, 0, 0, 0]) + img.SetRegions(region) + img.SetSpacing(spacing) + if origin is not None: + img.SetOrigin(origin) + if allocate: + img.Allocate() + return img + + def create_image_like(like_image, allocate=True, pixel_type=""): # TODO fix pixel_type -> copy from image rather than argument info = get_info_from_image(like_image) diff --git a/opengate/managers.py b/opengate/managers.py index e30f6f6a0..ade5a4f35 100644 --- a/opengate/managers.py +++ b/opengate/managers.py @@ -78,11 +78,11 @@ from .actors.base import ActorBase from .actors.doseactors import ( DoseActor, - TLEDoseActor, LETActor, FluenceActor, ProductionAndStoppingActor, ) +from .actors.tleactors import TLEDoseActor, VoxelizedPromptGammaTLEActor from .actors.dynamicactors import DynamicGeometryActor from .actors.arfactors import ARFActor, ARFTrainingDatasetActor from .actors.miscactors import ( @@ -121,6 +121,7 @@ # dose related "DoseActor": DoseActor, "TLEDoseActor": TLEDoseActor, + "VoxelizedPromptGammaTLEActor": VoxelizedPromptGammaTLEActor, "LETActor": LETActor, "ProductionAndStoppingActor": ProductionAndStoppingActor, "FluenceActor": FluenceActor, From 3b2c6b76c6820981c84c3ba02c1e934a219a26f1 Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 21 Jan 2025 12:40:46 +0100 Subject: [PATCH 07/13] first tests 87 template --- .../tests/src/test085_free_flight_helpers.py | 0 .../tests/src/test087_vpg_tle_stage1_actor.py | 90 +++++++++++++++++++ .../src/test087_vpg_tle_stage2_source.py | 71 +++++++++++++++ 3 files changed, 161 insertions(+) mode change 100755 => 100644 opengate/tests/src/test085_free_flight_helpers.py create mode 100755 opengate/tests/src/test087_vpg_tle_stage1_actor.py create mode 100755 opengate/tests/src/test087_vpg_tle_stage2_source.py diff --git a/opengate/tests/src/test085_free_flight_helpers.py b/opengate/tests/src/test085_free_flight_helpers.py old mode 100755 new mode 100644 diff --git a/opengate/tests/src/test087_vpg_tle_stage1_actor.py b/opengate/tests/src/test087_vpg_tle_stage1_actor.py new file mode 100755 index 000000000..609294b78 --- /dev/null +++ b/opengate/tests/src/test087_vpg_tle_stage1_actor.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from vtkmodules.vtkFiltersModeling import vtkGeodesicPath + +import opengate as gate +from opengate.tests import utility + +if __name__ == "__main__": + paths = utility.get_default_test_paths(__file__, output_folder="test087_vpg_tle") + + # create the simulation + sim = gate.Simulation() + + # main options + # sim.visu = True + sim.visu_type = "qt" + sim.random_seed = "auto" # FIXME to be replaced by a fixed number at the end + sim.output_dir = paths.output + sim.progress_bar = True + sim.number_of_threads = 1 + + # units + mm = gate.g4_units.mm + cm = gate.g4_units.cm + m = gate.g4_units.m + gcm3 = gate.g4_units.g_cm3 + keV = gate.g4_units.keV + MeV = gate.g4_units.MeV + Bq = gate.g4_units.Bq + + # change world size + world = sim.world + world.size = [3 * m, 3 * m, 3 * m] + + # insert voxelized CT + ct = sim.add_volume("Image", "ct") + ct.image = paths.data / f"ct_4mm.mhd" + if sim.visu: + ct.image = paths.data / f"ct_40mm.mhd" + ct.material = "G4_AIR" + f1 = str(paths.data / "Schneider2000MaterialsTable.txt") + f2 = str(paths.data / "Schneider2000DensitiesTable.txt") + tol = 0.05 * gcm3 + ( + ct.voxel_materials, + materials, + ) = gate.geometry.materials.HounsfieldUnit_to_material(sim, tol, f1, f2) + print(f"tol = {tol/gcm3} g/cm3") + print(f"mat : {len(ct.voxel_materials)} materials") + ct.dump_label_image = paths.output / "labels.mhd" + + # physics + sim.physics_manager.physics_list_name = "G4EmStandardPhysics_option3" + sim.physics_manager.global_production_cuts.all = 1 * mm + + # source of proton + # FIXME to replace by a more realistic proton beam, see tests 044 + source = sim.add_source("GenericSource", "proton_beam") + source.energy.mono = 130 * MeV + source.particle = "proton" + source.position.type = "sphere" + source.position.radius = 10 * mm + source.position.translation = [0, -44 * cm, 0] + source.activity = 10000 * Bq + source.direction.type = "momentum" + source.direction.momentum = [0, 1, 0] + + # add vpgtle actor + vpg_tle = sim.add_actor("VoxelizedPromptGammaTLEActor", "vpgtle") + vpg_tle.attached_to = ct + vpg_tle.output_filename = "vpgtle.nii.gz" + vpg_tle.size = [100, 100, 100] + vpg_tle.bins = 50 + vpg_tle.spacing = [2 * mm, 2 * mm, 2 * mm] + # FIXME to put elsewhere ? change the name ? + vpg_tle.stage_0_database = paths.data / "vpgtle_stage0.db" + + # add stat actor + stats = sim.add_actor("SimulationStatisticsActor", "stats") + stats.track_types_flag = True + + # start simulation + sim.run() + + # print results at the end + print(stats) + + # FIXME to a real test + is_ok = False + utility.test_ok(is_ok) diff --git a/opengate/tests/src/test087_vpg_tle_stage2_source.py b/opengate/tests/src/test087_vpg_tle_stage2_source.py new file mode 100755 index 000000000..a55cc41f5 --- /dev/null +++ b/opengate/tests/src/test087_vpg_tle_stage2_source.py @@ -0,0 +1,71 @@ +#!/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__, output_folder="test087_vpg_tle") + + # create the simulation + sim = gate.Simulation() + + # main options + sim.visu = True + sim.visu_type = "qt" + sim.random_seed = "auto" # FIXME to be replaced by a fixed number at the end + sim.output_dir = paths.output + sim.progress_bar = True + sim.number_of_threads = 1 + + # units + mm = gate.g4_units.mm + cm = gate.g4_units.cm + m = gate.g4_units.m + gcm3 = gate.g4_units.g_cm3 + keV = gate.g4_units.keV + MeV = gate.g4_units.MeV + Bq = gate.g4_units.Bq + + # change world size + world = sim.world + world.size = [3 * m, 3 * m, 3 * m] + + # insert voxelized CT + ct = sim.add_volume("Image", "ct") + ct.image = paths.data / f"ct_4mm.mhd" + if sim.visu: + ct.image = paths.data / f"ct_40mm.mhd" + ct.material = "G4_AIR" + f1 = str(paths.data / "Schneider2000MaterialsTable.txt") + f2 = str(paths.data / "Schneider2000DensitiesTable.txt") + tol = 0.05 * gcm3 + ( + ct.voxel_materials, + materials, + ) = gate.geometry.materials.HounsfieldUnit_to_material(sim, tol, f1, f2) + print(f"tol = {tol/gcm3} g/cm3") + print(f"mat : {len(ct.voxel_materials)} materials") + ct.dump_label_image = paths.output / "labels.mhd" + + # physics + sim.physics_manager.physics_list_name = "G4EmStandardPhysics_option3" + sim.physics_manager.global_production_cuts.all = 1 * mm + + # add vpgtle source + vpg_tle = sim.add_actor("VoxelizedPromptGammaTLESource", "vpgtle") + vpg_tle.attached_to = ct + vpg_tle.output_filename = "vpgtle.mhd" + + # add stat actor + stats = sim.add_actor("SimulationStatisticsActor", "stats") + stats.track_types_flag = True + + # start simulation + sim.run() + + # print results at the end + print(stats) + + # FIXME to a real test + is_ok = False + utility.test_ok(is_ok) From 5cc1b56fa278a901761cb5c950ba63ee0d59a6e6 Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 21 Jan 2025 18:52:19 +0100 Subject: [PATCH 08/13] vpgTLE source template --- core/opengate_core/opengate_core.cpp | 3 ++ .../GateVoxelizedPromptGammaTLESource.cpp | 52 +++++++++++++++++++ .../GateVoxelizedPromptGammaTLESource.h | 37 +++++++++++++ .../pyGateVoxelizedPromptGammaTLESource.cpp | 24 +++++++++ 4 files changed, 116 insertions(+) create mode 100644 core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLESource.cpp create mode 100644 core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLESource.h create mode 100644 core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaTLESource.cpp diff --git a/core/opengate_core/opengate_core.cpp b/core/opengate_core/opengate_core.cpp index 8f911dd66..bc6314765 100644 --- a/core/opengate_core/opengate_core.cpp +++ b/core/opengate_core/opengate_core.cpp @@ -392,6 +392,8 @@ void init_GatePencilBeamSource(py::module &m); void init_GateVoxelSource(py::module &); +void init_GateVoxelizedPromptGammaTLESource(py::module &); + void init_GateGANSource(py::module &); void init_GatePhaseSpaceSource(py::module &); @@ -577,6 +579,7 @@ PYBIND11_MODULE(opengate_core, m) { init_GateTemplateSource(m); init_GatePencilBeamSource(m); init_GateVoxelSource(m); + init_GateVoxelizedPromptGammaTLESource(m); init_GateGANSource(m); init_GatePhaseSpaceSource(m); init_GateGANPairSource(m); diff --git a/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLESource.cpp b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLESource.cpp new file mode 100644 index 000000000..2b3a5daba --- /dev/null +++ b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLESource.cpp @@ -0,0 +1,52 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + -------------------------------------------------- */ + +#include "GateVoxelizedPromptGammaTLESource.h" +#include "G4ParticleTable.hh" +#include "GateHelpersDict.h" +#include "GateHelpersGeometry.h" + +GateVoxelizedPromptGammaTLESource::GateVoxelizedPromptGammaTLESource() + : GateGenericSource() { + fVoxelPositionGenerator = new GateSPSVoxelsPosDistribution(); + std::cout << "construct GateVoxelizedPromptGammaTLESource" << std::endl; +} + +GateVoxelizedPromptGammaTLESource::~GateVoxelizedPromptGammaTLESource() = + default; + +void GateVoxelizedPromptGammaTLESource::PrepareNextRun() { + std::cout << "GateVoxelizedPromptGammaTLESource::PrepareNextRun" << std::endl; + // GateGenericSource::PrepareNextRun(); + // rotation and translation to apply, according to mother volume + GateVSource::PrepareNextRun(); + // This global transformation is given to the SPS that will + // generate particles in the correct coordinate system + auto &l = GetThreadLocalData(); + auto &ll = GetThreadLocalDataGenericSource(); + auto *pos = ll.fSPS->GetPosDist(); + pos->SetCentreCoords(l.fGlobalTranslation); + + // orientation according to mother volume + auto rotation = l.fGlobalRotation; + G4ThreeVector r1(rotation(0, 0), rotation(1, 0), rotation(2, 0)); + G4ThreeVector r2(rotation(0, 1), rotation(1, 1), rotation(2, 1)); + pos->SetPosRot1(r1); + pos->SetPosRot2(r2); + + // auto &l = fThreadLocalData.Get(); + fVoxelPositionGenerator->fGlobalRotation = l.fGlobalRotation; + fVoxelPositionGenerator->fGlobalTranslation = l.fGlobalTranslation; + // the direction is 'isotropic' so we don't care about rotating the direction. +} + +void GateVoxelizedPromptGammaTLESource::InitializePosition(py::dict) { + auto &ll = GetThreadLocalDataGenericSource(); + ll.fSPS->SetPosGenerator(fVoxelPositionGenerator); + // we set a fake value (not used) + fVoxelPositionGenerator->SetPosDisType("Point"); +} diff --git a/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLESource.h b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLESource.h new file mode 100644 index 000000000..84b3afcb4 --- /dev/null +++ b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLESource.h @@ -0,0 +1,37 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + -------------------------------------------------- */ + +#ifndef GateVoxelizedPromptGammaTLESource_h +#define GateVoxelizedPromptGammaTLESource_h + +#include "GateGenericSource.h" +#include "GateSPSVoxelsPosDistribution.h" +#include "GateSingleParticleSource.h" +#include + +namespace py = pybind11; + +class GateVoxelizedPromptGammaTLESource : public GateGenericSource { + +public: + GateVoxelizedPromptGammaTLESource(); + + ~GateVoxelizedPromptGammaTLESource() override; + + void PrepareNextRun() override; + + GateSPSVoxelsPosDistribution *GetSPSVoxelPosDistribution() { + return fVoxelPositionGenerator; + } + +protected: + void InitializePosition(py::dict user_info) override; + + GateSPSVoxelsPosDistribution *fVoxelPositionGenerator; +}; + +#endif // GateVoxelizedPromptGammaTLESource_h diff --git a/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaTLESource.cpp b/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaTLESource.cpp new file mode 100644 index 000000000..69b4b74e2 --- /dev/null +++ b/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaTLESource.cpp @@ -0,0 +1,24 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + -------------------------------------------------- */ + +#include + +namespace py = pybind11; + +#include "GateVoxelizedPromptGammaTLESource.h" + +void init_GateVoxelizedPromptGammaTLESource(py::module &m) { + + py::class_( + m, "GateVoxelizedPromptGammaTLESource") + .def(py::init()) + .def("GetSPSVoxelPosDistribution", + &GateVoxelizedPromptGammaTLESource::GetSPSVoxelPosDistribution, + py::return_value_policy::reference_internal) + .def("InitializeUserInfo", + &GateVoxelizedPromptGammaTLESource::InitializeUserInfo); +} From 214ead0a932eadadf1d34a669092b7fd13097dd4 Mon Sep 17 00:00:00 2001 From: David Sarrut Date: Tue, 21 Jan 2025 18:54:00 +0100 Subject: [PATCH 09/13] vpgTLE source template --- opengate/actors/tleactors.py | 3 +- opengate/managers.py | 3 +- opengate/sources/voxelsources.py | 56 ++++++++++++++++++- .../src/test087_vpg_tle_stage2_source.py | 5 +- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/opengate/actors/tleactors.py b/opengate/actors/tleactors.py index 0d0de91e6..b33b772f6 100644 --- a/opengate/actors/tleactors.py +++ b/opengate/actors/tleactors.py @@ -87,7 +87,6 @@ class VoxelizedPromptGammaTLEActor( user_output_config = { "vpg": { - # FIXME change to something like ImageOfHisto ? "actor_output_class": ActorOutputSingleImageOfHistogram, "active": True, }, @@ -118,7 +117,7 @@ def initialize(self, *args): self.InitializeCpp() def prepare_output_for_run(self, output_name, run_index, **kwargs): - ## need to override because create image is different for img of histo + # need to override because create image is different for img of histo self._assert_output_exists(output_name) self.user_output[output_name].create_image_of_histograms( run_index, diff --git a/opengate/managers.py b/opengate/managers.py index ade5a4f35..b56a5785a 100644 --- a/opengate/managers.py +++ b/opengate/managers.py @@ -40,7 +40,7 @@ from .sources.generic import SourceBase, GenericSource from .sources.phspsources import PhaseSpaceSource -from .sources.voxelsources import VoxelSource +from .sources.voxelsources import VoxelSource, VoxelizedPromptGammaTLESource from .sources.gansources import GANSource, GANPairsSource from .sources.beamsources import IonPencilBeamSource, TreatmentPlanPBSource from .sources.phidsources import PhotonFromIonDecaySource @@ -55,6 +55,7 @@ "IonPencilBeamSource": IonPencilBeamSource, "PhotonFromIonDecaySource": PhotonFromIonDecaySource, "TreatmentPlanPBSource": TreatmentPlanPBSource, + "VoxelizedPromptGammaTLESource": VoxelizedPromptGammaTLESource, } from .geometry.volumes import ( diff --git a/opengate/sources/voxelsources.py b/opengate/sources/voxelsources.py index 29f497eb2..a02637ca6 100644 --- a/opengate/sources/voxelsources.py +++ b/opengate/sources/voxelsources.py @@ -1,5 +1,4 @@ import itk - import opengate_core as g4 from .generic import GenericSource from ..image import ( @@ -86,4 +85,59 @@ def initialize(self, run_timing_intervals): GenericSource.initialize(self, run_timing_intervals) +class VoxelizedPromptGammaTLESource(VoxelSource, g4.GateVoxelizedPromptGammaTLESource): + """ + todo + """ + + user_info_defaults = { + "pg_image": ( + None, + { + "doc": "Filename of the image of the 3D activity distribution " + "(will be automatically normalized to sum=1)", + "is_input_file": True, + }, + ) + } + + def __init__(self, *args, **kwargs): + self.__initcpp__() + super().__init__(self, *args, **kwargs) + # the loaded image + self.itk_pg_image = None + # the image for the 3D distribution sampling : integral of the histogram + self.itk_image = None + + def __initcpp__(self): + g4.GateVoxelSource.__init__(self) + g4.GateVoxelizedPromptGammaTLESource.__init__(self) + + def initialize(self, run_timing_intervals): + # read source image + print("reading ", self.pg_image) + self.itk_pg_image = itk.imread(ensure_filename_is_str(self.pg_image)) + print( + "read image input", self.itk_pg_image.GetLargestPossibleRegion().GetSize() + ) + print("read image spacing", self.itk_pg_image.GetSpacing()) + print("before origin", self.itk_pg_image.GetOrigin()) + + # FIXME todo, compute itk_image with integrated histo for 3D sampling + + # compute position + # self.set_transform_from_user_info() + # print("after origin", self.itk_image.GetOrigin()) + + # create Cumulative Distribution Function + # self.cumulative_distribution_functions() + + # initialize standard options (particle energy, etc.) + # we temporarily set the position attribute to reuse + # the GenericSource verification + ## FIXME unsure here -> some options are fixed (particle is always gamma, etc), other maybe variable + GenericSource.initialize(self, run_timing_intervals) + + process_cls(VoxelSource) +process_cls(VoxelizedPromptGammaTLESource) diff --git a/opengate/tests/src/test087_vpg_tle_stage2_source.py b/opengate/tests/src/test087_vpg_tle_stage2_source.py index a55cc41f5..e6906ffd6 100755 --- a/opengate/tests/src/test087_vpg_tle_stage2_source.py +++ b/opengate/tests/src/test087_vpg_tle_stage2_source.py @@ -52,9 +52,10 @@ sim.physics_manager.global_production_cuts.all = 1 * mm # add vpgtle source - vpg_tle = sim.add_actor("VoxelizedPromptGammaTLESource", "vpgtle") + vpg_tle = sim.add_source("VoxelizedPromptGammaTLESource", "vpgtle") vpg_tle.attached_to = ct - vpg_tle.output_filename = "vpgtle.mhd" + vpg_tle.pg_image = paths.output / "vpgtle.nii.gz" + vpg_tle.activity = 100 * Bq # (total in the whole image) # add stat actor stats = sim.add_actor("SimulationStatisticsActor", "stats") From 2b529658f5277a2d81c49bae64aeee2750d1faee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean=20M=20L=C3=A9tang?= Date: Wed, 3 Dec 2025 09:31:47 +0100 Subject: [PATCH 10/13] merged Violette fork --- .../GateVoxelizedPromptGammaAnalogActor.cpp | 295 ++++++ .../GateVoxelizedPromptGammaAnalogActor.h | 98 ++ .../GateVoxelizedPromptGammaTLEActor.cpp | 248 ++++- .../GateVoxelizedPromptGammaTLEActor.h | 80 +- .../pyGateVoxelizedPromptGammaAnalogActor.cpp | 81 ++ .../pyGateVoxelizedPromptGammaTLEActor.cpp | 36 +- opengate/actors/tleactors.py | 348 ++++++- opengate/contrib/vpgTLE/howto_vpg_TLE.pdf | Bin 0 -> 210759 bytes opengate/contrib/vpgTLE/stage0/CMakeLists.txt | 62 ++ opengate/contrib/vpgTLE/stage0/GNUmakefile | 12 + .../vpgTLE/stage0/Hadr09.cc-ION_PROJECTILE | 365 ++++++++ .../vpgTLE/stage0/HadronicGenerator.cc | 629 +++++++++++++ opengate/contrib/vpgTLE/stage0/History | 31 + .../G4ParticleHPInelastic.cc | 318 +++++++ .../G4ParticleHPInelastic.hh | 90 ++ opengate/contrib/vpgTLE/stage0/README | 43 + .../include/HadronicGenerator-vpgtle.hh | 168 ++++ opengate/contrib/vpgTLE/stage0/merge_root.py | 98 ++ .../contrib/vpgTLE/stage0/run_stage0.slurm | 24 + .../stage0/src/HadronicGenerator-vpgtle.cc | 605 +++++++++++++ .../contrib/vpgTLE/stage0/stage0-vpgtle.cc | 343 +++++++ opengate/contrib/vpgTLE/stageX/__init__.py | 0 .../GateVoxelizedPromptGammaAnalogActor.cpp | 295 ++++++ .../GateVoxelizedPromptGammaAnalogActor.h | 98 ++ .../GateVoxelizedPromptGammaTLEActor.cpp | 282 ++++++ .../actor/GateVoxelizedPromptGammaTLEActor.h | 107 +++ .../pyGateVoxelizedPromptGammaAnalogActor.cpp | 81 ++ .../pyGateVoxelizedPromptGammaTLEActor.cpp | 79 ++ .../vpgTLE/stageX/data/GateMaterials.db | 478 ++++++++++ .../stageX/data/GateMaterialsElements.db | 52 ++ .../contrib/vpgTLE/stageX/data/Materials.xml | 849 ++++++++++++++++++ .../data/Schneider2000DensitiesTable.txt | 14 + .../data/Schneider2000MaterialsTable.txt | 36 + .../contrib/vpgTLE/stageX/data/database.db | 690 ++++++++++++++ .../contrib/vpgTLE/stageX/data/database.txt | 686 ++++++++++++++ .../contrib/vpgTLE/stageX/energy_patient.py | 71 ++ .../vpgTLE/stageX/lecture4Dcoupetheta_prot.py | 129 +++ opengate/contrib/vpgTLE/stageX/profile12.py | 148 +++ .../contrib/vpgTLE/stageX/profilelongitude.py | 134 +++ opengate/contrib/vpgTLE/stageX/readme.md | 8 + .../contrib/vpgTLE/stageX/remerge_data.py | 38 + opengate/contrib/vpgTLE/stageX/split_data.py | 74 ++ .../vpgTLE/stageX/stage1/multijobs_stage1.py | 304 +++++++ .../contrib/vpgTLE/stageX/stage1/stage_1a.py | 147 +++ .../contrib/vpgTLE/stageX/stage1/stage_1b.py | 160 ++++ .../contrib/vpgTLE/stageX/stage1/variables.py | 160 ++++ .../contrib/vpgTLE/stageX/time-patient.py | 78 ++ opengate/tests/src/test087_multijobs.py | 274 ++++++ opengate/tests/src/test087_spectrum.py | 122 +++ opengate/tests/src/test087_variables.py | 185 ++++ opengate/tests/src/test087_vpg_tle_script.py | 151 ++++ .../tests/src/test087_vpg_tle_stage1_actor.py | 5 +- 52 files changed, 9858 insertions(+), 51 deletions(-) create mode 100644 core/opengate_core/opengate_lib/GateVoxelizedPromptGammaAnalogActor.cpp create mode 100644 core/opengate_core/opengate_lib/GateVoxelizedPromptGammaAnalogActor.h create mode 100644 core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaAnalogActor.cpp create mode 100644 opengate/contrib/vpgTLE/howto_vpg_TLE.pdf create mode 100644 opengate/contrib/vpgTLE/stage0/CMakeLists.txt create mode 100644 opengate/contrib/vpgTLE/stage0/GNUmakefile create mode 100644 opengate/contrib/vpgTLE/stage0/Hadr09.cc-ION_PROJECTILE create mode 100644 opengate/contrib/vpgTLE/stage0/HadronicGenerator.cc create mode 100644 opengate/contrib/vpgTLE/stage0/History create mode 100644 opengate/contrib/vpgTLE/stage0/Modified_G4HP_File/G4ParticleHPInelastic.cc create mode 100644 opengate/contrib/vpgTLE/stage0/Modified_G4HP_File/G4ParticleHPInelastic.hh create mode 100644 opengate/contrib/vpgTLE/stage0/README create mode 100644 opengate/contrib/vpgTLE/stage0/include/HadronicGenerator-vpgtle.hh create mode 100644 opengate/contrib/vpgTLE/stage0/merge_root.py create mode 100644 opengate/contrib/vpgTLE/stage0/run_stage0.slurm create mode 100644 opengate/contrib/vpgTLE/stage0/src/HadronicGenerator-vpgtle.cc create mode 100644 opengate/contrib/vpgTLE/stage0/stage0-vpgtle.cc create mode 100644 opengate/contrib/vpgTLE/stageX/__init__.py create mode 100644 opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaAnalogActor.cpp create mode 100644 opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaAnalogActor.h create mode 100644 opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaTLEActor.cpp create mode 100644 opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaTLEActor.h create mode 100644 opengate/contrib/vpgTLE/stageX/actor/pyGateVoxelizedPromptGammaAnalogActor.cpp create mode 100644 opengate/contrib/vpgTLE/stageX/actor/pyGateVoxelizedPromptGammaTLEActor.cpp create mode 100644 opengate/contrib/vpgTLE/stageX/data/GateMaterials.db create mode 100644 opengate/contrib/vpgTLE/stageX/data/GateMaterialsElements.db create mode 100644 opengate/contrib/vpgTLE/stageX/data/Materials.xml create mode 100644 opengate/contrib/vpgTLE/stageX/data/Schneider2000DensitiesTable.txt create mode 100644 opengate/contrib/vpgTLE/stageX/data/Schneider2000MaterialsTable.txt create mode 100644 opengate/contrib/vpgTLE/stageX/data/database.db create mode 100644 opengate/contrib/vpgTLE/stageX/data/database.txt create mode 100644 opengate/contrib/vpgTLE/stageX/energy_patient.py create mode 100644 opengate/contrib/vpgTLE/stageX/lecture4Dcoupetheta_prot.py create mode 100644 opengate/contrib/vpgTLE/stageX/profile12.py create mode 100644 opengate/contrib/vpgTLE/stageX/profilelongitude.py create mode 100644 opengate/contrib/vpgTLE/stageX/readme.md create mode 100644 opengate/contrib/vpgTLE/stageX/remerge_data.py create mode 100644 opengate/contrib/vpgTLE/stageX/split_data.py create mode 100644 opengate/contrib/vpgTLE/stageX/stage1/multijobs_stage1.py create mode 100644 opengate/contrib/vpgTLE/stageX/stage1/stage_1a.py create mode 100644 opengate/contrib/vpgTLE/stageX/stage1/stage_1b.py create mode 100644 opengate/contrib/vpgTLE/stageX/stage1/variables.py create mode 100644 opengate/contrib/vpgTLE/stageX/time-patient.py create mode 100644 opengate/tests/src/test087_multijobs.py create mode 100755 opengate/tests/src/test087_spectrum.py create mode 100755 opengate/tests/src/test087_variables.py create mode 100755 opengate/tests/src/test087_vpg_tle_script.py diff --git a/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaAnalogActor.cpp b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaAnalogActor.cpp new file mode 100644 index 000000000..9e327e3d3 --- /dev/null +++ b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaAnalogActor.cpp @@ -0,0 +1,295 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + ------------------------------------ -------------- */ + +#include "G4EmCalculator.hh" +#include "G4Gamma.hh" +#include "G4ParticleDefinition.hh" +#include "G4RandomTools.hh" +#include "G4RunManager.hh" +#include "G4Threading.hh" +#include "G4Track.hh" + +#include "G4HadronInelasticProcess.hh" +#include "GateHelpers.h" +#include "GateHelpersDict.h" +#include "GateHelpersImage.h" +#include "GateMaterialMuHandler.h" +#include "GateVoxelizedPromptGammaAnalogActor.h" + +#include "CLHEP/Random/Randomize.h" +#include +#include +#include +#include +#include + +#include +#include +// #include +#include +#include + +GateVoxelizedPromptGammaAnalogActor::GateVoxelizedPromptGammaAnalogActor( + py::dict &user_info) + : GateVActor(user_info, true) { + fMultiThreadReady = + true; // But used as a single thread python side : nb pf runs = 1 +} + +GateVoxelizedPromptGammaAnalogActor::~GateVoxelizedPromptGammaAnalogActor() { + // not needed + cpp_tof_proton_image = nullptr; + cpp_E_proton_image = nullptr; + cpp_E_neutron_image = nullptr; + cpp_tof_neutron_image = nullptr; + + // Release the 3D volume + volume = nullptr; + std::cout << "GateVoxelizedPromptGammaAnalogActor destructor called. " + "Resources released." + << std::endl; +} + +void GateVoxelizedPromptGammaAnalogActor::InitializeUserInfo( + py::dict &user_info) { + GateVActor::InitializeUserInfo(user_info); + + // retrieve the python param here + timebins = py::int_(user_info["timebins"]); + timerange = py::float_(user_info["timerange"]); + energybins = py::int_(user_info["energybins"]); + energyrange = py::float_(user_info["energyrange"]); + + fTranslation = DictGetG4ThreeVector(user_info, "translation"); + fsize = DictGetG4ThreeVector(user_info, "size"); + fspacing = DictGetG4ThreeVector(user_info, "spacing"); +} + +void GateVoxelizedPromptGammaAnalogActor::InitializeCpp() { + GateVActor::InitializeCpp(); + // Create the image pointers + // (the size and allocation will be performed on the py side) + if (fProtonTimeFlag) { + cpp_tof_proton_image = ImageType::New(); + } + if (fProtonEnergyFlag) { + cpp_E_proton_image = ImageType::New(); + } + if (fNeutronEnergyFlag) { + cpp_E_neutron_image = ImageType::New(); + } + if (fNeutronTimeFlag) { + cpp_tof_neutron_image = ImageType::New(); + } + + // Construction of the 3D image with the same shape/mat that the voxel of the + // actor but is accepted by the method of volume_attach and "isInside" + volume = Image3DType::New(); + + Image3DType::RegionType region; + Image3DType::SizeType size; + Image3DType::SpacingType spacing; + + size[0] = fsize[0]; + size[1] = fsize[1]; + size[2] = fsize[2]; + region.SetSize(size); + + spacing[0] = fspacing[0]; + spacing[1] = fspacing[1]; + spacing[2] = fspacing[2]; + + volume->SetRegions(region); + volume->SetSpacing(spacing); + volume->Allocate(); + volume->FillBuffer(0); + + incidentParticles = + 0; // initiate the conuter of incidente protons - scaling factor +} + +void GateVoxelizedPromptGammaAnalogActor::BeginOfRunActionMasterThread( + int run_id) { + // Attach the 3D volume used to + + // Fill the 4D volume of interest with 0 to ensure that it is well initiated + if (fProtonTimeFlag) { + cpp_tof_proton_image->FillBuffer(0); + } + if (fProtonEnergyFlag) { + cpp_E_proton_image->FillBuffer(0); + } + if (fNeutronEnergyFlag) { + cpp_E_neutron_image->FillBuffer(0); + } + if (fNeutronTimeFlag) { + cpp_tof_neutron_image->FillBuffer(0); + } + AttachImageToVolume(volume, fPhysicalVolumeName, fTranslation); +} + +void GateVoxelizedPromptGammaAnalogActor::BeginOfRunAction(const G4Run *run) {} + +void GateVoxelizedPromptGammaAnalogActor::BeginOfEventAction( + const G4Event *event) { + T0 = event->GetPrimaryVertex()->GetT0(); + incidentParticles++; +} + +void GateVoxelizedPromptGammaAnalogActor::SteppingAction(G4Step *step) { + if (step->GetTrack()->GetParticleDefinition() != G4Neutron::Neutron() && + (step->GetTrack()->GetParticleDefinition() != G4Proton::Proton())) { + return; + } + static G4HadronicProcessStore *store = G4HadronicProcessStore::Instance(); + static G4VProcess *protonInelastic = + store->FindProcess(G4Proton::Proton(), fHadronInelastic); + static G4VProcess *neutronInelastic = + store->FindProcess(G4Neutron::Neutron(), fHadronInelastic); + if (step->GetPostStepPoint()->GetProcessDefinedStep() != protonInelastic && + step->GetPostStepPoint()->GetProcessDefinedStep() != neutronInelastic) { + return; + } + + auto position = step->GetPostStepPoint()->GetPosition(); + auto touchable = step->GetPreStepPoint()->GetTouchable(); + // Get the voxel index + auto localPosition = + touchable->GetHistory()->GetTransform(0).TransformPoint(position); + + // convert G4ThreeVector to itk PointType + Image3DType::PointType point; + point[0] = localPosition[0]; + point[1] = localPosition[1]; + point[2] = localPosition[2]; + + Image3DType::IndexType index; + G4bool isInside = volume->TransformPhysicalPointToIndex(point, index); + if (!isInside) { // verification + return; // Skip if not inside the volume + } + + // Get the spatial index from the index obtained with the 3D volume and th + // emethod GetStepVoxelPosition() + ImageType::IndexType ind; + ind[0] = index[0]; + ind[1] = index[1]; + ind[2] = index[2]; + + auto secondaries = step->GetSecondary(); + for (size_t i = 0; i < secondaries->size(); i++) { + auto secondary = secondaries->at(i); + auto secondary_def = secondary->GetParticleDefinition(); + if (secondary_def != G4Gamma::Gamma()) { + continue; + } + G4double gammaEnergy = secondary->GetKineticEnergy(); // in MeV + // thershold with a minimum energy of 40 keV + if (gammaEnergy < 0.04 * CLHEP::MeV) { + continue; + } + + if (fProtonTimeFlag || + fNeutronTimeFlag) { // If the quantity of interest is the time of flight + // Get the time of flight + // G4double randomtime = G4UniformRand(); + // G4double pretime = step->GetPreStepPoint()->GetGlobalTime()- T0; //ns + // G4double posttime = step->GetPostStepPoint()->GetGlobalTime()- T0;//ns + G4double time = secondary->GetGlobalTime() - T0; // ns + + // Get the voxel index (fourth dim) corresponding to the time of flight + G4int bin = static_cast( + time / (timerange / timebins)); // Always the left bin + + if (bin >= timebins) { + bin = timebins; + } + ind[3] = bin; + // Store the value in the volume for neutrons OR protons -> LEFT BINNING + if (fProtonTimeFlag && + step->GetTrack()->GetParticleDefinition()->GetParticleName() == + "proton") { + ImageAddValue(cpp_tof_proton_image, ind, 1); + } + if (fNeutronTimeFlag && + step->GetTrack()->GetParticleDefinition()->GetParticleName() == + "neutron") { + ImageAddValue(cpp_tof_neutron_image, ind, 1); + } + } + if (fProtonEnergyFlag || + fNeutronEnergyFlag) { // when the quantity of interest is the energy + // Get the voxel index (fourth dim) corresponding to the energy of the + // projectile + G4int bin = static_cast( + gammaEnergy / (energyrange / energybins)); // Always the left bin + if (bin >= energybins) { + bin = energybins; + } + if (bin < 0) { + bin = 0; // underflow + } + ind[3] = bin; + // Store the value in the volume for neutrons OR protons -> LEFT BINNING + if (fProtonEnergyFlag && + step->GetTrack()->GetParticleDefinition()->GetParticleName() == + "proton") { + ImageAddValue(cpp_E_proton_image, ind, 1); + } + if (fNeutronEnergyFlag && + step->GetTrack()->GetParticleDefinition()->GetParticleName() == + "neutron") { + ImageAddValue(cpp_E_neutron_image, ind, 1); + } + } + } +} + +void GateVoxelizedPromptGammaAnalogActor::EndOfRunAction(const G4Run *run) { + std::cout << "incident particles : " << incidentParticles << std::endl; + if (incidentParticles == 0) { + std::cerr << "Error: incidentParticles is zero. Skipping scaling." + << std::endl; + return; + } + // scaling all the 4D voxels with th enumber of incident protons (= number of + // event) + if (fProtonTimeFlag) { + itk::ImageRegionIterator it( + cpp_tof_proton_image, cpp_tof_proton_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } + if (fProtonEnergyFlag) { + itk::ImageRegionIterator it( + cpp_E_proton_image, cpp_E_proton_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } + if (fNeutronEnergyFlag) { + itk::ImageRegionIterator it( + cpp_E_neutron_image, cpp_E_neutron_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } + if (fNeutronTimeFlag) { + itk::ImageRegionIterator it( + cpp_tof_neutron_image, + cpp_tof_neutron_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } +} + +int GateVoxelizedPromptGammaAnalogActor::EndOfRunActionMasterThread( + int run_id) { + return 0; +} diff --git a/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaAnalogActor.h b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaAnalogActor.h new file mode 100644 index 000000000..f207a5846 --- /dev/null +++ b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaAnalogActor.h @@ -0,0 +1,98 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + -------------------------------------------------- */ + +#ifndef GateVoxelizedPromptGammaAnalogActor_h +#define GateVoxelizedPromptGammaAnalogActor_h + +#include "G4Cache.hh" +#include "G4EmCalculator.hh" +#include "G4NistManager.hh" +#include "G4VPrimitiveScorer.hh" +#include "GateDoseActor.h" +#include "GateMaterialMuHandler.h" + +#include + +namespace py = pybind11; + +class GateVoxelizedPromptGammaAnalogActor : public GateVActor { + +public: + // destructor + ~GateVoxelizedPromptGammaAnalogActor() override; + + explicit GateVoxelizedPromptGammaAnalogActor(py::dict &user_info); + + void InitializeUserInfo(py::dict &user_info) override; + + void InitializeCpp() override; + + void BeginOfRunActionMasterThread(int run_id); + + int EndOfRunActionMasterThread(int run_id) override; + + void EndOfRunAction(const G4Run *run); + + void BeginOfRunAction(const G4Run *run); + + void BeginOfEventAction(const G4Event *event) override; + + void SteppingAction(G4Step *) override; + + inline bool GetProtonTimeFlag() const { return fProtonTimeFlag; } + + inline void SetProtonTimeFlag(const bool b) { fProtonTimeFlag = b; } + + inline bool GetNeutronTimeFlag() const { return fNeutronTimeFlag; } + + inline void SetNeutronTimeFlag(const bool b) { fNeutronTimeFlag = b; } + + inline bool GetProtonEnergyFlag() const { return fProtonEnergyFlag; } + + inline void SetProtonEnergyFlag(const bool b) { fProtonEnergyFlag = b; } + + inline bool GetNeutronEnergyFlag() const { return fNeutronEnergyFlag; } + + inline void SetNeutronEnergyFlag(const bool b) { fNeutronEnergyFlag = b; } + + inline std::string GetPhysicalVolumeName() const { + return fPhysicalVolumeName; + } + + inline void SetPhysicalVolumeName(std::string s) { fPhysicalVolumeName = s; } + + std::string fPhysicalVolumeName; + + // Image type + typedef itk::Image ImageType; + ImageType::Pointer cpp_tof_neutron_image; + ImageType::Pointer cpp_tof_proton_image; + ImageType::Pointer cpp_E_proton_image; + ImageType::Pointer cpp_E_neutron_image; + + typedef itk::Image Image3DType; + Image3DType::Pointer volume; + + G4double T0; + G4int incidentParticles; + + G4int timebins; + G4double timerange; + G4int energybins; + G4double energyrange; + + G4bool fProtonTimeFlag{}; + G4bool fProtonEnergyFlag{}; + G4bool fNeutronTimeFlag{}; + G4bool fNeutronEnergyFlag{}; + + G4ThreeVector fsize; + G4ThreeVector fspacing; + G4ThreeVector fTranslation; +}; + +#endif // GateVoxelizedPromptGammaAnalogActor_h diff --git a/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.cpp b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.cpp index 0ea9f364f..97c0dad42 100644 --- a/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.cpp +++ b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.cpp @@ -6,59 +6,277 @@ ------------------------------------ -------------- */ #include "G4EmCalculator.hh" +#include "G4Gamma.hh" #include "G4ParticleDefinition.hh" #include "G4RandomTools.hh" #include "G4RunManager.hh" #include "G4Threading.hh" +#include "G4Track.hh" +#include "G4HadronInelasticProcess.hh" #include "GateHelpers.h" #include "GateHelpersDict.h" #include "GateHelpersImage.h" #include "GateMaterialMuHandler.h" #include "GateVoxelizedPromptGammaTLEActor.h" +#include "CLHEP/Random/Randomize.h" #include #include +#include #include #include GateVoxelizedPromptGammaTLEActor::GateVoxelizedPromptGammaTLEActor( py::dict &user_info) - : GateVActor(user_info, false) { - fMultiThreadReady = false; + : GateVActor(user_info, true) { + fMultiThreadReady = + true; // But used as a single thread python side : nb pf runs = 1 +} + +GateVoxelizedPromptGammaTLEActor::~GateVoxelizedPromptGammaTLEActor() { + // not needed } void GateVoxelizedPromptGammaTLEActor::InitializeUserInfo(py::dict &user_info) { GateVActor::InitializeUserInfo(user_info); + // retrieve the python param here + timebins = py::int_(user_info["timebins"]); + timerange = py::float_(user_info["timerange"]); + energybins = py::int_(user_info["energybins"]); + energyrange = py::float_(user_info["energyrange"]); + weight = py::bool_(user_info["weight"]); + fTranslation = DictGetG4ThreeVector(user_info, "translation"); + fsize = DictGetG4ThreeVector(user_info, "size"); + fspacing = DictGetG4ThreeVector(user_info, "spacing"); } void GateVoxelizedPromptGammaTLEActor::InitializeCpp() { GateVActor::InitializeCpp(); // Create the image pointers // (the size and allocation will be performed on the py side) - cpp_image = ImageType::New(); + if (fProtonTimeFlag) { + cpp_tof_proton_image = ImageType::New(); + } + if (fProtonEnergyFlag) { + cpp_E_proton_image = ImageType::New(); + } + if (fNeutronEnergyFlag) { + cpp_E_neutron_image = ImageType::New(); + } + if (fNeutronTimeFlag) { + cpp_tof_neutron_image = ImageType::New(); + } + + // Construction of the 3D image with the same shape/mat that the voxel of the + // actor but is accepted by the method of volume_attach and "isInside" + volume = Image3DType::New(); + + Image3DType::RegionType region; + Image3DType::SizeType size; + Image3DType::SpacingType spacing; + + size[0] = fsize[0]; + size[1] = fsize[1]; + size[2] = fsize[2]; + region.SetSize(size); + + spacing[0] = fspacing[0]; + spacing[1] = fspacing[1]; + spacing[2] = fspacing[2]; + + volume->SetRegions(region); + volume->SetSpacing(spacing); + volume->Allocate(); + volume->FillBuffer(0); + + incidentParticles = + 0; // initiate the conuter of incidente protons - scaling factor } void GateVoxelizedPromptGammaTLEActor::BeginOfRunActionMasterThread( int run_id) { - std::cout << "Begin of run " << run_id << std::endl; - std::cout << "Image size " << cpp_image->GetLargestPossibleRegion().GetSize() - << std::endl; + // Attach the 3D volume used to + + // Fill the 4D volume of interest with 0 to ensure that it is well initiated + if (fProtonTimeFlag) { + cpp_tof_proton_image->FillBuffer(0); + } + if (fProtonEnergyFlag) { + cpp_E_proton_image->FillBuffer(0); + } + if (fNeutronEnergyFlag) { + cpp_E_neutron_image->FillBuffer(0); + } + if (fNeutronTimeFlag) { + cpp_tof_neutron_image->FillBuffer(0); + } + AttachImageToVolume(volume, fPhysicalVolumeName, fTranslation); } +void GateVoxelizedPromptGammaTLEActor::BeginOfRunAction(const G4Run *run) {} + void GateVoxelizedPromptGammaTLEActor::BeginOfEventAction( - const G4Event *event) {} -void GateVoxelizedPromptGammaTLEActor::PreUserTrackingAction( - const G4Track *track) {} + const G4Event *event) { + T0 = event->GetPrimaryVertex()->GetT0(); + incidentParticles++; +} void GateVoxelizedPromptGammaTLEActor::SteppingAction(G4Step *step) { + const G4ParticleDefinition *particle = + step->GetTrack()->GetParticleDefinition(); + if ((particle != G4Neutron::Neutron()) && (particle != G4Proton::Proton())) { + return; + } + auto position = step->GetPostStepPoint()->GetPosition(); + auto touchable = step->GetPreStepPoint()->GetTouchable(); // Get the voxel index - G4ThreeVector position; - bool isInside; - ImageType::IndexType index; - GetStepVoxelPosition(step, "random", cpp_image, position, isInside, - index); - // if IsInside ... + + auto localPosition = + touchable->GetHistory()->GetTransform(0).TransformPoint(position); + + // convert G4ThreeVector to itk PointType + Image3DType::PointType point; + point[0] = localPosition[0]; + point[1] = localPosition[1]; + point[2] = localPosition[2]; + + Image3DType::IndexType index; + G4bool isInside = volume->TransformPhysicalPointToIndex(point, index); + if (!isInside) { // verification + return; // Skip if not inside the volume + } + + // Convert the index to a 4D index + ImageType::IndexType ind; + ind[0] = index[0]; + ind[1] = index[1]; + ind[2] = index[2]; + + // Get the step lenght + const G4double &l = step->GetStepLength(); + G4Material *mat = step->GetPreStepPoint()->GetMaterial(); + G4double rho = mat->GetDensity() / (CLHEP::g / CLHEP::cm3); + auto w = step->GetTrack() + ->GetWeight(); // Get the weight of the track (particle history) + // for potential russian roulette or splitting + + // Get the energy of the projectile + const G4double &preE = step->GetPreStepPoint()->GetKineticEnergy(); + const G4double &postE = step->GetPostStepPoint()->GetKineticEnergy(); + G4double projectileEnergy = preE; + if (postE != 0) { + const G4double &randomenergy = G4UniformRand(); + projectileEnergy = postE + randomenergy * (preE - postE); // MeV + } + // Get the energy bin index + G4int binE = static_cast( + projectileEnergy / (energyrange / energybins)); // Always the left bin + if (binE >= energybins) { + binE = energybins; // last bin = overflow + } + + if (binE < 0) { + binE = 0; // underflow + } + + if ((fProtonTimeFlag) || + (fNeutronTimeFlag)) { // If the quantity of interest is the time of flight + // Get the time of flight + const G4double &randomtime = G4UniformRand(); + const G4double &pretime = + step->GetPreStepPoint()->GetGlobalTime() - T0; // ns + const G4double &posttime = + step->GetPostStepPoint()->GetGlobalTime() - T0; // ns + G4double time = (posttime + randomtime * (pretime - posttime)); // ns + // Get the voxel index (fourth dim) corresponding to the time of flight + G4int bin = static_cast(time / (timerange / timebins)); + if (bin >= timebins) { + bin = timebins; // overflow + } + if (bin < 0) { + bin = 0; // underflow + } + ind[3] = bin; + G4double pg_sum = 1; + // Store the value in the volume for neutrons OR protons -> LEFT BINNING + if (fProtonTimeFlag && particle == G4Proton::Proton()) { + if (weight) { + pg_sum = fProtonVector[binE]; + } + ImageAddValue(cpp_tof_proton_image, ind, pg_sum * l * rho * w); + } + if (fNeutronTimeFlag && particle == G4Neutron::Neutron()) { + if (weight) { + pg_sum = fProtonVector[binE]; + } + ImageAddValue(cpp_tof_neutron_image, ind, + pg_sum * l * rho * w); + } + } + if (fProtonEnergyFlag || + fNeutronEnergyFlag) { // when the quantity of interest is the energy + ind[3] = binE; + // Store the value in the volume for neutrons OR protons -> LEFT BINNING + if (fProtonEnergyFlag && particle == G4Proton::Proton()) { + ImageAddValue(cpp_E_proton_image, ind, l * rho * w); + } + if (fNeutronEnergyFlag && particle == G4Neutron::Neutron()) { + ImageAddValue(cpp_E_neutron_image, ind, l * rho * w); + } + } +} + +void GateVoxelizedPromptGammaTLEActor::EndOfRunAction(const G4Run *run) { + std::cout << "incident particles : " << incidentParticles << std::endl; + if (incidentParticles == 0) { + std::cerr << "Error: incidentParticles is zero. Skipping scaling." + << std::endl; + return; + } + // scaling all the 4D voxels with th enumber of incident protons (= number of + // event) + if (fProtonTimeFlag) { + itk::ImageRegionIterator it( + cpp_tof_proton_image, cpp_tof_proton_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } + if (fProtonEnergyFlag) { + itk::ImageRegionIterator it( + cpp_E_proton_image, cpp_E_proton_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } + if (fNeutronEnergyFlag) { + itk::ImageRegionIterator it( + cpp_E_neutron_image, cpp_E_neutron_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } + if (fNeutronTimeFlag) { + itk::ImageRegionIterator it( + cpp_tof_neutron_image, + cpp_tof_neutron_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } } + +int GateVoxelizedPromptGammaTLEActor::EndOfRunActionMasterThread(int run_id) { + return 0; +} + +void GateVoxelizedPromptGammaTLEActor::SetVector(py::array_t vect_p, + py::array_t vect_n) { + fProtonVector = + std::vector(vect_p.data(), vect_p.data() + vect_p.size()); + fNeutronVector = + std::vector(vect_n.data(), vect_n.data() + vect_n.size()); +} \ No newline at end of file diff --git a/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.h b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.h index 4998b7ce0..202e5b3c5 100644 --- a/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.h +++ b/core/opengate_core/opengate_lib/GateVoxelizedPromptGammaTLEActor.h @@ -8,14 +8,14 @@ #ifndef GateVoxelizedPromptGammaTLEActor_h #define GateVoxelizedPromptGammaTLEActor_h -#include "GateDoseActor.h" -#include "GateMaterialMuHandler.h" - #include "G4Cache.hh" #include "G4EmCalculator.hh" #include "G4NistManager.hh" #include "G4VPrimitiveScorer.hh" - +#include "GateDoseActor.h" +#include "GateMaterialMuHandler.h" +#include +#include #include namespace py = pybind11; @@ -23,25 +23,85 @@ namespace py = pybind11; class GateVoxelizedPromptGammaTLEActor : public GateVActor { public: - // Constructor + // destructor + ~GateVoxelizedPromptGammaTLEActor() override; + explicit GateVoxelizedPromptGammaTLEActor(py::dict &user_info); void InitializeUserInfo(py::dict &user_info) override; void InitializeCpp() override; - void BeginOfRunActionMasterThread(int run_id) override; + void BeginOfRunActionMasterThread(int run_id); - void BeginOfEventAction(const G4Event *event) override; + int EndOfRunActionMasterThread(int run_id) override; - void PreUserTrackingAction(const G4Track *track) override; + void EndOfRunAction(const G4Run *run); + + void BeginOfRunAction(const G4Run *run); + + void BeginOfEventAction(const G4Event *event) override; - // Main function called every step in attached volume void SteppingAction(G4Step *) override; + inline bool GetProtonTimeFlag() const { return fProtonTimeFlag; } + + inline void SetProtonTimeFlag(const bool b) { fProtonTimeFlag = b; } + + inline bool GetNeutronTimeFlag() const { return fNeutronTimeFlag; } + + inline void SetNeutronTimeFlag(const bool b) { fNeutronTimeFlag = b; } + + inline bool GetProtonEnergyFlag() const { return fProtonEnergyFlag; } + + inline void SetProtonEnergyFlag(const bool b) { fProtonEnergyFlag = b; } + + inline bool GetNeutronEnergyFlag() const { return fNeutronEnergyFlag; } + + inline void SetNeutronEnergyFlag(const bool b) { fNeutronEnergyFlag = b; } + + void SetVector(py::array_t vect_p, py::array_t vect_n); + + inline std::string GetPhysicalVolumeName() const { + return fPhysicalVolumeName; + } + + // void SetVector(pybind11::array_t vect_p, pybind11::array_t + // vect_n ); + + inline void SetPhysicalVolumeName(std::string s) { fPhysicalVolumeName = s; } + + std::string fPhysicalVolumeName; + // Image type typedef itk::Image ImageType; - ImageType::Pointer cpp_image; + ImageType::Pointer cpp_tof_neutron_image; + ImageType::Pointer cpp_tof_proton_image; + ImageType::Pointer cpp_E_proton_image; + ImageType::Pointer cpp_E_neutron_image; + + typedef itk::Image Image3DType; + Image3DType::Pointer volume; + + G4double T0; + G4int incidentParticles; + + G4int timebins; + G4double timerange; + G4int energybins; + G4double energyrange; + + G4bool fProtonTimeFlag{}; + G4bool fProtonEnergyFlag{}; + G4bool fNeutronTimeFlag{}; + G4bool fNeutronEnergyFlag{}; + G4bool weight; + + G4ThreeVector fsize; + G4ThreeVector fspacing; + G4ThreeVector fTranslation; + std::vector fProtonVector; + std::vector fNeutronVector; }; #endif // GateVoxelizedPromptGammaTLEActor_h diff --git a/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaAnalogActor.cpp b/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaAnalogActor.cpp new file mode 100644 index 000000000..bf197db05 --- /dev/null +++ b/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaAnalogActor.cpp @@ -0,0 +1,81 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + -------------------------------------------------- */ + +#include +#include + +namespace py = pybind11; + +#include "GateVoxelizedPromptGammaAnalogActor.h" + +class PyGateVoxelizedPromptGammaAnalogActor + : public GateVoxelizedPromptGammaAnalogActor { +public: + // Inherit the constructors + using GateVoxelizedPromptGammaAnalogActor:: + GateVoxelizedPromptGammaAnalogActor; + + void BeginOfRunActionMasterThread(int run_id) override { + PYBIND11_OVERLOAD(void, GateVoxelizedPromptGammaAnalogActor, + BeginOfRunActionMasterThread, run_id); + } + + int EndOfRunActionMasterThread(int run_id) override { + PYBIND11_OVERLOAD(int, GateVoxelizedPromptGammaAnalogActor, + EndOfRunActionMasterThread, run_id); + } +}; + +void init_GateVoxelizedPromptGammaAnalogActor(py::module &m) { + py::class_, + GateVActor>(m, "GateVoxelizedPromptGammaAnalogActor") + .def(py::init()) + .def("InitializeUserInfo", + &GateVoxelizedPromptGammaAnalogActor::InitializeUserInfo) + .def("BeginOfRunActionMasterThread", + &GateVoxelizedPromptGammaAnalogActor::BeginOfRunActionMasterThread) + .def("EndOfRunActionMasterThread", + &GateVoxelizedPromptGammaAnalogActor::EndOfRunActionMasterThread) + .def("SetPhysicalVolumeName", + &GateVoxelizedPromptGammaAnalogActor::SetPhysicalVolumeName) + .def("GetPhysicalVolumeName", + &GateVoxelizedPromptGammaAnalogActor::GetPhysicalVolumeName) + + .def("SetProtonTimeFlag", + &GateVoxelizedPromptGammaAnalogActor::SetProtonTimeFlag) + .def("GetProtonTimeFlag", + &GateVoxelizedPromptGammaAnalogActor::GetProtonTimeFlag) + + .def("SetProtonEnergyFlag", + &GateVoxelizedPromptGammaAnalogActor::SetProtonEnergyFlag) + .def("GetProtonEnergyFlag", + &GateVoxelizedPromptGammaAnalogActor::GetProtonEnergyFlag) + + .def("SetNeutronEnergyFlag", + &GateVoxelizedPromptGammaAnalogActor::SetNeutronEnergyFlag) + .def("GetNeutronEnergyFlag", + &GateVoxelizedPromptGammaAnalogActor::GetNeutronEnergyFlag) + + .def("SetNeutronTimeFlag", + &GateVoxelizedPromptGammaAnalogActor::SetNeutronTimeFlag) + .def("GetNeutronTimeFlag", + &GateVoxelizedPromptGammaAnalogActor::GetNeutronTimeFlag) + + .def_readwrite("fPhysicalVolumeName", + &GateVoxelizedPromptGammaAnalogActor::fPhysicalVolumeName) + .def_readwrite( + "cpp_tof_neutron_image", + &GateVoxelizedPromptGammaAnalogActor::cpp_tof_neutron_image) + .def_readwrite("cpp_tof_proton_image", + &GateVoxelizedPromptGammaAnalogActor::cpp_tof_proton_image) + .def_readwrite("cpp_E_neutron_image", + &GateVoxelizedPromptGammaAnalogActor::cpp_E_neutron_image) + .def_readwrite("cpp_E_proton_image", + &GateVoxelizedPromptGammaAnalogActor::cpp_E_proton_image); +} diff --git a/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaTLEActor.cpp b/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaTLEActor.cpp index 356893d0c..64458959f 100644 --- a/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaTLEActor.cpp +++ b/core/opengate_core/opengate_lib/pyGateVoxelizedPromptGammaTLEActor.cpp @@ -41,5 +41,39 @@ void init_GateVoxelizedPromptGammaTLEActor(py::module &m) { &GateVoxelizedPromptGammaTLEActor::BeginOfRunActionMasterThread) .def("EndOfRunActionMasterThread", &GateVoxelizedPromptGammaTLEActor::EndOfRunActionMasterThread) - .def_readwrite("cpp_image", &GateVoxelizedPromptGammaTLEActor::cpp_image); + .def("SetPhysicalVolumeName", + &GateVoxelizedPromptGammaTLEActor::SetPhysicalVolumeName) + .def("GetPhysicalVolumeName", + &GateVoxelizedPromptGammaTLEActor::GetPhysicalVolumeName) + .def("SetVector", &GateVoxelizedPromptGammaTLEActor::SetVector) + .def("SetProtonTimeFlag", + &GateVoxelizedPromptGammaTLEActor::SetProtonTimeFlag) + .def("GetProtonTimeFlag", + &GateVoxelizedPromptGammaTLEActor::GetProtonTimeFlag) + + .def("SetProtonEnergyFlag", + &GateVoxelizedPromptGammaTLEActor::SetProtonEnergyFlag) + .def("GetProtonEnergyFlag", + &GateVoxelizedPromptGammaTLEActor::GetProtonEnergyFlag) + + .def("SetNeutronEnergyFlag", + &GateVoxelizedPromptGammaTLEActor::SetNeutronEnergyFlag) + .def("GetNeutronEnergyFlag", + &GateVoxelizedPromptGammaTLEActor::GetNeutronEnergyFlag) + + .def("SetNeutronTimeFlag", + &GateVoxelizedPromptGammaTLEActor::SetNeutronTimeFlag) + .def("GetNeutronTimeFlag", + &GateVoxelizedPromptGammaTLEActor::GetNeutronTimeFlag) + + .def_readwrite("fPhysicalVolumeName", + &GateVoxelizedPromptGammaTLEActor::fPhysicalVolumeName) + .def_readwrite("cpp_tof_neutron_image", + &GateVoxelizedPromptGammaTLEActor::cpp_tof_neutron_image) + .def_readwrite("cpp_tof_proton_image", + &GateVoxelizedPromptGammaTLEActor::cpp_tof_proton_image) + .def_readwrite("cpp_E_neutron_image", + &GateVoxelizedPromptGammaTLEActor::cpp_E_neutron_image) + .def_readwrite("cpp_E_proton_image", + &GateVoxelizedPromptGammaTLEActor::cpp_E_proton_image); } diff --git a/opengate/actors/tleactors.py b/opengate/actors/tleactors.py index b33b772f6..ba4423cc1 100644 --- a/opengate/actors/tleactors.py +++ b/opengate/actors/tleactors.py @@ -4,9 +4,9 @@ from ..base import process_cls from .actoroutput import ( ActorOutputSingleImageOfHistogram, - ActorOutputImage, - ActorOutputSingleImage, + UserInterfaceToActorOutputImage, ) + from .doseactors import DoseActor, VoxelDepositActor @@ -76,69 +76,190 @@ class VoxelizedPromptGammaTLEActor( """ user_info_defaults = { - "stage_0_database": ( + "timebins": ( + 200, + { + "doc": "Number of bins in the histogram for time", + }, + ), + "timerange": ( + 5 * g4_units.ns, + { + "doc": "Range of the histogram in ns", + }, + ), + "energybins": ( + 250, + { + "doc": "Number of bins in the histogram for energy", + }, + ), + "energyrange": ( + 200 * g4_units.MeV, + { + "doc": "Range of the histogram in MeV", + }, + ), + "weight": ( + True, + { + "doc": "if the ToF spectra is weighted or not", + }, + ), + "vect_p": ( None, { - "doc": "TODO", + "doc": "Vector of weights for proton ToF deposition.", + }, + ), + "vect_n": ( + None, + { + "doc": "Vector of weights for neutron ToF deposition.", }, ), - "bins": (100, {"doc": "TODO"}), } user_output_config = { - "vpg": { + "p_E": { "actor_output_class": ActorOutputSingleImageOfHistogram, - "active": True, + "interfaces": { + "prot_E": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 0, + "active": True, + }, + }, + }, + "p_tof": { + "actor_output_class": ActorOutputSingleImageOfHistogram, + "interfaces": { + "prot_tof": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 0, + "active": False, + }, + }, + }, + "n_E": { + "actor_output_class": ActorOutputSingleImageOfHistogram, + "interfaces": { + "neutr_E": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 0, + "active": False, + }, + }, + }, + "n_tof": { + "actor_output_class": ActorOutputSingleImageOfHistogram, + "interfaces": { + "neutr_tof": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 0, + "active": False, + }, + }, }, } - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs): + VoxelDepositActor.__init__(self, *args, **kwargs) self.__initcpp__() def __initcpp__(self): + g4.GateVoxelizedPromptGammaTLEActor.__init__(self, self.user_info) self.AddActions( { "BeginOfRunActionMasterThread", "BeginOfEventAction", + "BeginOfRunAction", "SteppingAction", - "PreUserTrackingAction", + "EndOfRunAction", "EndOfRunActionMasterThread", } ) def initialize(self, *args): + self.check_user_input() VoxelDepositActor.initialize(self) - # C++ side + if not (self.user_output.p_E.get_active(item=0)): + self.user_output.p_E.set_write_to_disk(False, item=0) + if not (self.user_output.p_tof.get_active(item=0)): + self.user_output.p_tof.set_write_to_disk(False, item=0) + if not (self.user_output.n_E.get_active(item=0)): + self.user_output.n_E.set_write_to_disk(False, item=0) + if not (self.user_output.n_tof.get_active(item=0)): + self.user_output.n_tof.set_write_to_disk(False, item=0) + self.InitializeUserInfo(self.user_info) + + self.SetProtonEnergyFlag(self.user_output.p_E.get_active(item=0)) + self.SetProtonTimeFlag(self.user_output.p_tof.get_active(item=0)) + self.SetNeutronEnergyFlag(self.user_output.n_E.get_active(item=0)) + self.SetNeutronTimeFlag(self.user_output.n_tof.get_active(item=0)) + + self.SetVector(self.user_info.get("vect_p"), self.user_info.get("vect_n")) + + self.SetPhysicalVolumeName(self.user_info.get("attached_to")) self.InitializeCpp() def prepare_output_for_run(self, output_name, run_index, **kwargs): # need to override because create image is different for img of histo self._assert_output_exists(output_name) - self.user_output[output_name].create_image_of_histograms( - run_index, - self.size, - self.spacing, - self.bins, - origin=self.translation, - **kwargs, - ) + if (output_name == "p_E") or (output_name == "n_E"): + self.user_output[output_name].create_image_of_histograms( + run_index, + self.size, + self.spacing, + self.energybins + 1, + origin=self.translation, + **kwargs, + ) + if (output_name == "p_tof") or (output_name == "n_tof"): + self.user_output[output_name].create_image_of_histograms( + run_index, + self.size, + self.spacing, + self.timebins + 1, + origin=self.translation, + **kwargs, + ) def BeginOfRunActionMasterThread(self, run_index): - self.prepare_output_for_run("vpg", run_index) - self.push_to_cpp_image("vpg", run_index, self.cpp_image) + if self.user_output.p_E.get_active(item=0): + self.prepare_output_for_run("p_E", run_index) + self.push_to_cpp_image("p_E", run_index, self.cpp_E_proton_image) + if self.user_output.p_tof.get_active(item=0): + self.prepare_output_for_run("p_tof", run_index) + self.push_to_cpp_image("p_tof", run_index, self.cpp_tof_proton_image) + if self.user_output.n_E.get_active(item=0): + self.prepare_output_for_run("n_E", run_index) + self.push_to_cpp_image("n_E", run_index, self.cpp_E_neutron_image) + if self.user_output.n_tof.get_active(item=0): + self.prepare_output_for_run("n_tof", run_index) + self.push_to_cpp_image("n_tof", run_index, self.cpp_tof_neutron_image) g4.GateVoxelizedPromptGammaTLEActor.BeginOfRunActionMasterThread( self, run_index ) def EndOfRunActionMasterThread(self, run_index): - print("end of run action master thread", run_index) - self.fetch_from_cpp_image("vpg", run_index, self.cpp_image) - self._update_output_coordinate_system("vpg", run_index) + if self.user_output.p_E.get_active(item=0): + self.fetch_from_cpp_image("p_E", run_index, self.cpp_E_proton_image) + self._update_output_coordinate_system("p_E", run_index) + if self.user_output.p_tof.get_active(item=0): + self.fetch_from_cpp_image("p_tof", run_index, self.cpp_tof_proton_image) + self._update_output_coordinate_system("p_tof", run_index) + if self.user_output.n_E.get_active(item=0): + self.fetch_from_cpp_image("n_E", run_index, self.cpp_E_neutron_image) + self._update_output_coordinate_system("n_E", run_index) + if self.user_output.n_tof.get_active(item=0): + self.fetch_from_cpp_image("n_tof", run_index, self.cpp_tof_neutron_image) + self._update_output_coordinate_system("n_tof", run_index) VoxelDepositActor.EndOfRunActionMasterThread(self, run_index) return 0 @@ -147,5 +268,186 @@ def EndSimulationAction(self): VoxelDepositActor.EndSimulationAction(self) +class VoxelizedPromptGammaAnalogActor( + VoxelDepositActor, g4.GateVoxelizedPromptGammaAnalogActor +): + """ + FIXME doc todo + """ + + user_info_defaults = { + "timebins": ( + 200, + { + "doc": "Number of bins in the histogram for time", + }, + ), + "timerange": ( + 5 * g4_units.ns, + { + "doc": "Range of the histogram in ns", + }, + ), + "energybins": ( + 250, + { + "doc": "Number of bins in the histogram for energy", + }, + ), + "energyrange": ( + 200 * g4_units.MeV, + { + "doc": "Range of the histogram in MeV", + }, + ), + } + + user_output_config = { + "p_E": { + "actor_output_class": ActorOutputSingleImageOfHistogram, + "interfaces": { + "prot_E": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 0, + "active": True, + }, + }, + }, + "p_tof": { + "actor_output_class": ActorOutputSingleImageOfHistogram, + "interfaces": { + "prot_tof": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 0, + "active": False, + }, + }, + }, + "n_E": { + "actor_output_class": ActorOutputSingleImageOfHistogram, + "interfaces": { + "neutr_E": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 0, + "active": False, + }, + }, + }, + "n_tof": { + "actor_output_class": ActorOutputSingleImageOfHistogram, + "interfaces": { + "neutr_tof": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 0, + "active": False, + }, + }, + }, + } + + def __init__(self, *args, **kwargs): + + VoxelDepositActor.__init__(self, *args, **kwargs) + self.__initcpp__() + + def __initcpp__(self): + + g4.GateVoxelizedPromptGammaAnalogActor.__init__(self, self.user_info) + self.AddActions( + { + "BeginOfRunActionMasterThread", + "BeginOfEventAction", + "BeginOfRunAction", + "SteppingAction", + "EndOfRunAction", + "EndOfRunActionMasterThread", + } + ) + + def initialize(self, *args): + + self.check_user_input() + VoxelDepositActor.initialize(self) + if not (self.user_output.p_E.get_active(item=0)): + self.user_output.p_E.set_write_to_disk(False, item=0) + if not (self.user_output.p_tof.get_active(item=0)): + self.user_output.p_tof.set_write_to_disk(False, item=0) + if not (self.user_output.n_E.get_active(item=0)): + self.user_output.n_E.set_write_to_disk(False, item=0) + if not (self.user_output.n_tof.get_active(item=0)): + self.user_output.n_tof.set_write_to_disk(False, item=0) + + self.InitializeUserInfo(self.user_info) + + self.SetProtonEnergyFlag(self.user_output.p_E.get_active(item=0)) + self.SetProtonTimeFlag(self.user_output.p_tof.get_active(item=0)) + self.SetNeutronEnergyFlag(self.user_output.n_E.get_active(item=0)) + self.SetNeutronTimeFlag(self.user_output.n_tof.get_active(item=0)) + + self.SetPhysicalVolumeName(self.user_info.get("attached_to")) + self.InitializeCpp() + + def prepare_output_for_run(self, output_name, run_index, **kwargs): + # need to override because create image is different for img of histo + self._assert_output_exists(output_name) + if (output_name == "p_E") or (output_name == "n_E"): + self.user_output[output_name].create_image_of_histograms( + run_index, + self.size, + self.spacing, + self.energybins + 1, + origin=self.translation, + **kwargs, + ) + if (output_name == "p_tof") or (output_name == "n_tof"): + self.user_output[output_name].create_image_of_histograms( + run_index, + self.size, + self.spacing, + self.timebins + 1, + origin=self.translation, + **kwargs, + ) + + def BeginOfRunActionMasterThread(self, run_index): + + if self.user_output.p_E.get_active(item=0): + self.prepare_output_for_run("p_E", run_index) + self.push_to_cpp_image("p_E", run_index, self.cpp_E_proton_image) + if self.user_output.p_tof.get_active(item=0): + self.prepare_output_for_run("p_tof", run_index) + self.push_to_cpp_image("p_tof", run_index, self.cpp_tof_proton_image) + if self.user_output.n_E.get_active(item=0): + self.prepare_output_for_run("n_E", run_index) + self.push_to_cpp_image("n_E", run_index, self.cpp_E_neutron_image) + if self.user_output.n_tof.get_active(item=0): + self.prepare_output_for_run("n_tof", run_index) + self.push_to_cpp_image("n_tof", run_index, self.cpp_tof_neutron_image) + g4.GateVoxelizedPromptGammaAnalogActor.BeginOfRunActionMasterThread( + self, run_index + ) + + def EndOfRunActionMasterThread(self, run_index): + if self.user_output.p_E.get_active(item=0): + self.fetch_from_cpp_image("p_E", run_index, self.cpp_E_proton_image) + self._update_output_coordinate_system("p_E", run_index) + if self.user_output.p_tof.get_active(item=0): + self.fetch_from_cpp_image("p_tof", run_index, self.cpp_tof_proton_image) + self._update_output_coordinate_system("p_tof", run_index) + if self.user_output.n_E.get_active(item=0): + self.fetch_from_cpp_image("n_E", run_index, self.cpp_E_neutron_image) + self._update_output_coordinate_system("n_E", run_index) + if self.user_output.n_tof.get_active(item=0): + self.fetch_from_cpp_image("n_tof", run_index, self.cpp_tof_neutron_image) + self._update_output_coordinate_system("n_tof", run_index) + VoxelDepositActor.EndOfRunActionMasterThread(self, run_index) + return 0 + + def EndSimulationAction(self): + g4.GateVoxelizedPromptGammaAnalogActor.EndSimulationAction(self) + VoxelDepositActor.EndSimulationAction(self) + + process_cls(TLEDoseActor) process_cls(VoxelizedPromptGammaTLEActor) +process_cls(VoxelizedPromptGammaAnalogActor) diff --git a/opengate/contrib/vpgTLE/howto_vpg_TLE.pdf b/opengate/contrib/vpgTLE/howto_vpg_TLE.pdf new file mode 100644 index 0000000000000000000000000000000000000000..33a582290ce6931261641ebe060d68393322c966 GIT binary patch literal 210759 zcmeFYW0WObvo2b;?OJ8q?oyX++qS!GcDc*8ZFkwWZ5y}yd-u2RKIiOl$N6>7pR@kV z5s^7(W;~JWiI^FYWbz{4>6qx*V92_k@;+ghSpkdyJ3~ttUS0r$w1usSfun_|i7|lb zj|5<5V&q`s0x*aJv;a(Oj9dU_7B+wmfI$wx%n4u+1#q!*aWMlJWC1Jy2IaqJv#_!Q z`1xT>Y>ode3Hbl<2gA(vUkp+9ur~oPs4JS7{xQSE*38))@W&JY!*>g7XA{RiqqTvv ziHM1jo$;U3{u$*B9BrHce^C&&vv#qubpo&f7~~yIj4h0u?HmD2%zq36FsS`S;*YsZ ze_@cYH8yeoqxgd%6Z3y)Ozi;7jDPM#5x}5mXXgxHX8%V}`hzNfiS?f~UH`#~Z72XESPRgGV{ZZCm9#ZD z-x*sWDy-O2Cn7{7nHiZrv$-(OAs_}791}@n!GPw(5QF|Y{y(nzf8Y`LpV9ok^8ZEP z{}=)=|N7(qn^@SL!Lvw^jp*?(bP=D)}PeewU6^ppS`?0<=ziR}+} z|C=INx&BAq(AU>D(%1JH?dj`@%kBpZ1A*(a(;zW3*6`|^!~CI>2orq`MkyBKg_Hev zSFgaV16`^9ya&Mwh!R$$Yi33hJMdZJZ?xd-=BS_opn=Xqfdv84RR{^VR_Q_&YpPuM zYcG!Foc;TOjnV=d$quhjT9yQx_v^k_*hQAfcI$FEw$o<%(pm@w*w|P>A3$i;RH6SX z!~BUv|F5ZnwzhW8PJbww{jXLLz|Qrrf#V-W=3x3q`Uhc-f4sr*HvlUBrE_6BTW1s7 zKhv52nZ+RUhx`qM?EX^x-v!vX*yx%6pvK9{M9<0fcgCNX@n^z+dPC8~$m=#PIm z{syBzA>beX{Po#?bVaD|wq#hT~^Q#%|9}vMzs;Rl6!twV3PjZRiP@eK&Mb4__EF+eN+LSTob> z62F&BlON&Kw0WW-%a@OEHi?3fDNijyL^_qr1GiKZ>&&`whMBeWCZG{2q<{JWX@MMG`4!wPC?Z_OST&9f=?7i{ zF}wlHv|x?#z=4NSbt9ao7D;2Lp-Pyp@$piOW-o%ZM;mr>nb0v|fWwe+DU} zFMl4R*f6i=9-SW$Zj7C&Ty|&}rHl~^$G}v|ee{fD^eqS!*Te$}Gk9WaBt3KFgN{u; zKEfYL^~t@qCGIDIVZVnOBLX4v_wC-7%ihiC_IfVE_Se~qo#Z}*e{dhnZ&Z=F*NAHE zgLB_aO;~9WzLL<3^~x-z6nEHZ#sMtERusnIGL}2QMGdH`VZYC7R+sI~t?jl5Rx0q6 zEQd#lx3|4p>EAsKWhnh#e4#Te1!Sxb5U`ECi07Y7mH~g( zU((SKhM}xPhG%aC)l$Y-=m}puJQpiOPtPijo#bOYW)i6tM#9~E6{KP@lI^mjyQza3 z2#&4vntG_t-B)h|Q9Zcmm1WAZr5kgQ4QUDr&Y^T0wk4XMEKvj#ZJ-_OImIQE7;+2< z1ueATobGw@fzDOo{Nf{^;TJ7N=$PQSst*w!S%Kw|GbW^uT@j-nq=kG;jA+Tqp2~4p z1M5o8T-`PgFdz=>&+qe0miW z4IZi`CaQR67kkXvmQSzfG6sXQxoiAo`5baLQ??$t9Cxu>E@=Pp7{(PQeBUW+2>&(F zFqV8;czILPvNg306)eOH(~1H|`(@H5d+>&`zO=I1!~Ud==FN!Z9!_5ohiMI8&sSP7 zd-r%IE>IHk=G3N^MoS*jvJeeqymm%GM2vzytEYlL1s17?;B1iD-nx=vV$4(eHITxc zxOU#f(C3D-(x{M-)X?1rKUI{aq-uw`TY;$qi4#G9k}9hMT8M*f5C8d1T9bM89PN2gdk5BCYBd`ZIqyDgqbTE4a-)qG8-*q~ z6>jd3Qwi#w<6S4%@4Ml%$^OWRuY}ZEXgF~7u;@i_9d`f|ZmHDc90<-zd~RQ&d@>F! zPaJs^x2TU~iJVKByrJ?|3S#PPMhE`9gDW0hjSUg-p$fSd${@^}@yFjx)l3vVzr9#3 z*fW4q*%Zsa$gGG9L0VJUn9kcfnvXd6S3|>5s62Go9iZhNjJmAEW|K9S$0+#D_c}`3 zo%-RzpkUNk@M2hpOFedp{3^^_ab!_nK0aD$x4l~^=IZ%b=C0nE#5hQw%ESZs@S zN zl2yJIJW&HXud#?DY(tio2yhfmJnS9r7vOD$yV`bsAJ5SK{*qrgz+9r3o#$04uan2B zPmA-%ZkSc(YG_x|JmvgdQ)rrprFemTS4h0a*TB0X`Yvxu9zM;m4GDe|I&`!b>KCS+ zNrheAe%qRrO;rvvI#S@=GLm*BcilA^U!L}!#|u~K`#Utkw_*EWZ(2|%Gvnx;aHq}l zSOZ@kWj)Z>P#L4hqV3*n-}LQV&nll&>wnE6SpEk1|BaZ;^d~`M{JS^)*QSygz|PF^ zXMg;U^f#aQcPhof{Aa88U-w|q&XCGV7qj#MVL8s|al0YYRv=&}ollzu!AJlk6hN+2 zwW3HOIiM4o8VZV9{7TdlSr8Z)Skx2p#faJPcI7CJcZ@D?Qk25H*mC1V zMAdKao`C=XZ|<236b1!^oDj603>FA%z^J~f_bsZJGzaePJ2wz_fFC(TbWgkkrNR0K z?@!rD&UdCSPwp{YX%Pf$o(X!5z9de*lHJK7$A4)?j(_kK=}j^q$}NZDsZY zsepq5g%Dm}xvRHO%;JZl4-hMKe@A;&f$$xpHismOO2-9>?vMXCmBT&>_uH%9kUO3- z)tSK}bnemXM-F$je`gB{OlgmU#yQyuo|gZF?QI|a+;bAz2BK7dZwhOH^1B8?>asV> z)jNp(SV!&-jMKAy2lEHM#B%FF762ngItlCf`U$RYun7fpbP@~&_q^rrt7(h`we1(Q z2jmrmiX!uved^B9cM}_}*F`al*#bJ~gb3p2`|bwRp~}xLC`cbtgv<9(7TpX%g9iAU>WEA zU=Z}^JBsKt_UffQ$0RlPx@qntAJf|E+djDyCfcE+VHfYh`4k#+nkHq0t3+k&^fGM` z1feV0>HW5w!7SS@>YD;||Ib z1&#=^_}}po-CE05iwIuqb!k*OE3sE{)|Rz3(h7Y7CHn5)0*Ef7eVT7joKv!B%zEjV z-laMgI$1a1*y&CNN$NuR(UqNaIyYG6FUIK{a?>^qq|(n7I4#jjZOKg(;$h}~c}L7; zpj_EuvjRU_Hb`z>kU2+K!zkQc`p%>V*`pH60dLa(B zibJ23%rb~^frh&c;48Ppnte89ZgUH|+y=eTz9K%Ayp!j1!#J+GtDn8t&fy2o=S;cB zw3h-Ftk)g@v(e>&uSUf4KG=@NA(dI>XwSFj>10xz9l+*9N@3zeo|9dBtT(zbpzcf* zr|LN_+-Z}p+|Hbf-%ip;6A(pS=DPj9Bq+};a>{NE=(Hb-aNqg}Lafotjst=ccFEjdboHI~-(cyXWQRmW*I zF0)X#>D=rWB1&~DsU}|SUD%{lu>mGM@Y+mN5FhJEggLNBzDzy=?kU}QN<%Bef7Ruk|XpB6toxTCLG-MJkGD73D*qF$XsNo z&D!{N0CkK6rAhgo(t3q>f=y1F!o`|XT&xxUc(d5sX#_X4SnlVbc#Z$XM)7#N5@ZVi zsS<8u;9-dgWOJBWL5lQ}SIx2^<+C}Jkr}CwK_+x9lN%orMO!{jJUD2qM#FQ16?Ux; z)#D>z*yE&_n$I^Zf4($2RRUl1e!mM}Fj+g1ObYQBTqTTIL(WWmoi8ZQ(FM&AbIMGi z#Ty^mgq`}SO^6Bp5Q%lm1*vo04gm3a*|io_R;SsuuyLR0TAN+1_|*+C>Y76-mkQ|n zo$lc#2HANfRX|k2;CcQu;rd#-d1Lty(|KD|987zj^Ipl<5t3u&8$p5gubvk6e9D`y^2o`P z?Si}*`nNSiOe`7SbIu-Ub2Wb`a}|iD-w@0fXrOm^GNTZ+7a57WJ@k@u1^#3N$=DzGy~UGAZ->AgJ&oGmy*hL^do=u|T)ugFux%s`fp5Rv!5kdoow5 z3|5Zxj5N)6r_yaFHAO_K#xa8d8LE4Jv$4oajzchw#mt`oBbmHg(8MjfOTN&$FR*7h zfbX<(>{4Oc%Q!GNU^4Rq9GRXRZ*W|Vi0a8Rv?EU|=+0X~U+Ez8rD-}dq#oodS^A6H zqq1_)U2IzOLxXYT5{^*y!C%=I(4ssBDDI2svLN4zr;hyDs7GZknz#qp-#c=mF5*%t zyF+a}>Q`xFz@wULP#O63Y~s0AGuoBRDZdnT<(VjI?hO?>)PJ~} zr`p^3I|S)k49N{LhP2T+o*jf1wdue_X5bio^o7J_CX~`Z&(+@+PLTRUaKTw8Y$Xw_ zhM3H79ZY&s1-t#?V|7!#3iBzVYw7ASfO~>7>~q~x7@cN3FRqTPJUK{X%yx8VcnqLf zFx6s@VYCn!v9DSgbDqU)$Rs;E!ua#1D8cD+HtT9liVN3aWB{>F-Id|(;Cm~<0((|$ z>uXu;I!0~u8@r3$dlEe76!W?N3}DFm)VWnUZH!E>r1e*@qv|INwAJ{gmBr;W$v}`( zx_Vz{fZC)>Yhyx0BgHN05_XIqPuKp8PZZij<@~O&OKdS^MO2FcuSbbVYEZquS%O{T9n9{ zVx5m;D?iI8{|Y}mIXW%3kwR|hx&10B>HLo0(ERlKMh!l)p+0j|aHTh?v=@aK2$29G zsm!i=jBkHlo3$VIo_&@-Wa_0u`wjO_1iE?LEY+#us6S}nLMlKb+51_-+D4CgS2FUH zewJexv_~M!6g&^VquAQzF@(G&0*1crCzqR?pwHf?%KftP$W!4G7S{^L4W!NZ0nW6n zo7ksrmj-1w4wBnZP1DW0qqBAR8#{MB46#4IvowL%6Ll8mXC>VHVSCYD(G4aUNxu9d zePte{uv(gt{{J* zaRug6ODkr~Dq9hC0SRI;1Lg zRzTwS2h%h+xbV=HYE4M>IsP?v?|y@os%xJG*+!4(hI9k>__SuG2|@cS(T_hhZI$tSkyjQN#TKCB{j$Abo2CQKV+sH=DR89 zKqu#g16Fi4l zqMcc;w7famMg#`>Evhc))Qjj9MIFmXq4sPY3ZG92pJvJ4tnD%qmBF5wXv{kticih@Xlu7b5V@LSTB zc|YE?h(6+dM){Qbk>yNq7F#CaZ7Y0Z1N5@C*_vG7F+QWT$}_$zJFdgKD#TJ6-$@C1 z1Km*whE7NhBZ9rT#z?QjQpjm}iIp3RG>$U4!K1>(*aZ;A7^Cn+Dj{WL1$b|l$Cz84h&K{{A$ zCstX3+xn__C?BG9NJ{v2Gg`;t{IbGm>LjXffN6+?3XiSNv1W=s9|ZL-NHBhKB$g!x z0NUV^r|AQpF{;-o#2>@N5LbZ2jP+Y{Twy|i8&cX-48f7r=8aq<)QjRql7d7!k$0bG zmrJdi5<1I1cLJv0&Nh8ib0*Wz001&*eVbaO9zy89*i~$>84}~d+X7MsQt|kf)R&+^ zcAfyH#qy#r|2A6Qu_oR*KUk!D7mw&P!Z`^PkL0y4{==see6`{C-tJk==Q@|_oqfmv zPwInzBshO@lK@bY<4fyr1I;soQEi!-_2WilIg+FlP_-8*-Rtf=;9>-EUK&{y_P0{a z-x%nX&isJb42>?9~|b^)`WrzA&}dKKozd)w9bA;a4TAT?-vY}68RkAPl%;)H$4 zfAcPkv_-TPzjdL))v%5fS|`e9Z{OnF3SA@NTenaDQD@NLoh!o5`86nrX zOp>tAk|%$4JXF@?Hmb%x!Nj3@O( zm9Fzn8GXxAsiZL63pRzrUtiv-t~yXBZw`5-9VHZ@jqjHDmnw>B%lSZ51hmlw=;^i$ z!pOsKT#{e+O5nmqhaxCm^B{}iz!^jWYIz0Ox}B`w z7AwZ7^kuX>h}@J}74)#UcCFG9jtzbLh2CI<=68F3xTXl7M^L^gxDz;c|5ZZ7UFA^$Plj?WL!{gIO zd_u@oFs|=OigK&mH$ z##*VRkq`kc$*uu#vUr2sHuhH%mej@)^-^de{kbZhc5O?%zIc@T!4`K@H`T0tSfz<8 zA~yG>e)sF1G^b;ayiSZP(y&u3?IdvH^@(ERY$5&>vGGGQEbocHrMa`y^NcS~e7E>D z8lAkZ#(kbo@1JxmbaA!Tuk8sHS`;JQ?f7K%{s*TRS}E^GRMeD4W1i`Dp~8KqSP3iB zJ=T3gHjgD-E=6Jxmdm4a4rtBS-^&{jt8o96N96Z0C2o2;<;86U|6G%jH8V^=-% z$Tq@bu5O7`*!uHBZeIhjn|I`Br07ICgm+Jkw)G=Wk#4tev&9j!5T@pSv+>3!Tg9bY z*Vt{O30%12J(q=>Pc+`G+_;rob@LlN3E6v67fh#P*i4Ly==#kZo4zZx8&h!!bDmIX z+NV2(qu9_&&$hy3SoNg-s*%YR)!D-H>-xyY9wy1}gS#}74!bDKTR|3U9+F8yY!Zs* zIMXKi)y*L)juQh-(7{3z4!30r!5Od_JFxJjA3(X87U^oVVfq8Mdcv#5P=s*Yw&`?! zt9H}mRlD3eoF585ahwis;abM0W2UN)EE59lLR4=F?B3nQhRLjsR;Z09`!WO{Miad< zp(3eBB9tS|*=UKw)-AhuW46mIbT}vJ_NQc@klJJ8IvhbG(s*9DD!%q~tOUT#MlZa? zgqHM-nUqb=eJPYY_Q$M#N(ejFD2(zq&aX0Qt@CCxrs-w0XLa6@w9s0D#>4Sl6;0K} zck1%CU1>vWhQO6SQmC7(QSrKwoiN8cc~o}Aag&XhIjgkF&ZfzA4!3|xx>d4G-gw3* zS>zR{RL|Y<**^_N7Q=q48x%*UkE^JJ$X`E{Y>nniIlw(@?VYXSbs^_>g8RTr4M>aC zLuA=a*&1#}v0mYr`e0q_ByZ)W6h%!?;IThY0))*A$czfgnwl)kY5!zq=~mjJfxI;Q z(kOCr;7;Tt`Y3}D;AIzn7W`)R(oFAvzzqfZB`!ce7PNfuD~!^$lXw@#m>%qQW@Tdg z<$fpGTuIELLZQ9-G2xKN@`5wkJaFDNvp#BU5;=3qB0D;TyzmzbmYHa{ePl;ZqTKi2 z0bVc!d_*?%>p!HlocA_a8-GjD|7v(AEmrGOy6wvsl%}cWkj?qP}@XZsP;LMtkHq>e7d$4E~Pw? z7`h$xs=XULg{sxY)=f@B!Z*g{kO-?&bXbeOzy)u`T4HOh_vbE`Z?E_06@r#zA-N|H zaa|-FhbhdF8%YG^l>}HYg%sY-5#kUNutNd0n;JundsY?-(!iEIso99O z!so0a!ETG2^`o)C?}FkO2sZmV<4S-d`fBoxvMK4=rE{9FZ>e|k6cFWgw0R!$rO%y< z#)sRifQst;#525F%to()O0KvNH}_ayaCLG0Z?~L+`aIi^b_S2ig2@s+WHo zoBp?|mmGgO!~U~)$;JAoBkI2?UNSKEvZh!4+Ar>WMxn49RMsXtZn3AUxzn`!ysHqO9CADv1rbx!paaA3{4 z_*cC3^>ZLsReDk!94a>}!1CsA4IHQ@46tJjE#QU<9bmi+AX!&e1X3Ko#K`jM_RdOt ztZ#NB*K$ZXuppW%JS**>&N|=dbsv*mfeaWJ7#tW7wWBer?6z!<#5M)^-*b=LQo%H4YAtexDaWv^p7;=U=}^ z0v`xLCUqAtx4Q?d*L5<0sF_z$>MYAvfv~CAZK!~zS735zzna&v&xxKfZEP%d?y65x zfw#Xj0o%TGkzbl$6K!wrYlfn`+d%3oS^>Zqi%pM{;?`fYGQBH<;5~0wHA%&1zKXuk z9`6T`IbY>Opgp6jhYYKzV2a?}8lcC8+8Nq>y^EJX=$JHCZf1T3u){EbJVWevgcYyh)i-sm(zU=ZNn22`kEGM=maK>Fr@QL! zUsa!LFxPI?s`Qh9tuKpbw^ZNN+EN-ROUg2qk0-cKi7`&CHuV*rHIRlLUv%&`tq+cC zIZb@?uPfYF#H6ovG`~GxA9TR-#wMixz~pN+iNV*>Tdc`#H>HFGCZPa^EW!^USWHL; zzrd7GR6tKJ>b38&re|Bb7Ej>U&yOqGubVF}3M(r@E2)A7GEAvl#?bM_u+wk9W`E(R z7yY7g!Y?927(N)uvLcaw1LmQr50uK|vB@L0Lz_LB!XW0sT!V3=OY9M`O3c3r=Zv6C zAj>;)EW=srd7aq8`OOfmP^8A#79^Wo#<(=oynWudfaEd=eYqY`??GYkcEVQ~aNd}z-IOW1UJA{40o2oVSPdgOkVxunKfA+)WY zV_t~o$6O#EOX8@UnAMOyE|*@7*A6@ea^EtB`HJp1%lW<19yDnvZz%*1N*P1F!=U2fb?v1}Z?xeVwvK@md@ZuMI`eI@)j}{~ zBiR744`hn^QMgZ}s~)Fq(xXF3+m9lsUzA2Lm(nrzAiJJZpld;q@AZWhoAb^elEh#| zv(~<*pMxdawS()k7e$ENwT12V9qzy+)bwS5k@XFEg;t|A4byjegz0H9*FJODm>^nf zB2MC>(f0vR~GDYo(Lb<07hFs}mQ9%}KxlblolON0gI zJMEf6LfsiOW|_I+pLWF`3s5g4zTBz!n7%8!jkI-9N~aebi^F0oU2t-|=NWdYnmO7= z^q5zyT3LI8tC0wKO;CwC+i#}D?k`H1Dka2eVx#XiC;|<8Si`{#t^%jUe5FA^g4N5T z5|84u)@JMCmewiJ@ZqB8WN)vsrIzRf*<-tYds1z2ciW9JhV&zZ_4Vb19>`SJLdQnu z=-GqpNG(ax*8{=9XCp_VvFMBKQOxJ&0q+uQ8>xNT-$o$Z_%qT^PJ+2K(Q+a zwHqr~@!#u4(K#x<*T&M4BG!M~ANf^5iunaX$N&V zTd}7PUk$&y7h)LtdyL7SsMMA{DKnHs)@LGOyeM-*-&O7e-W7ET+~Z3c}b;~!l>kW zjT_3MSEzB8QRdLu<6ePv=9AVKj_G#Y8E1{yS#<=X?w$PVPZeD4+9Wa1yFS~rmb zW`$2^o6&XY#op18;`BfI1`n13@wlfPc>=k^u`c=U>_!6ZNm|~{@NtRz=Iu1>AvmpM znbou=Q6T~<*K+7K$pn%y*sarnWTNwKxa9(qN(rs1=KE)18O8FCn6@kBLz`xq%(=r- zil!i*dZ~C?Aeto#2m_KkEuBs1av{UR+3O%WS=J*!YH>p$?$&87OvlzU##nVEPXo4! z_IZ7aSn$Ck(sf%(3ipw^|TYENBb9pQkAd*9j1ip@%I3h1xV)Xf*$j&WsOG^ zBPNfA#e_DO$bDv;+3pO9Y0xxr0;%71seo+l7m73LGLZ9T5$PRuL?YM%5}y?0sDivQ ze*NB=#Rg3TdI(g#xC*;rWT))ivb_}Co_7VWdo0Q~AEgM!bnEhq^(#60rgRG~4(e%{ z3v!Ow{@NKzNa^mVa`20I(jXecfk-b+Y9(jI$>)fV^E8;=P>QojeOsE2E7SU}PKyrg zbsdy*bX}m4>jqtF={Sol$v1yIDojHR%wX;*Qtfz|`F@Wuw*pMWJ_wV-a2>h#UhbxB z{97&fercAwahvV5aslB~Sz-|?O0Wd@Rx`y0eB8!8q!-zHyf&z>%dom)SS>a^w6nQ* zU>;pfH%Hkd;bP#=y!Vmn#Z^y{{BD+Tsu2 z)ooX55Ed;fIDekDv}@Z=1~}%D_#_>ft{VTvwcFY`L*IW? zaT=^B4w2PgY_nNTwelW~T{8=E(xCEGGD@5ZHU;rGs^%hsb>j+gF5fK`r8mGBblFI9 z|ElH%u?C`F@gtW;@Av)X@t)YAwrZ2~gm63`ZQ_FQ^}J)7Fw zVRwAOkEkMu+7Ca0c-5{5M1yB~93!M4#E^A(-bedY0*rG(${CbDsnyg^$$8>DrO#uvmwUGv%pQ-a7fHUUzhn8Ip1EwXSu47;_#p*1X;?Xy ztApvj_`uj`7MAH?1n=8eex<#9-+kz%T(4^u3kEN~y`KrJi8~)InK!JWe_kTd;3(pa zYM`Crup57VKXG4H#F&L4#)xaA@DC>T$Zmx0by|6{Fn{dL&nv$n)DCoS!QAB5uq+lO zKCHlm!>e)-@f2H<&(3YvYJvqBKwBV|IBSHRseYst2p*P6L=xGuH{ZWKyGMcfaO`iA z1FKJ4hK9Pk?h!zJm8=vhrpnWd7K1&Aa3e6pKMm{j+NL*p_}rv(#(Z%Hse$R&@>neN zhnY4t7>5%|rWqBg{G_Of9{tc=K0j&LMbK(+ydO6N<(DSg%qcuR@$;|M`8p<#|) zfTzq-6Na58ac`9EVQwdR?@h+_@)Ps$r&>a^!tF#zQ*8l{Suz6f9g4}hN_9j}bg(`s zI^K#5m(b{NTDGBVnTm=-Mc0r&V@P|&`if%@j9m6neYvFqr*3QlJW-u< zg|-eygt$}2tcx02Y$2CB$Dfc!D0QwUtGW4%zaTuG*R_LoEn`j<6g($RzbC{$~l{FSQ^Cx6kUw z^*!fwZ`{eZ3ZH^&g->d6zUi|x4T;Pzi68`dAgL;5*Wkwj^{mK8v=1iYOMJnnS zdCRY&Jq%y|M&BBRtRb5AzJ+-5Wx;Q5ALVXcKwI~S9+e$eDDRJID}@L;yGl4>lseCa+Ms}Te&DuKChF<7wFLY=)lK|OWf0>chGjXM^3Nzc$ClG z$B8y3P(t;w7M60%cJzf(MrNa}NaNN@ z1cEb9CjmiLeZ*}7T`suKQKP?{+U_JD%l(_Mg}T2}hE&K^jFsFD7ywo!l2B*Tv04#d zOJyoraOXIlUKo9%C<`%y0$V z{sBCGZvG9qG6Xn0cngik&oYI`zi(nDNF9FiT^PZ#WLnwnqt9-&e|KUI>+LWo z%;TNBMI~PM_rxPAyw>`aWA+lTBF?e9Vl!Z2)2g?#!w9AtEt}YKjS}zU6yUeLzTi$1 z{9-~8#Z$ul(dvhdn_`um>T$EPxv^Wf6ZBAV9qn{^=&)TK#Cju4<4|V0G){b`u1|4F z3~*F6R}!RN$gV#Q=EdGW@5e`DuO)R9pqcQ~_N0aYOG2N19L&FBtQiSyo=cpxCM|E) zo+9rNYCMX)9{F|2oFGwN}sDeyk+d&0F(dj0f8;tnP;?+Yh=mwLQ@{yV-@5i}s;2jj8Q<=6(Gi-7i?QL{S z{x)^aWsBYny^3AC_JxOO#GB8y7ZlsSq{tN6gA{QSEycIRp%dNr3t7oFANTnbMS; zE8X>%TD4mJHh5!x9-5PiYPuZQXh0YewSM}iWw?g7R6oKPDm+{k(S-Yb@R?|<*==ny zkKiiUSmG5k+^%Q|$PQs?VWVz{7%9KXEW4lAsS-BP5bz|`1uy*a5@8CkYcvGeby5<) zcGxtoL9uTSfoy9wUaD6>d292~3zd9Q8$xihTyab<1b+uL?PD(eQxK)Z8W9BT9a5Ev zKf2SV1mQa9z)Vk<=Jmy^2v4<9Vz?WxoUn#>C?j-UuWZjR!Ad~8$X*ip$SUIhaWFpq zEAKNTPpXJPW;pF#@r>~u)Qq{Kh0JY^bDFJ+N~zi~y@WxfmA*eT?plk*v}xOlhS!2=}7r z{&H)#CoD!oLAd=rW@4PA{Ru79J_WGO+;+zuKR9S{c~U5|4;KZ~(-cgsF1Re=+qOV0 zn9xh=`s$j3h(~$>K2$_wDa`K*y&~6Oup!+MnLW6UV%%+^SS}? z(3Oto?PG5qXW~VW%2JyzmlVdZ)Rn}~8WzcdriYwr@m^iFd zorUTTSDlEwL2ZZ1FMU=mMn$Mw7y{*b0n*9lGjtQD=V&W&Go7@0qWBnKYJ3hJW?vo> zc+XHg!Z>p|5!I9_bm-kH;cjRg3@8(61OIHR)7%z7+c2yfj^qSM|L7*aO9>$W$+)!) zX_DRe^I=MP8xo6&;>339Yw5z4H6=@Ea=WG{3?DxU8fL~or&VD_n9Y%tSlcx)bt8h+ zRvwf_nL}YG)h*I>x|5Sob!UtQIl8$k?wvM4q!IP06c3L{omo&7m#fzBa)X6j zZ$I7%hUL(X`*#G6aLUC(!@wJf4rE#QtegxNWI+ihhv3`d^0d*$B}_|GLO6hXISUjY z0j@2$nac78dlR-L1M26TL0C~O*f!$cVIQ49@l&oNVTCClFu8h@p9}trnfY|@lSmo* zg22q0loS1o*mGPBcCm=Per&8t@upnbMEz=xfL>%d;?zqYA}0 z{?>IrXa5fbYQLHT(SdO7ZX1vTkocTgny+^^_}A8rg1*ifOl?VS0{?*h@1Lc2q1Ye? zp2wh@XD1GHs7XzbH|rDm4O3;Q`FXcSik%xzl;Vm74TgyIsVkyHBkq49K)T#(3uZ3@cZ4%%Ay!<>Sa1;N37<-3cQNnFobJ=^@wr$(C zZQHhOTYK5IZQHiJYTtMf73W2rsJNYMWFuQSBIloTjBl7rG?qEo@jIxieqxr3M_4N} z4Z(RJF)IMM^)fjy*#XkDN;nJC%^<|;xuF*F5KX0wA&W|`JXCuDu~Ey^*CE@+X$3t~ zN}4+@HHwYqMXJBhD@F4_F*zCWjA)gtj;^R8x;&C;0z`X%t;6HwVJm0_)7%o2acvjsQdsz5FMGeiVq^bwE;prNSgpPK`GR3Co3JAu<)0*NM zwwZoG5~8C52MNN?wIniq!9I@K$?sNo3v(xaJ#eDDi@y${=8dEID?Sfuxa(dRIkz^& z#}LdAI@`X$TwLVuRnW_0ME|@#lrn$gq)`hD-b=~Xno1;$HE2jl(QqQi|*|Bp= zBWZGHc$CT6Y;=U{@+k3)#81XvuG;{Fcao}@opysz!UKJ z?+ci0!!Quz_k`VgyIE93(_j6JP#8X;^nTFB2gM1LCKkkw!1>?LjJoCmY) zVz)I2?`JS!RrVZz!`yX?31>d_3aPPO*U-@y$pQC!Lh8DVTlH1?n;6<~Uz?Jgd6F2G z&7}#lHrY`B>`OO2be5Jw(7-HCsK;FAfI_&LR)37X{o-2PXg0vw(yTZkJ(~t}l?9xVGqyy}Rp6~WM@?zN->d2f+FFFQ9 zSHj|fIlb^&7eMtS2x;1i2eD4r6qbuN>oDdiuk_ebO#vpvmS)??+{n*VmAf0-hBD#d zE%cwNm3(!`Y(rM1Z?z<<=ao9=vLLsk9SA5y8Z-bcj%VmgAJ{o7g5ND*=kxt29MTiq zRvTmeY3S$pS_%~5t+QE{3Rg1I0Y2LZqnHX!^Sv4dddXyN4bsVPWPCapM72&%!`(vc>fMXSeAY^g9rFx(! zYx$3xkj{a)Z4xPQ5Tk9RY!FbWZo7i+rq&Ea!DqL%(Kbl7Vc#Q^Z>ka)K3B*SR)?H@ z@!Ec!T;_G$IsBVk5ionh$vP4mmt7w$;Srv3ndPX(yS|vw%$j{O#)KkHZdS9Bk*AuF zw8R7HtlP2d2n#ZWlepD~GHu@J@Y3ll-=aX`1)LkfT}v^qy{%P!h2_}4MnbxO9* zR2AiR8D&je*6$GEiCa;xMnt81`}?WQcb$-#z=v56dPr(;bNNT9&nz9;((QQ)Qs0J} z2`1H`E8Nz&zXdc|I)__5hkbakjW<8oPfbGQ$IhA-GU=b$TOh)ll3juyL+)^#FV$a1 zowLe^Ntdsk6Oir6QC#kb+uM4A)A22-M5fI|m{IJEKW;{wbVFi`(neWiF!}l*jHU;z zJf8JXKtP&O;{w^BfkBwBa6_uXve0cfI;1(3Mc~CVT)frsHDmP>74Z#v&y^yMBh6af z44htS+{0(2u?>!BAd8qFw9frPS+8D<*_zXvLVqKK-L^ue=EiHZoQZ`lv%qv4p3_iM z=Y5o+vO9oPu&t!}fEQ>z#swNFtqBBQuHGNi7Rg`cV#C>RV%U$-PkH)BihM^T;dXdt3f5;J z@W0ux3@^VuIViQ61;y8{F{yYa1EN{C087p?s+zsuB|$dHg(dKw&+N^vO|3B@(B3X& zyD^;%^;B9R9FFl(i`kiCqL?&uTujIBqq+_nk7C7c_G;QE>XU{^CnrdAGNNHzhh~bW zyt%1&(ojm&jp!wpi~yT_eL{Gi!%`%lMa00M{7OQhLh)-0yJGf~>LX(#LyFR;X^E$H zcNYjPYZl8&=j(KhYU(h)3U zNavwLC%VDaLQ^D74dWIME;Wxc;nq2b<&j=EvkECk6qTEM#_K>$w&pKg{}M4@W5sI( z+?`E$_S=?7f^q5L7A3|6W-riRq`Cz@EM;*V%Hrl>2^+V*s`=4&v&U%^NIfK{0~bIb zsX8h>xgB{5XJQ>K#vy>Z#;pkISEXPnK%aP}2^)vvXA==kHs1svX?Zbjmy{xpwHf~#vf|+|WMpEe~&D#1u zg!-Ysa%u$y)vR;nnz$eF0}oXStmpu&aDba>dkyeh64rY195*?nM=9dt9F?Wer98)IkZ zv;sDX+1y2v6Bs&eDf_JONE z3MI`Ga6F+2)Q31aioK?9!Y6UMPk{5K#^rMju!l>S%s(F8DBqBOHTa9=sZqU(efgd8 zVRMYCP`Y-bQU?BUu$32-Pe6V`AYYZVKVUTt!PouVZNs^^GBhWDX5REfD!6}AOnl_N z2RcJN_m(N8PiB7ZzYTV}PU@Yes_gOldHZVt&OFLEBNw^<0ehZR-PFuNC9Wg*N92r( zc{>f|a$2KEPCoa3|NU#$K0t;UVuY>Mqrm{(hUSq7;J+< zu*eRI0)m3m`0hBddzM7cpW-vvYJoM@^k72J&K%k}C)XDV?8dlV2)3tdYoW-_g)O+q zKeA6A+hOKKDyhd=9B12A=E((3vIXZ$!V!*?!1r+L3*&&8-Wtbv_Qm4^jZSor!3r;Z zr$h-wf1ENmD~Sab1gv?_m%-^C$Dp(S01iccvuD-0IxpYPRuGwTtj;st%Lp7svA7AC z`ar~R{TVE!S9HmCz2F}B5yZCAOJlqtIc{o{e}1G{p3*T1B`ouTCLzZ@oXEBLIHvoT z1P~QjB>sucLd5NG=A|j886~Gr?hudEriiUi9EAOVie>$JQMz}8A=o4Mi9wC~H(Md7 zB6iD2M`E73_?xLp0s8sns1_ltSCn=m?Nr#w6dbbm%A@u{1Ep1KQxM z_MLkZdf&%nkqkHGA2!7{4=Jc+GyL&(pfJ0GKKEf)pONEc>~~an{(9ETw`h1}?)~iWLB%>9(1V~*E+&oAqPrdNQWK<8k`xEa$`F8P6_w7Oorjv5YLWRY zG?TX6lC3@d4PigUmVVB%dtp-W)p*vxS%q|?G79>|8Ie08mgb(`PpeMg!$BB$$1t9t zlrmluF7|;QR$i^Zui@9uNX%DPiqq4*JshG2xffi*OJ!JZ!7FWDB4IJSgqxRM_>->Z&5Pj9;+658@>v^3M{a>rXJwZdMiw}L6d3|; z{$0o!+@xNjov$19Jz*%ZEzh7<>J2x!?!evi_LWyGP5hR5-{363FXA#q*g-K>vD6jS zT)@1Pp`sfzo01&9NO#)r$kXr%8J0M--iRQWHKtqQ(1@MbIo1JS0SVQrJts<6hy&;W zl&K2}KupP5ySs!0F&mFC?ZMM@Ju~_ZDwZUStYMZR;qvrwjN4K(*o5NEjZ2P=96Fcn zxFLcu!vB`@uyvYPb&{ip;f7Ru&p|dF6gzxP)vECm=U6EytJ55YX409n#+t8Ikw1-Q zh8B?z#g~pMWwnF9A&6xOw3cyU|E?qFL|#bFE-v#8NlTfa69iknHjHPSPC+eYZ$~>4 z*W8+f?5sd^$vWj=+B{F1XhsgM!le)#puUTZc|V2K=tujK1m2>GT3k@w6ICG)E+*Yx z$B>GGY9)sud8?Iqn`9`&P%lXejU1{-Mo@Zc>l}7K&|71)6IvBc;}+!Qaue%TVxa(f zu+s~+mofrIY{|&*$aDDVj}|{++hub|j`mQ9IsDRC!ZGLuRRqVNTJ`FZi)UZjyAD$o zr7r5ZSJx z+6ZrfW>N3iO8&04f<~Z)d-o~gKr<(-2HiZ^!Q^4vKrUKQoODB6!a-ff23>V0lA&;B z3+j7H0P_g^u>BxUtF$PNrEsbQd^-wyJscszd5pZE`G8M9mquaL#k?Ec)_UVqj{m43 z6>tk48NK)mH{`@n}FzYkK4Yzvw7+C!#?}?{o~tv`ufeYD<%~r$+R^sh@MXa1P$XLst&XV0?HWd zjfvw*?+p4K>;>u!>yLQiOhH4#Cf)~__D4Vh3s)SCA40kidJdh9J)CXl8$q{DfpnN?1medCoU`TO~k#9Z#{b;8Cy*e}i3{anZ zY482Pysd%u9aV%@@HssdV_=j$AZyMbI1vGB_)jt6+mk*#{9UlV@z5{g@0P^AJpi;J z6ad&+nC|%N1vyMZ+Doi6ARH8ESG}k@HEbWPuU{=}e%hpoyQQb3HwBojn;8k2FaU!9|7)kgi_vs_x!N3_tp?F|klu`q85Tm2U}u`tx9cae71@{u*39q&(At zM^$ftNG8S6{Tzo+HwE+SX6~N?nm~Q>v1H6Mv2-p&xRg=dJc{iYNCxk9n~kn%A5VdD zThf#v1RPdPheZhrd_1Gn*LlKk?1+uy-o`brl0(V)wncQug_n4ME|>0v;OsAkc9dk0 z6ow+JR9TP=NQrH)q&3r&BP7X;Q@zdLW`!LKWN9zU>!x-hu_z%{;lDHPG0|WgQASHp zv?$fCxYw(Lc3;>`H`4pq4g~-Bbgc1pVTK1xCD)jho~ABWUB#6nyyIxglQ>tGcJ-+& zaThV0L?q;S`ug0Dtw@+R%$eRVwXM6$_#4J|RtKOt2^5E$3y-s$&t4`&DT+xdd1F-y ziXC1{TLku_07{5S%Wm~0Jz}j&BL@llwr_h{q7vbz2YCDM?P#o2kT}FI++IKKr6(;C ztAih65MC3veGzM}A&hpaxBh5`K^h)5($Y!UX5YuY5sjWD^Q>i7Q?HD)*j>Swcz`_} z0+exno%xI8w-C4wvk!nLfSQ#wxvv1hFRHAF5b&_NAgSWJO@0I0A z?cEI@10IljvL-h0{|E@F=t-8`N+leP9)6;OXQ=|*Bynh!k-c9Z)N-HhSK|l@!0s+` ziMdS+6w)bI7ZmH`R7!FbZqQINsM(USy>Z@NcOOok!Tnalu4$63G;n=o80@3(h>F(f z)*qw+20x2pYOV{Yj?l$`{YQ}y>zVKm(1v^i=tf(SuBcR;^iA*{%EdHc2btXl%;VfD zQ)x5u#a0@=I~n$eu7TZ-3*8&l)w9U!jW&U_2Oo1SldCsFGvdVycCO@AkDQ_t>@30= zbjPQLrzM8t^QvKw1GAsKJgFGGG_>@n^^E^=!kAlWyWQw>onrx5zXc4fS4iM5d}BhSccyQ>x-PWv#hoe1BR2r&*hzO1YoZO^y3ynb)& zN0oMzH@kF5nwF?r$$9T{Lw%rh03jP|&N}K;vzbd~%x}gVr*I&?Kr_B_)OATX2`CN` zu`5LoryDX=eVDhnP@kh2s%vvk4wNe5h=M-h-0~8is>s^3ph#YvU;0`#Dd zBS!hP*kve9WgmgVt-Dm$Q|_5%uo$Z%GV1JJrl&LaFoKWMXs;CG7^pVa4pZ$QOL^nG zy&RaicMEM-6j-OF2{d6TdH&0MAI)#D3|T47?zTJ+F63>3b(TLwJSlh}^$Q!L-uujcT<8EGv33 zdAe_j)pfJuSd~%}rJ#k<8g&cF#?~$<97(#;CGhYp(Hv6d7RG08>E4`}Ky}Glha-Fc zzRaG^Vj{G=zyUvmh1-DMQJUN>&TihU&TgSxfH`ytDf}%sRn)P7eK<~P%ihWgAG8va z-JVv!CH)TLxz3QDv+DLN)OTfHTQ8FbQKU(Hb1IYbo`k zaRqmMnabTIoP>Z*(WiKS&gcWV{K5(}FRxd@De!9K0p*{~7C9rm9%IJJY&9Lvj#x!W zrgEXTNf(p+;~SG>Q`bZCT8R9$RS7F-(mrfZbB81*kutZpixT_;Tr)${Bli(F5*Ohm zK_xH7X>}aegWEv8z|C{u`Q)o+4@5M{r0-%njS^1UOF?M@J1a%!Iw^x9N!W$-90mw%z_d`FwVnXl@**)AG4$^gEW>i~b=L9pS@5J2zR zfOt8G-!=kHyU0;(i&}@H>@SO2yX`MS%tGw>1~yy<0}Ct#(R#y2QN>^B*ZPOAEQOpR z>6}1d8?Rh)$?Zj9EIFA+v-9PitL}F zy1I|O^cVDH|7EuQA12=aVz&JU>ixfQc{a*2b_ewE-KT2qa48$W!+692Pz39c3GCKd zDVEJ@;?iu067mpXKcCN7NL8vl@G`l(Q(gDaPOq0PT=Ocetw_&f>sdT*X+2FzSFbn7 z+uO%F9v0SI+*(@emfSpi^{YKpt*-j7>!iC)&v$!cSuNbUlw1wIq!^zHtpS0&S3lc7 zImt3gvt*xAKG+wo1N}a=VMT{u&QsVUJ^Im#KWjhQZr4aiaoJAbRhyKTm{az#rD{0V zoRnNkHp_a$bv(r)f(;e4VV77ulOsA{iLH63{j6Cpb0U)@Sv~E8k8^&B17YSL3u-O3 zn{ckbA{kuky0T?OAy6Yl%aJNCdr%$gd2TT#i+NyO3OjsqsS=)Ox+?1`FWws|ExXNl zteTEJhLeTrA}!TDITANYFJTE;oV0m|1 z7~uYxgs=WU@@?u+aK*6k;%U%=GGGAb3-J6b$QzXO>}mj};&FeW^90mkT%&0jCGv>? ziJqfDU}9)J5keQ!C&THp6#z~Q*n^yn2JB^XfFJ0y1!AJZ_8B4hw?my;d3A5Cd3D|1 z?N;@2_ALXLVKEdPosSm|orZcA2Jmy=-DB3Y@(cP&6r^2h9u;V}3*(g=@w@uA0^hde zuezrZq;}Kn&p%JvQ2$-ZiF|eg?RQsYt`3$)hXiCWuo_akVwvtz*n|Xo8F@oDDIa?j zv%R|byI$Du|Hc-7Fx}ij`50Z45Szw9;{nTs6M@_%dqRT#p?FS&+F00fvR9*6-siY` zUzXj=^x|nVqY2d01nX^r@;%qMlN#-(%K_v$EIjaQC7p{9W1!eEl!)|9`eC{8tY5 z|3OFMv;PBD|L2*_M@7c!@ZbCYsb&rr@)9rFa9E27)PN%dyO~-_?ImMff@OZSM_l*) zTFJ&_>z1|eKy@lt>fdREoZ%Hw@uu!d@-K$F*(;J@$TaUQ&UsX(~hm}*5g*= za?S45p%f>yTeYpBlU8ZDd_BvS+jWt-Ds?Hka{6zBdDRIAwW6>(dviL(vbk%@x|noA z&!H1*o1Q%_Omytw=li@wr#>O~ceWH6V_! z>+OaBas|y37^(7g565Kq&U8Ol@X_<;`4w!X?JD?VdnWJu_@ak9Q8!!vlY#km-L3A@ zvl$SNfe#0+9?lRs4mE0p3zy#Nr(-V%JIikfxV4aK^a9 zOyCSSV*O%e4Kn>B{UZ(_QqZCyN|e<*_H;FUS?p`+%0S4?>^K5V_LLFz}t1$C^!Q5f`?s|FbWV!>4D`DR1)>Y}6Awf;yWsO-mE z$n1yg_f9j_0W_ogO|BNvokpbgcO+EkLQblTp<)`t=@&}Bq$9aw zmLqc+jX&s@Csll4d}iNGVU82=mW^bm$W)*b2LcRZPH^S%D+Y;~w>4nekp&c+iAs;Xu|9f6Hd|m_{FB0Ej zQb1j{$XN;e@4OVjj}ac~0pIeG->{<0j~rA5(NB=ly(WWqIXkYHyFK$bwn1*CB}c)T z@PI;$)X6ukIql$HEo4W;+16-8@9oy7iOW^oSYeoL`+ec?Spn=1z#Zr*`F|1Xe>RBx z|B03D9|871#cHG6Z=1yr-~EWPDO7y4=Z*%WOE`%LM)W>M8d+4K2EGER2m`U1^6;9& z);MILG%ng0m*HWzL+kZ?ZNN2takDN{s8x--UC#xVl6AD}=40=o;=KI6x3LI!YN-}y z7w2|ia8^;x_4m?S$$I0&S!g_~#gl8*cIv}#VWOM)ngo>l;$!?f^QpEsYz7wT!-ANX zrHoDT?LihHpVt52D9@IiRmZDrs(K~U-$m9Jby8NP) z^Bsk+64=B4;QI6a2sX=g7c?p)lXqeA(8Ar!=AS}+%eUv6=q^48qxP=bZ+EeY{|Pj$ z!qw|S^zmm6Cp*(OxdfqU6Yf z?}RkD^3**&Lj$o+RGX2W4uU?_Flm7ZCJ6(OU?WVsjDi#Z!-2w|1fKzUwmm_1Zi#R+ zkR&;gxsW}FLEjNzOq^s;7{vH2;dqNkC=TX3vK_z*2x_i|qzK@;qi>7Q`p-E@!z2nY zGLrw5Vz2^CkSLiU52sT14cG(V7bmr%JK_X6qifxKaeu z5LE<$=IK=xq(QSOcbM|v7wN!R==)&!z1$Y$N~4@r?d*`B(VRKrx{mo8`lCh!lX+dJSLoefXd*Bl^{la?dEJjZ ze^$)#kiIOH?g{pz#S#MZTC|l$oDL-HpH#Teh<384q+#vORE&()BX_Wzzo&w}7q2mJ zrj*UmUbJsXp0rOAJZLjEn&EbPk{MJp9HV}UdlOLiF|+0FXMAdYA9wo)p6_jN6_50D zD*bNoyFhP0St~x;N# z08V>!{>OSM0Ad(`2#LTODWCuV0(|)cJA7;kNO>?X{%b%^AOIP5m_WjP+m-swG$;Ap;%U!7BoB09QBvgiuy~IKY5!(3Kx(d$_w(djNdEx!!^A`yUht zsLyMdmd2n?fdWe#_xR&#%x2 z05GEZQo@^ng%rtG03t8Hti=TN3+cyggqqTYyg302C{Q5oeNYM|Bye4GdPV(O)J8+! zLOOYDJpu?>=lY@?{9zEB0|9gKgPE8A3F@y9|EYEg&Ic^yzyBOKCU~~=ga$L{!$_Y3;=kM=VI`S4~|H(-=0W{m&c4U%y8k-M{-J-C$Ne*?27Cel{sr|yrFSLpozpmq zhiLzs0gv0q7j!;QzZ3${_B;I~#;=f%^8)_lck*eM_KP>|C-SbB@w1EZm)F(h+vfBK z`tEnF-&S7jmJfp31+S>yC?9wf7WnEP5;?xtR1Mq^&gbf7SM?7wIu|Syy%=@Ly8s~& ze?sq*{J&}qXv11Kkxhh%Y)U*NH3bx>X3M$?0Bc zePq|8xn8VysZjl}i#(6=zN@{L-cmVnWf%Za!u}oNwG$%}1%EgQuF@Cy+TANK2&le2 z^D1Zntwy{)z*yq=y~oczsRk$7}|_;0N*F(}u#1aa%w z-u9sBTf6sH8-M_Jfh+MI>Ave?!E7r*&A3YBbnbVx&=Sa&#n|bPw=n3c@HrmJqLnB} zQH=~G&06JN=OE?kY%XV%?YPEM(pT?PtM!X65!m;2<<)17{ZEvb(0VB-G9M8^WVe813&tYaDH{8F*G30N`o zOi7W7JPnN_&ysWLotVdL!?@|wCEgZG=7+2oo{~pyicyx1PT9%MtkkP5iJaFQ>qKlr z{#^z#YUOYY&0%I^+aJs1Z)aC_EM22K>jQ*|CMukYAJ>{Ms;DSa_wv-&CHK{_fp?>7 zL^ks;sq_6&^D(PGBRlh54-FN*3aH2M(s79MQB7W4dbiHP zp(0Aq%p3bV94JMhF`d(d_wiKGt{5Ye4FBHGu6d|>BD1eOdS zbkBE_8F8-dkuN7N35HX?9d9qntfeY+{Y%5Mfb7o(ImZcB*#x$jjfg(VNb$utd zw$NOS4Ywwf+xt!DZKqvLyg*xf+Ka}lHR=5}u$bMu4OXvMH{rmxjzg=WTcP*1UEYLL zl6AS|#1#QgTe|0L8_l9`E*}_N)`3lWgK!GZtKM?K$JsRfmQ;eW@l-*$Qn&E00M=XB z%q)9=J~x`UhEX?p_dO2Z@!dCP0-?{co0N=VopwLh5`symBl^o3nB$`MBJfxa68)S9 z4F;e9c(dwA&kY<=`qOJ}tNUSIQ90GCj0hf5n!DQ2caz_YnacT#hTvxxd!As1>5Ix9 z$@k%AbK8%08YAmv+4ce1+sreO`*rL-oXM**#q*xar!CU%vfy+?>Y(2mk7kJzt0;%BiHY_ayA6 zdo;Y#Se{EO=fpcTd6+C!6=k*zZ&Ri(~>;;rAS_ir^!q)FP|y`${Wn`P`m4?WR* z+VRRd6E-L;DXdm=r$o6P5rBQZuyx$C_Q`*O6RAQ!;5<7DLok-tAv?+B8_G2TK_6X3 zH{1RC!`L0Uwx@1?RIosf_guYqIvKr%O9Rp|$t-sEE8PbA)49Pj2VYA`e*u6 zD*zXS4J1Bk#7KWHz%7Y-^N*^zmSV!FxEpS*i05pMCY;HC`up##t94`<2&THq-yIZ; z?W9bxPI?)CSi#+%OOjFTuw7{CRrugt`}xA3ODN5jH)haz$F=rwO$aT??Wq(9&sSGu zZyN5ca8_W&gI~^mAKVS;LcLpn0=HJlRGx+EjcVuNB2uGXKH)`!+;otW3P0&~*p7RW zPG#7M`>FWBYb{6J9t&)X9-`G%P0S`Za1ExW=5*p77Fz3@N2OL=ch=u%&n@K~3wBgZ z+LJ5tijq4f_VDR(IUQH^Wf>f@W7^(TbcCjNo@FiR>!$#=Lu!R14!B--cP%-jA;y9j z=SVFFa%B?Vaz7E@6pP!Y^P)x|Jrh+o4}10!K5;IW2jM2HZ;5S{BxWa$lJ%_@y{V)1 z2Se3J&I32N6_JoV=kX$et3J+J@zMLdO}uo5d$bJ0-`&SEGY5*bJg}5 zypQT}Pg<%Nj3)7yZM*zi&*{Wx{j|VUgk`v}>E#j&rP7x-9vv74)%fRAvjLEjW-(1I zgMK=ou>(bWJX*psPn!SX*#IaIu}#s32y>N{x7;w1J#qtBnitw~|Dau#Gu)${?x+FY zD{0C7Lk0YACG^UJW3KEbX(zF2E?@aesF#nmea)KW^F%F!rPQF;k=6*`baVdT6bwhP3b=fkET{S1j3zt%*xrTwB4pgXeanxxhCs&IPeq|X zTvq-e!mrQ^I@rpU%rq^c3$AH#zkxPSRKS_R50a6mmJOf4x`6`P#qEEcRSRP8NRTS=vIy=Ae}8)V0KgtsK?JDAFCSW!4iLrUI2SJI5z zR7XQH+t6wuhwG@fTjM&GO3?{;XkjKT#%fs;Qb|=_{2ErVxurdGQ?WD8+3An8+^OkV zkEl~}t%X3|-YvaA1epU}iE`D;C5_db#ZP)Kng7%CiIi!P7l>oNnPb0ju@@55A=OA_9330D+TX-asOs>KKm^Ph=?dU78bG;%vQCKPLpEbDR)I?6Na;=?dV^bJkP z&6_Msek~e1)kbYt=7h3-4}_cxrkwdlYmMeo)2f~?VoUqoShsLrd_So%XQ^o|BP?jo zJolbhZAn?RTFNSfK?Co1G#tecBv}G0qEOIS#w~${xby5tQi2Ptx^baW{bU^sFN&d7}wj955bKV9i2nAPK~m zB%i!iVn?;bh@&Yqr`lWc7418v_NE@&nHw1g3hgaEuj*mVeN-MQJ~N$sdpDPcM}tXq zGM0HBSRsh|&9(+mT@PzB>H$CdGDEV&tkm>&w+&C zMAsqjOM{DPQZSO{LM*_D4m_udT-Vu8Nx-HWy9fnW$|<~<*Y!2(RxT;1#Dkpy5A{## zy^Dq7@T6De==1s0Zm{9y;i13o=K(P&-{`O)xlSJZX15(?Xq?G#nvCGp^D5)iq5HK3;plEIRSR9uA;)p8jj#Q0KaYQlnH!z6#?@ zJ@@+quhsr?6*?+ymkf6nz94Q4+e=6!h0HzGuTwHpdbq4QgzNy?d=&k|$S&BFwTQ~g zM%(u8Cw9{)5J<@dr+?J)F>0{+W$Dmfpi7*`0V1C5RtCRnzqP-b#g2Nv$-Sg_%Sjh) zR4$%8f<{Xii!daqZMjhWsN)(sT&_x7N#OBBh3h41-H>0t44zV*juR=X@1-xr9=x#k zuJuI+&nuLYg%ovs`%lGe0bObF-C1@okP7b#mOZ}w*(kC0TQ=uklPR}eJK6?lrz}`m z9+*5w7w-~m;!3f0z`8p>JWU}2Hry_4!bj)faB`UwcF zy=Oz{>D=4&2-0E_Kch-!7zc?_Kp9A`_ocH{^xwzGx{oYZ*WvKGP10Ybs^|+T83JI? zV<+#tYWVdsx0 zMq(dX3yNLW7j89mZw7Qipl_?@hpCS-E*!Mj|BB7G=i8pCJ(~FlvCciEU(P} zrqMY(LC3wU69~UAVttvpxr zdxA(_ea9`)22ff=4Gyt`Iz8kchYqOInX4q9Q@J&~6_ida6gy5p4zY~_ZCmB;q&_0W zF`CH>-q>oTIj#fcK{sQ{!j*W#!bWF_1daMtxRv?mTBW#yRB=Vwdmb>7VQ@k zE~05;E0|~KM}VM{Ie#RyT3dkRe2>9rnv}N*>7vKx`L@p+323KCK2Y@E+=sX|Mi}Hz z0_kGEL>2quC;ol%(HBz>x2tUbaj&t^f_vpOwKX1`9xb}INTbbUGdnwV;*!KZe!3tS z0a3J~XqtS(_7hzRg@875Z~zXl93OY(1YWTZxrWHP`3{3D{3KTQ# z=y6%ma`6@~SYwm8xxZ@)Nu(#vDK6vY*i~YvRxj&UT~dsv3{M-mQtEMomoJ29 zDze8jazblo3J=#YB57QW9G{R6qU~ipnZ?{{pBHg=84woVHzYBh>0=a9k03Cm)>^2> z+Q`-mQ~% z{j>{|Wkiw6&W`s^Cf%2jTb|&BF*aiWlBt?!+8wK&eDMqSt%2`{-I zTjuvFk@e(oDgR`FMn2GzqhCSah&10ro10<~*+RckjX70&PPTsMe{!M{i;JU2Ze{?X zaeIg28W2Ad%It9MVHF;B@77iS^na}Jd*I>k4R3GSD|f`X%ttAk=J72O0sU%U16S)U2c6JF>wLWV9)tVhlHbU&Ek=0UFkYWh zNSVgY)79z_hM9m=QgF8MSB&^lrt)cp=uM$vIWRh%Gkc7l@X1q(6SnYZSW-qF6RF;H z{Y{Z`TmsXljhiK&QA5x&bzl(+l|qWt#=V45XVSzJEB@S-c`P^PNMgDpbeR ze>(bOkxA#5lEX^kJLa3iz^gKYI?|gL3B~ppDe;-iaP{ME(2lt~IMa{)Z&UVO9{VSQ zlu);(fdBBb2b8x*3Dtj(1rnWID|PxytcOdL2h zM-OHBYD-#71kC7iN3H;^AL-Zmd}=~X@mrL4M`!HYrO2Z&siPD3Zs9Gc9USdPM{%0H5IQ@^`6;v1=CXJeDx$_G?2a)L zunX*-TQ)q=$Yu6+C)MiPAE$N`Xpb{qx?KSW`rT5 z$48Tj00F&0&e!*6q6A@uk0Z^>gT3~6e9fG9Np}$FgmTQ zL5DI*HY`t;%+}%9+Z+q@-hGI;ZtRztH^iP4cNrc~KpWj|#%8C9mSVT#%v`^M4Hl~P zuT;fa{hUKtKV*c?24hHBW)xX+GPoNTP5N)e&{(wt5x-ph*E~t@EI==F|-^gFc&`L$GSR@g(nVY~+;=AQhwY=gv%oTPv{E^a&}k$cPw-spqndzq!m%ET z%Yk^Su1%pZl=LmAvco;PQPco!5gzUR9JR{B(;Fa^dEilMoJKt8K|2>XG~1VU(hN^X zJan^eLaw8wmSK9t4NQZPi{kx6hrH+w{U3tX{M?=oAkMs*LOV)^z)7aI9idAyF{=Td zn#+$jYe(68ts!cY$FlolW+mvp;|w!M%&MmG#@cs9ly#m#^VWA0*CqI-BE7e@c%KjN z{*DKr_xqzU`L6?g0W1?SY+Qj7UZLP2%qb$$EL6xb~owzHxzlZu*aDxvov+zP*SrXg8mo3cSU%9oq7> z5R4buW z)1_c;5Ziw`U1`)^X3UlqjP5RY4MiKVIZ!jYfB{z_uk@bzlyw>~((uSBR627{B+>>ld;)C>;SDe2dJ1sFkm99D~_pV^GmOM)nf7 z`csf6q7T_fg6jf-mmz7`fpVCntk`Vmaeia}DM|d?sM;#$Q0cqLlwSW$83-s>%|h0* zC+}45dWk*Gteaypn)+iv&Yg*wAvHygp(z)0TLIWO;OvX zMO`LSq|0MRUVTgT>Q{E9N~XB6c>pxLS$3e~4DC+_e(DN4p4x0me^e{Gc0iYFo;)ed zyFixonoVvY3=*viSiwe^L{`5ylJhwsvTFtoTuPAH@YANiPc>7aa z5BG$T!+LX;35S!tOI~KHb3%sSyI_s3+0_i&J_T-C!*kxV-r;3x%QW{4WckO&(I?jQ z2jA?B)UsWTy<}dP&g1XaQsI`amRqBI7Tnrj2Ax&zNo_o@N7dDnfY;%afYGpDKxMV- z@c*)OX8YeRo!J=w-_Q#qJ_9||f3M$Vpy&Ai5Uf??b*>v|Ze;Dp2YmK#Zf<6PP!YFC z;J3DE+CU&>esO?w<#X36mV_+?MYgK^htBfi&I# zE))+As<5%C0Z{R|5K&Uvn9z?bul4Bw9YG%foBB@xTu;6R!a7@%dvsmn(Nd%?0*M^wv|eiyAktH13B#M+B-0NcuX52XJ+E zbvKY6Jlo!H(aQA92A0j;NBpZl#=HE_99iH%t^sp#i6;c}&py3nY=P+>DGbGw;RVqM zP1a$`WtZ7zVlX-ip{=wZ*1EL1mwBnkW|Dnj9aSd9>_ZJ)L98d3q^qWsb=EQd=%-&d zJtiYxMhfK{=hX0opd<4&1a6C-ncp7uk!0^#OsNWf(q(D~7o#S{(QaPg+)v}goa}r3 zyw-DzeDm0y@zKRe6@>hS5lbvv-bZUl6XKVGl+!d8njJ}u(n*G;zEy*4n~1^zXs5w8 z^CF!b%fLS}%U~KF>0{NtvDz2Enx&-seNKub`qV*NwlS86ZL@mRxj)^1x`Pe}#OQIL z|Kg%5aObD<=G>g1AES+@$3G0c#zTZW(V+Gg*_tP7njSs8kQQkTb;Cd( zputct{(h|CvJ#G_%Y9u6WFslZdb<<|S!|`VnV%6g6!SasEsw;heWRZ25_&S}gLbRy z@r+Hi3p6~ns}7Ttx+-8AQOiWIcU0vgyb%_Y;Gknq3zkyFIlNK@73+3y|Epmtlu#p{f%7kD6z5q z*@(T}mtPBBquF`_-4L6kC}d+@_I?j~rKxano2S|>#zVoIhj}=xXJiWNiJRC`>^el{RN|TKG|Tuoxp2ApLLbZ&vsDb6Fo&;R9sF*c3xVPL#XvS53V) z1tiX7Ghs@7MB$0#;VT$GS>HzYWTs{Y-ku1YE=b=6R3y~xXAyESlP6P=ij}gKY!cc# zo{m?&1szY!$(6`k+jC)8T^g$?`ISYDKVa}+_-Q#fgEffkwZ(9D4BP@jKg|@;`-Cmh z%e;4qxIP-=bztcAUwiKmV8BqsVnhx5U?Wl#Jt-?*`^nn-c zJb)xIIUq&tpd=EmU4hSu1N~d&quR7drAgtDZ-eYvh!JLstnt6$otQ3Wzy);_a-6qz zLwz}l9&JWoUHH#P(%+pM{E^H_L4U&hTSG5_O@2-|9!H~%VSc})lb1XchOj=WK|+P` z`?35fIqx6l&jKOS*ZcF9tHO!(P=Pz`pAGgmjtT`azg2ZG7tK|Qn2uNyo;-thg(#+{ z#-Lt>JADLU>$rcK+GD_PfuBwaNaq+**@2xmOzOO^g_4dKNvBLGmd?9lhWQK@9fVw9 z{#kk4jzoT*;;#%T(L}QKjaXTE8OmfLuhD1(%B&y3wwNBtr{V1Qx#J=rGs&BqQ+8^z=}Jq9*o_d2|}5x~P!4 z3Rrx5iS$($75&Sv5kv0-)I$S^O!6_O`yf?!;H^_wHd)1eVZ8Ag=a(mu>5L+d7itQ2 z+imYsOFiDHi|%pAiY@0BoDIR>uqWBneZpKgy}g6G57ZJYb`6W3Ia2E2#q^bAMzNB3 z%=#jyvfgi`TY9yok!V=*LV}tJ(EIVw_%cJS;(lG28q@MAGxp*cx5vapa&(&Bq4 zDl{x@dGwYpAQWWCGy5o6mL%Lx*g5cP5x;}+aF{O`Y@!G*6xCQM2)7G=VDP3$1i7{z z!{jh+XRRrPjTP};dJEO^NI{|0KP$XkOe3d{njBw>Imew0I>5Qz|8e4%UcsCUw!uwV z<8~UD$56|VxGo)wb<+qDfJ6^*YAOcqBh&D9+>F@lwuHGFuB%nrLA_1mwpXO^sD4mE zy~$Uri$RX~Hbt0}LY30-ACqLevr**>{BX(uE7%Wq^6|Xw>1%9R5|Q|OCo|P-xfmzG zyBsNXY)6*RhG`%O&qo*KakZN{SLD*-_M{eRR=t|D@ZI;X3GT%B&H}O0T%oWxEqNYo zz%VM(>2WDNJHk^(^_wUuk~h_Ku3G86cpqAoyvmqH^0@JK4>WPXVC(HtUFdOWhBFT` zs<6z|D?JI=q)?e|9X)bP(6JqU^{u#^4T_@cW z0iRRBzIF78KjN?=URI0S{y=*2;9JAng|@K%!zB)`8>995X)|KTb!3?7*s@+9kxwkQ zocega#TVprOF_xN_;_}RVZqMPamdtAJceC5aAPE!O4wf(O==`3+1`7BnM(YjS78fM zbSU&DyuDxS;V7p�Br&c8|{KY*ET{x0^+AgzMVc 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

3`1GP`0ep&L^UU3(GV?%u{S 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 +#include "globals.hh" +#include "G4ios.hh" +#include "G4PhysicalConstants.hh" +#include "G4SystemOfUnits.hh" +#include "G4Material.hh" +#include "G4NistManager.hh" +#include "G4VParticleChange.hh" +#include "G4UnitsTable.hh" +#include "G4SystemOfUnits.hh" +#include "HadronicGenerator.hh" +#include "G4GenericIon.hh" +#include "G4ProcessManager.hh" +#include "G4ParticleTable.hh" +#include "G4IonTable.hh" +#include "CLHEP/Random/Randomize.h" +#include "CLHEP/Random/Ranlux64Engine.h" + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +int main( int , char** ) { + + G4cout << "=== Test of the HadronicGenerator ===" << G4endl; + + // See the HadronicGenerator class for the possibilities and meaning of the "physics cases". + // ( In short, it is the name of the Geant4 hadronic model used for the simulation of + // the collision, with the possibility of having a transition between two models in + // a given energy interval, as in physics lists. ) + //const G4String namePhysics = "FTFP_BERT"; //***LOOKHERE*** PHYSICS CASE + //const G4String namePhysics = "FTFP_BERT_ATL"; + //const G4String namePhysics = "QGSP_BERT"; + //const G4String namePhysics = "QGSP_BIC"; + //const G4String namePhysics = "FTFP_INCLXX"; + const G4String namePhysics = "FTFP"; + //const G4String namePhysics = "QGSP"; + //const G4String namePhysics = "BERT"; + //const G4String namePhysics = "BIC"; + //const G4String namePhysics = "IonBIC"; + //const G4String namePhysics = "INCL"; + + // The kinetic energy of the projectile will be sampled randomly, with flat probability + // in the interval [minEnergy, maxEnergy]. + G4double minEnergy = 1.0*CLHEP::GeV; //***LOOKHERE*** HADRON PROJECTILE MIN Ekin + G4double maxEnergy = 30.0*CLHEP::GeV; //***LOOKHERE*** HADRON PROJECTILE MAX Ekin + + const G4int numCollisions = 1000; //***LOOKHERE*** NUMBER OF COLLISIONS + + // Enable or disable the print out of this program: if enabled, the number of secondaries + // produced in each collisions is printed out; moreover, once every "printingGap" + // collisions, the list of secondaries is printed out. + const G4bool isPrintingEnabled = true; //***LOOKHERE*** PRINT OUT ON/OFF + const G4int printingGap = 100; //***LOOKHERE*** GAP IN PRINTING + + // Vector of Geant4 names of hadron projectiles: one of this will be sampled randomly + // (with uniform probability) for each collision, when the projectile is not an ion. + // Note: comment out the corresponding line in order to exclude a particle. + std::vector< G4String > vecProjectiles; //***LOOKHERE*** POSSIBLE HADRON PROJECTILES + vecProjectiles.push_back( "pi-" ); + //Note: vecProjectiles.push_back( "pi0" ); // Excluded because too short-lived + vecProjectiles.push_back( "pi+" ); + vecProjectiles.push_back( "kaon-" ); + vecProjectiles.push_back( "kaon+" ); + vecProjectiles.push_back( "kaon0L" ); + vecProjectiles.push_back( "kaon0S" ); + //Note: vecProjectiles.push_back( "eta" ); // Excluded because too short-lived + //Note: vecProjectiles.push_back( "eta_prime" ); // Excluded because too short-lived + vecProjectiles.push_back( "proton" ); + vecProjectiles.push_back( "neutron" ); + vecProjectiles.push_back( "deuteron" ); + vecProjectiles.push_back( "triton" ); + vecProjectiles.push_back( "He3" ); + vecProjectiles.push_back( "alpha" ); + vecProjectiles.push_back( "lambda" ); + vecProjectiles.push_back( "sigma-" ); + //Note: vecProjectiles.push_back( "sigma0" ); // Excluded because too short-lived + vecProjectiles.push_back( "sigma+" ); + vecProjectiles.push_back( "xi-" ); + vecProjectiles.push_back( "xi0" ); + vecProjectiles.push_back( "omega-" ); + vecProjectiles.push_back( "anti_proton" ); + vecProjectiles.push_back( "anti_neutron" ); + vecProjectiles.push_back( "anti_lambda" ); + vecProjectiles.push_back( "anti_sigma-" ); + //Note: vecProjectiles.push_back( "anti_sigma0" ); // Excluded because too short-lived + vecProjectiles.push_back( "anti_sigma+" ); + vecProjectiles.push_back( "anti_xi-" ); + vecProjectiles.push_back( "anti_xi0" ); + vecProjectiles.push_back( "anti_omega-" ); + vecProjectiles.push_back( "anti_deuteron" ); + vecProjectiles.push_back( "anti_triton" ); + vecProjectiles.push_back( "anti_He3" ); + vecProjectiles.push_back( "anti_alpha" ); + // Charm and bottom hadrons + vecProjectiles.push_back( "D+" ); + vecProjectiles.push_back( "D-" ); + vecProjectiles.push_back( "D0" ); + vecProjectiles.push_back( "anti_D0" ); + vecProjectiles.push_back( "Ds+" ); + vecProjectiles.push_back( "Ds-" ); + //Note: vecProjectiles.push_back( "etac" ); // Excluded because too short-lived + //Note: vecProjectiles.push_back( "J/psi" ); // Excluded because too short-lived + vecProjectiles.push_back( "B+" ); + vecProjectiles.push_back( "B-" ); + vecProjectiles.push_back( "B0" ); + vecProjectiles.push_back( "anti_B0" ); + vecProjectiles.push_back( "Bs0" ); + vecProjectiles.push_back( "anti_Bs0" ); + vecProjectiles.push_back( "Bc+" ); + vecProjectiles.push_back( "Bc-" ); + //Note: vecProjectiles.push_back( "Upsilon" ); // Excluded because too short-lived + vecProjectiles.push_back( "lambda_c+" ); + vecProjectiles.push_back( "anti_lambda_c+" ); + //Note: vecProjectiles.push_back( "sigma_c+" ); // Excluded because too short-lived + //Note: vecProjectiles.push_back( "anti_sigma_c+" ); // Excluded because too short-lived + //Note: vecProjectiles.push_back( "sigma_c0" ); // Excluded because too short-lived + //Note: vecProjectiles.push_back( "anti_sigma_c0" ); // Excluded because too short-lived + //Note: vecProjectiles.push_back( "sigma_c++" ); // Excluded because too short-lived + //Note: vecProjectiles.push_back( "anti_sigma_c++" ); // Excluded because too short-lived + vecProjectiles.push_back( "xi_c+" ); + vecProjectiles.push_back( "anti_xi_c+" ); + vecProjectiles.push_back( "xi_c0" ); + vecProjectiles.push_back( "anti_xi_c0" ); + vecProjectiles.push_back( "omega_c0" ); + vecProjectiles.push_back( "anti_omega_c0" ); + vecProjectiles.push_back( "lambda_b" ); + vecProjectiles.push_back( "anti_lambda_b" ); + //Note: vecProjectiles.push_back( "sigma_b+" ); // Excluded because too short-lived + //Note: vecProjectiles.push_back( "anti_sigma_b+" ); // Excluded because too short-lived + //Note: vecProjectiles.push_back( "sigma_b0" ); // Excluded because too short-lived + //Note: vecProjectiles.push_back( "sigma_b0" ); // Excluded because too short-lived + //Note: vecProjectiles.push_back( "sigma_b-" ); // Excluded because too short-lived + //Note: vecProjectiles.push_back( "anti_sigma_b-" ); // Excluded because too short-lived + vecProjectiles.push_back( "xi_b0" ); + vecProjectiles.push_back( "anti_xi_b0" ); + vecProjectiles.push_back( "xi_b-" ); + vecProjectiles.push_back( "anti_xi_b-" ); + vecProjectiles.push_back( "omega_b-" ); + vecProjectiles.push_back( "anti_omega_b-" ); + + G4ParticleDefinition* projectileNucleus = nullptr; + G4GenericIon* gion = G4GenericIon::GenericIon(); + gion->SetProcessManager( new G4ProcessManager( gion ) ); + G4ParticleTable* partTable = G4ParticleTable::GetParticleTable(); + G4IonTable* ions = partTable->GetIonTable(); + partTable->SetReadiness(); + ions->CreateAllIon(); + ions->CreateAllIsomer(); + + const G4bool isProjectileIon = true; //***LOOKHERE*** HADRON (false) OR ION (true) PROJECTILE ? + if ( isProjectileIon ) { + minEnergy = 40.0*13.0*CLHEP::GeV; //***LOOKHERE*** ION PROJECTILE MIN Ekin + maxEnergy = 40.0*13.0*CLHEP::GeV; //***LOOKHERE*** ION PROJECTILE MAX Ekin + G4int ionZ = 18, ionA = 40; //***LOOKHERE*** ION PROJECTILE (Z, A) + projectileNucleus = partTable->GetIonTable()->GetIon( ionZ, ionA, 0.0 ); + } + + // Vector of Geant4 NIST names of materials: one of this will be sampled randomly + // (with uniform probability) for each collision and used as target material. + // Note: comment out the corresponding line in order to exclude a material; + // or, vice versa, add a new line to extend the list with another material. + std::vector< G4String > vecMaterials; //***LOOKHERE*** : NIST TARGET MATERIALS + //vecMaterials.push_back( "G4_H" ); + //vecMaterials.push_back( "G4_He" ); + //vecMaterials.push_back( "G4_Be" ); + //vecMaterials.push_back( "G4_C" ); + //vecMaterials.push_back( "G4_Al" ); + //vecMaterials.push_back( "G4_Si" ); + vecMaterials.push_back( "G4_Sc" ); + //vecMaterials.push_back( "G4_Ar" ); + //vecMaterials.push_back( "G4_Fe" ); + //vecMaterials.push_back( "G4_Cu" ); + //vecMaterials.push_back( "G4_W" ); + //vecMaterials.push_back( "G4_Pb" ); + + const G4int numProjectiles = vecProjectiles.size(); + const G4int numMaterials = vecMaterials.size(); + + G4cout << G4endl + << "================= Configuration ==================" << G4endl + << "Model: " << namePhysics << G4endl + << "Ekin: [ " << minEnergy/CLHEP::GeV << " , " << maxEnergy/CLHEP::GeV + << " ] GeV" << G4endl + << "Number of collisions: " << numCollisions << G4endl + << "Number of hadron projectiles: " << numProjectiles << G4endl + << "Number of materials: " << numMaterials << G4endl + << "IsIonProjectile: " << ( projectileNucleus != nullptr ? "true \t" : "false" ) + << ( projectileNucleus != nullptr ? projectileNucleus->GetParticleName() : "") << G4endl + << "===================================================" << G4endl + << G4endl; + + CLHEP::Ranlux64Engine defaultEngine( 1234567, 4 ); + CLHEP::HepRandom::setTheEngine( &defaultEngine ); + G4int seed = time( NULL ); + CLHEP::HepRandom::setTheSeed( seed ); + G4cout << G4endl << " Initial seed = " << seed << G4endl << G4endl; + + // Instanciate the HadronicGenerator providing the name of the "physics case" + HadronicGenerator* theHadronicGenerator = new HadronicGenerator( namePhysics ); + //**************************************************************************** + + if ( theHadronicGenerator == nullptr ) { + G4cerr << "ERROR: theHadronicGenerator is NULL !" << G4endl; + return 1; + } else if ( ! theHadronicGenerator->IsPhysicsCaseSupported() ) { + G4cerr << "ERROR: this physics case is NOT supported !" << G4endl; + return 2; + } + + // Loop over the collisions + G4double rnd1, rnd2, rnd3, rnd4, rnd5, rnd6, normalization, projectileEnergy; + G4VParticleChange* aChange = nullptr; + for ( G4int i = 0; i < numCollisions; ++i ) { + // Draw some random numbers to select the hadron-nucleus interaction: + // projectile hadron, projectile kinetic energy, projectile direction, and target material. + rnd1 = CLHEP::HepRandom::getTheEngine()->flat(); + rnd2 = CLHEP::HepRandom::getTheEngine()->flat(); + rnd3 = CLHEP::HepRandom::getTheEngine()->flat(); + rnd4 = CLHEP::HepRandom::getTheEngine()->flat(); + rnd5 = CLHEP::HepRandom::getTheEngine()->flat(); + rnd6 = CLHEP::HepRandom::getTheEngine()->flat(); + // Sample the projectile kinetic energy + projectileEnergy = minEnergy + rnd1*( maxEnergy - minEnergy ); + if ( projectileEnergy <= 0.0 ) projectileEnergy = minEnergy; + // Sample the projectile direction + normalization = 1.0 / std::sqrt( rnd2*rnd2 + rnd3*rnd3 + rnd4*rnd4 ); + const G4bool isOnSmearingDirection = false; //***LOOKHERE*** IF true THEN SMEAR DIRECTION + G4ThreeVector aDirection = G4ThreeVector( 0.0, 0.0, 1.0 ); //***LOOKHERE*** ELSE USE THIS FIXED DIRECTION + if ( isOnSmearingDirection ) { + aDirection = G4ThreeVector( normalization*rnd2, normalization*rnd3, normalization*rnd4 ); + } + // Sample the projectile hadron from the vector vecProjectiles + G4int index_projectile = std::trunc( rnd5*numProjectiles ); + G4String nameProjectile = vecProjectiles[ index_projectile ]; + G4ParticleDefinition* projectile = partTable->FindParticle( nameProjectile ); + if ( projectileNucleus ) { + nameProjectile = projectileNucleus->GetParticleName(); + projectile = projectileNucleus; + } + // Sample the target material from the vector vecMaterials + // (Note: the target nucleus will be sampled by Geant4) + G4int index_material = std::trunc( rnd6*numMaterials ); + G4String nameMaterial = vecMaterials[ index_material ]; + G4Material* material = G4NistManager::Instance()->FindOrBuildMaterial( nameMaterial ); + if ( material == nullptr ) { + G4cerr << "ERROR: Material " << nameMaterial << " is not found !" << G4endl; + return 3; + } + if ( isPrintingEnabled ) { + G4cout << "\t Collision " << i << " ; projectile=" << nameProjectile; + if ( projectileNucleus ) { + G4cout << " ; Ekin[MeV]/nucleon=" << projectileEnergy / + static_cast< G4double >( std::abs( projectileNucleus->GetBaryonNumber() ) ); + } else { + G4cout << " ; Ekin[MeV]=" << projectileEnergy; + } + G4cout << " ; direction=" << aDirection << " ; material=" << nameMaterial; + } + + // Call here the "hadronic generator" to get the secondaries produced by the hadronic collision + aChange = theHadronicGenerator->GenerateInteraction( projectile, projectileEnergy, + /* ********************************************** */ aDirection, material ); + + G4int nsec = aChange ? aChange->GetNumberOfSecondaries() : 0; + G4bool isPrintingOfSecondariesEnabled = false; + if ( isPrintingEnabled ) { + G4cout << G4endl << "\t --> #secondaries=" << nsec + << " ; impactParameter[fm]=" << theHadronicGenerator->GetImpactParameter() / fermi + << " ; #projectileSpectatorNucleons=" << theHadronicGenerator->GetNumberOfProjectileSpectatorNucleons() + << " ; #targetSpectatorNucleons=" << theHadronicGenerator->GetNumberOfTargetSpectatorNucleons() + << " ; #NNcollisions=" << theHadronicGenerator->GetNumberOfNNcollisions() << G4endl; + if ( i % printingGap == 0 ) { + isPrintingOfSecondariesEnabled = true; + G4cout << "\t \t List of produced secondaries: " << G4endl; + } + } + // Loop over produced secondaries and eventually print out some information. + for ( G4int j = 0; j < nsec; ++j ) { + const G4DynamicParticle* sec = aChange->GetSecondary(j)->GetDynamicParticle(); + if ( isPrintingOfSecondariesEnabled ) { + G4cout << "\t \t \t j=" << j << "\t" << sec->GetDefinition()->GetParticleName() + << "\t p=" << sec->Get4Momentum() << " MeV" << G4endl; + } + delete aChange->GetSecondary(j); + } + if ( aChange ) aChange->Clear(); + } + + G4cout << G4endl << " Final random number = " << CLHEP::HepRandom::getTheEngine()->flat() + << G4endl << "=== End of test ===" << G4endl; +} + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... diff --git a/opengate/contrib/vpgTLE/stage0/HadronicGenerator.cc b/opengate/contrib/vpgTLE/stage0/HadronicGenerator.cc new file mode 100644 index 000000000..b6a4ff122 --- /dev/null +++ b/opengate/contrib/vpgTLE/stage0/HadronicGenerator.cc @@ -0,0 +1,629 @@ +// +// ******************************************************************** +// * License and Disclaimer * +// * * +// * The Geant4 software is copyright of the Copyright Holders of * +// * the Geant4 Collaboration. It is provided under the terms and * +// * conditions of the Geant4 Software License, included in the file * +// * LICENSE and available at http://cern.ch/geant4/license . These * +// * include a list of copyright holders. * +// * * +// * Neither the authors of this software system, nor their employing * +// * institutes, nor the agencies providing financial support for this * +// * work make any representation or warranty, express or implied, * +// * regarding this software system or assume any liability for its * +// * use. Please see the license in the file LICENSE and URL above * +// * for the full disclaimer and the limitation of liability. * +// * * +// * This code implementation is the result of the scientific and * +// * technical work of the GEANT4 collaboration. * +// * By using, copying, modifying or distributing the software (or * +// * any work based on the software) you agree to acknowledge its * +// * use in resulting scientific publications, and indicate your * +// * acceptance of all terms of the Geant4 Software license. * +// ******************************************************************** +// +/// \file HadronicGenerator.cc +/// \brief Implementation of the HadronicGenerator class +// +//------------------------------------------------------------------------ +// Class: HadronicGenerator +// Author: Alberto Ribon (CERN EP/SFT) +// Date: May 2020 +//------------------------------------------------------------------------ + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +#include "HadronicGenerator-vpgtle.hh" + +#include +#include +#include + +#include "G4AblaInterface.hh" +#include "G4BGGNucleonInelasticXS.hh" +#include "G4BinaryCascade.hh" +#include "G4BinaryLightIonReaction.hh" +#include "G4Box.hh" +#include "G4CascadeInterface.hh" +#include "G4ComponentGGHadronNucleusXsc.hh" +#include "G4ComponentGGNuclNuclXsc.hh" +#include "G4CrossSectionDataStore.hh" +#include "G4CrossSectionInelastic.hh" +#include "G4DecayPhysics.hh" +#include "G4DynamicParticle.hh" +#include "G4ExcitationHandler.hh" +#include "G4ExcitedStringDecay.hh" +#include "G4FTFModel.hh" +#include "G4GeneratorPrecompoundInterface.hh" +#include "G4HadronInelasticProcess.hh" +#include "G4HadronicInteraction.hh" +#include "G4HadronicParameters.hh" +#include "G4HadronicProcessStore.hh" +#include "G4INCLXXInterface.hh" +#include "G4IonTable.hh" +#include "G4LundStringFragmentation.hh" +#include "G4Material.hh" +#include "G4NeutronInelasticXS.hh" +#include "G4PVPlacement.hh" +#include "G4ParticleTable.hh" +#include "G4PhysicalConstants.hh" +#include "G4PreCompoundModel.hh" +#include "G4ProcessManager.hh" +#include "G4QGSMFragmentation.hh" +#include "G4QGSModel.hh" +#include "G4QGSParticipants.hh" +#include "G4QuasiElasticChannel.hh" +#include "G4StateManager.hh" +#include "G4Step.hh" +#include "G4SystemOfUnits.hh" +#include "G4TheoFSGenerator.hh" +#include "G4TouchableHistory.hh" +#include "G4TransportationManager.hh" +#include "G4UnitsTable.hh" +#include "G4VCrossSectionDataSet.hh" +#include "G4VParticleChange.hh" +#include "G4ios.hh" +#include "globals.hh" + +// particle libraries +#include "G4Alpha.hh" +#include "G4Deuteron.hh" +#include "G4He3.hh" +#include "G4Neutron.hh" +#include "G4Proton.hh" +#include "G4Triton.hh" + +// HP libraries +#include "G4NeutronCaptureProcess.hh" +#include "G4NeutronHPCapture.hh" +#include "G4NeutronHPCaptureData.hh" +#include "G4NeutronHPElastic.hh" +#include "G4NeutronHPElasticData.hh" +#include "G4NeutronHPFission.hh" +#include "G4NeutronHPFissionData.hh" +#include "G4NeutronHPInelastic.hh" +#include "G4NeutronHPInelasticXS.hh" + +// root libraries +#include "TH1D.h" +#include "TH2D.h" + +#include "G4Element.hh" + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +HadronicGenerator::HadronicGenerator(const G4String physicsCase) + : fPhysicsCase(physicsCase), fLastHadronicProcess(nullptr), + fPartTable(nullptr) { + + // The constructor set-ups all the particles, models, cross sections and + // hadronic inelastic processes. + // This should be done only once for each application. + // In the case of a multi-threaded application using this class, + // the constructor should be invoked for each thread, + // i.e. one instance of the class should be kept per thread. + // The particles and processes that are created in this constructor + // will then be used by the method GenerateInteraction at each interaction. + // Notes: + // - Neither the hadronic models nor the cross sections are used directly + // by the method GenerateInteraction, but they are associated to the + // hadronic processes and used by Geant4 to simulate the collision; + // - Although the class generates only final states, but not free mean paths, + // inelastic hadron-nuclear cross sections are needed by Geant4 to sample + // the target nucleus from the target material. + + if (fPhysicsCase != "QGSP_BIC_HP") { + G4cerr + << "ERROR: Not supported final-state hadronic inelastic physics case !" + << fPhysicsCase << G4endl + << "\t Re-try by choosing one of the following:" << G4endl + << "\t - Hadronic models : QGSP" << G4endl + << "\t - \"Physics-list proxies\" : QGSP_BIC_HP" << G4endl; + } + + // Definition of particles + G4GenericIon *gion = G4GenericIon::Definition(); + gion->SetProcessManager(new G4ProcessManager(gion)); + G4DecayPhysics *decays = new G4DecayPhysics; + decays->ConstructParticle(); + fPartTable = G4ParticleTable::GetParticleTable(); + fPartTable->SetReadiness(); + G4IonTable *ions = fPartTable->GetIonTable(); + ions->CreateAllIon(); + ions->CreateAllIsomer(); + + std::vector myElements; + + myElements.push_back(new G4Element("Hydrogen", "H", 1., 1.01 * g / mole)); + myElements.push_back(new G4Element("Helium", "He", 2., 4.00 * g / mole)); + myElements.push_back(new G4Element("Lithium", "Li", 3., 6.94 * g / mole)); + myElements.push_back(new G4Element("Beryllium", "Be", 4., 9.01 * g / mole)); + myElements.push_back(new G4Element("Boron", "B", 5., 10.81 * g / mole)); + myElements.push_back(new G4Element("Carbon", "C", 6., 12.01 * g / mole)); + myElements.push_back(new G4Element("Nitrogen", "N", 7., 14.01 * g / mole)); + myElements.push_back(new G4Element("Oxygen", "O", 8., 16.00 * g / mole)); + myElements.push_back(new G4Element("Fluorine", "F", 9., 19.00 * g / mole)); + myElements.push_back(new G4Element("Neon", "Ne", 10., 20.18 * g / mole)); + myElements.push_back(new G4Element("Sodium", "Na", 11., 22.99 * g / mole)); + myElements.push_back(new G4Element("Magnesium", "Mg", 12., 24.31 * g / mole)); + myElements.push_back(new G4Element("Aluminium", "Al", 13., 26.98 * g / mole)); + myElements.push_back(new G4Element("Silicon", "Si", 14., 28.09 * g / mole)); + myElements.push_back(new G4Element("Phosphorus", "P", 15., 30.97 * g / mole)); + myElements.push_back(new G4Element("Sulfur", "S", 16., 32.07 * g / mole)); + myElements.push_back(new G4Element("Chlorine", "Cl", 17., 35.45 * g / mole)); + myElements.push_back(new G4Element("Argon", "Ar", 18., 39.95 * g / mole)); + myElements.push_back(new G4Element("Potassium", "K", 19., 39.10 * g / mole)); + myElements.push_back(new G4Element("Calcium", "Ca", 20., 40.08 * g / mole)); + + myElements.push_back(new G4Element("Titanium", "Ti", 22., 47.87 * g / mole)); + + myElements.push_back(new G4Element("Copper", "Cu", 29., 63.55 * g / mole)); + myElements.push_back(new G4Element("Zinc", "Zn", 30., 65.38 * g / mole)); + + myElements.push_back(new G4Element("Silver", "Ag", 47., 107.87 * g / mole)); + + myElements.push_back(new G4Element("Tin", "Sn", 50., 118.71 * g / mole)); + + // Build BIC model + G4BinaryCascade *theBICmodel = new G4BinaryCascade; + G4PreCompoundModel *thePreEquilib = + new G4PreCompoundModel(new G4ExcitationHandler); + theBICmodel->SetDeExcitation(thePreEquilib); + + // Build BinaryLightIon model + G4PreCompoundModel *thePreEquilibBis = + new G4PreCompoundModel(new G4ExcitationHandler); + G4BinaryLightIonReaction *theIonBICmodel = + new G4BinaryLightIonReaction(thePreEquilibBis); + + // HP model + G4NeutronHPInelastic *theNeutronHP = new G4NeutronHPInelastic; + theNeutronHP->BuildPhysicsTable(*G4Neutron::Neutron()); + // G4NeutronHPCapture* theNeutronHPCapture = new G4NeutronHPCapture; + // theNeutronHPCapture->BuildPhysicsTable(*G4Neutron::Neutron()); + + // Model instance with constraint to be above a kinetic energy threshold. + // (Used for ions in all physics lists) + + G4GeneratorPrecompoundInterface *theCascade = + new G4GeneratorPrecompoundInterface; + theCascade->SetDeExcitation(thePreEquilib); + G4LundStringFragmentation *theLundFragmentation = + new G4LundStringFragmentation; + G4ExcitedStringDecay *theStringDecay = + new G4ExcitedStringDecay(theLundFragmentation); + G4FTFModel *theStringModel = new G4FTFModel; + theStringModel->SetFragmentationModel(theStringDecay); + + G4TheoFSGenerator *theFTFPmodel_aboveThreshold = + new G4TheoFSGenerator("FTFP"); + theFTFPmodel_aboveThreshold->SetMaxEnergy( + G4HadronicParameters::Instance()->GetMaxEnergy()); + theFTFPmodel_aboveThreshold->SetTransport(theCascade); + theFTFPmodel_aboveThreshold->SetHighEnergyGenerator(theStringModel); + + // Model instance with constraint to be within two kinetic energy thresholds. + // (Used in the case of QGS-based physics lists for nucleons) + + G4TheoFSGenerator *theFTFPmodel_constrained = new G4TheoFSGenerator("FTFP"); + theFTFPmodel_constrained->SetMaxEnergy( + G4HadronicParameters::Instance()->GetMaxEnergy()); + theFTFPmodel_constrained->SetTransport(theCascade); + theFTFPmodel_constrained->SetHighEnergyGenerator(theStringModel); + + // Build the QGSP model + G4TheoFSGenerator *theQGSPmodel = new G4TheoFSGenerator("QGSP"); + theQGSPmodel->SetMaxEnergy(G4HadronicParameters::Instance()->GetMaxEnergy()); + theQGSPmodel->SetTransport(theCascade); + G4QGSMFragmentation *theQgsmFragmentation = new G4QGSMFragmentation; + G4ExcitedStringDecay *theQgsmStringDecay = + new G4ExcitedStringDecay(theQgsmFragmentation); + G4VPartonStringModel *theQgsmStringModel = new G4QGSModel; + theQgsmStringModel->SetFragmentationModel(theQgsmStringDecay); + theQGSPmodel->SetHighEnergyGenerator(theQgsmStringModel); + G4QuasiElasticChannel *theQuasiElastic = + new G4QuasiElasticChannel; // QGSP uses quasi-elastic + theQGSPmodel->SetQuasiElasticChannel(theQuasiElastic); + + // For the case of "physics-list proxies", select the energy range for each + // hadronic model. + + const G4double ftfpMinE = + G4HadronicParameters::Instance()->GetMinEnergyTransitionFTF_Cascade(); + const G4double BICMaxE = + G4HadronicParameters::Instance()->GetMaxEnergyTransitionFTF_Cascade(); + const G4double ftfpMaxE = + G4HadronicParameters::Instance()->GetMaxEnergyTransitionQGS_FTF(); + const G4double qgspMinE = + G4HadronicParameters::Instance()->GetMinEnergyTransitionQGS_FTF(); + + theBICmodel->SetMaxEnergy(BICMaxE); + theIonBICmodel->SetMaxEnergy(BICMaxE); + theFTFPmodel_aboveThreshold->SetMinEnergy(ftfpMinE); + theFTFPmodel_constrained->SetMinEnergy(ftfpMinE); + theFTFPmodel_constrained->SetMaxEnergy(ftfpMaxE); + theQGSPmodel->SetMinEnergy(qgspMinE); + + // Cross sections (needed by Geant4 to sample the target nucleus from the + // target material) + + G4VCrossSectionDataSet *theProtonXSdata = + new G4BGGNucleonInelasticXS(G4Proton::Proton()); + theProtonXSdata->BuildPhysicsTable(*(G4Proton::Definition())); + + G4VCrossSectionDataSet *theNeutronXSdata = new G4NeutronInelasticXS; + theNeutronXSdata->BuildPhysicsTable(*(G4Neutron::Definition())); + + G4VCrossSectionDataSet *theNeutronHPXSdatainel = new G4NeutronHPInelasticXS; + theNeutronHPXSdatainel->BuildPhysicsTable(*G4Neutron::Definition()); + + // G4VCrossSectionDataSet* theNeutronHPCapturedata = new + // G4NeutronHPCaptureData; + // theNeutronHPCapturedata->BuildPhysicsTable(*G4Neutron::Definition()); + + G4VCrossSectionDataSet *theHyperonsXSdata = + new G4CrossSectionInelastic(new G4ComponentGGHadronNucleusXsc); + + G4VCrossSectionDataSet *theNuclNuclXSdata = + new G4CrossSectionInelastic(new G4ComponentGGNuclNuclXsc); + + // Set up inelastic processes : store them in a map (with particle definition + // as key) + // for convenience + + typedef std::pair ProcessPair; + + G4HadronicProcess *theProtonInelasticProcess = + new G4HadronInelasticProcess("protonInelastic", G4Proton::Definition()); + fProcessMap.insert( + ProcessPair(G4Proton::Definition(), theProtonInelasticProcess)); + fProcessMapHP.insert( + ProcessPair(G4Proton::Definition(), theProtonInelasticProcess)); + + G4HadronicProcess *theNeutronHPInelasticProcess = + new G4HadronInelasticProcess("NeutronHPInelastic", + G4Neutron::Definition()); + fProcessMapHP.insert( + ProcessPair(G4Neutron::Definition(), theNeutronHPInelasticProcess)); + + G4HadronicProcess *theNeutronInelasticProcess = + new G4HadronInelasticProcess("neutronInelastic", G4Neutron::Definition()); + fProcessMap.insert( + ProcessPair(G4Neutron::Definition(), theNeutronInelasticProcess)); + + // For the HP model, we need to create a new process for the neutron : + // interaction that are considered as relevant are the capture and fission (+ + // inelastic HP process neutron -nucleus) + // Prompt-gamma timing with a track-length estimator in proton therapy, JM + // Létang et al, 2024 + + G4HadronicProcess *theDeuteronInelasticProcess = + new G4HadronInelasticProcess("dInelastic", G4Deuteron::Definition()); + fProcessMap.insert( + ProcessPair(G4Deuteron::Definition(), theDeuteronInelasticProcess)); + fProcessMapHP.insert( + ProcessPair(G4Deuteron::Definition(), theDeuteronInelasticProcess)); + + G4HadronicProcess *theTritonInelasticProcess = + new G4HadronInelasticProcess("tInelastic", G4Triton::Definition()); + fProcessMap.insert( + ProcessPair(G4Triton::Definition(), theTritonInelasticProcess)); + fProcessMapHP.insert( + ProcessPair(G4Triton::Definition(), theTritonInelasticProcess)); + + G4HadronicProcess *theHe3InelasticProcess = + new G4HadronInelasticProcess("he3Inelastic", G4He3::Definition()); + fProcessMap.insert(ProcessPair(G4He3::Definition(), theHe3InelasticProcess)); + fProcessMapHP.insert( + ProcessPair(G4He3::Definition(), theHe3InelasticProcess)); + + G4HadronicProcess *theAlphaInelasticProcess = + new G4HadronInelasticProcess("alphaInelastic", G4Alpha::Definition()); + fProcessMap.insert( + ProcessPair(G4Alpha::Definition(), theAlphaInelasticProcess)); + fProcessMapHP.insert( + ProcessPair(G4Alpha::Definition(), theAlphaInelasticProcess)); + + G4HadronicProcess *theIonInelasticProcess = + new G4HadronInelasticProcess("ionInelastic", G4GenericIon::Definition()); + fProcessMap.insert( + ProcessPair(G4GenericIon::Definition(), theIonInelasticProcess)); + fProcessMapHP.insert( + ProcessPair(G4GenericIon::Definition(), theIonInelasticProcess)); + + // Add the cross sections to the corresponding hadronic processes + + // cross-sections for nucleons + theProtonInelasticProcess->AddDataSet(theProtonXSdata); + theNeutronInelasticProcess->AddDataSet(theNeutronXSdata); + + // specific cross-sections for neutrons in HP model + theNeutronHPInelasticProcess->AddDataSet(theNeutronHPXSdatainel); + // theNeutronHPInelasticProcess->AddDataSet( theNeutronHPCapturedata ); + + // cross-sections for ions + theDeuteronInelasticProcess->AddDataSet(theNuclNuclXSdata); + theTritonInelasticProcess->AddDataSet(theNuclNuclXSdata); + theHe3InelasticProcess->AddDataSet(theNuclNuclXSdata); + theAlphaInelasticProcess->AddDataSet(theNuclNuclXSdata); + theIonInelasticProcess->AddDataSet(theNuclNuclXSdata); + + // Register the proper hadronic model(s) to the corresponding hadronic + // processes. in the physics list QGSP_BIC, BIC is used only for nucleons + + // processes for BIC => mean-energy on nucleons + theProtonInelasticProcess->RegisterMe(theBICmodel); + theNeutronInelasticProcess->RegisterMe(theBICmodel); + + // processes for QGSP => High-energy on nucleons + theProtonInelasticProcess->RegisterMe(theQGSPmodel); + theNeutronInelasticProcess->RegisterMe(theQGSPmodel); + + // processes for HP => low-energy on neutrons + theNeutronHPInelasticProcess->RegisterMe(theNeutronHP); + // theNeutronHPInelasticProcess->RegisterMe( theNeutronHPCapture ); + + // processes for ion (BIC for ions) + theDeuteronInelasticProcess->RegisterMe(theIonBICmodel); + theTritonInelasticProcess->RegisterMe(theIonBICmodel); + theHe3InelasticProcess->RegisterMe(theIonBICmodel); + theAlphaInelasticProcess->RegisterMe(theIonBICmodel); + theIonInelasticProcess->RegisterMe(theIonBICmodel); + + G4TheoFSGenerator *theFTFPmodelToBeUsed = theFTFPmodel_aboveThreshold; + + theFTFPmodelToBeUsed = theFTFPmodel_constrained; + theNeutronInelasticProcess->RegisterMe(theFTFPmodelToBeUsed); + theProtonInelasticProcess->RegisterMe(theFTFPmodelToBeUsed); + + theFTFPmodelToBeUsed = theFTFPmodel_aboveThreshold; + theDeuteronInelasticProcess->RegisterMe(theFTFPmodelToBeUsed); + theTritonInelasticProcess->RegisterMe(theFTFPmodelToBeUsed); + theHe3InelasticProcess->RegisterMe(theFTFPmodelToBeUsed); + theAlphaInelasticProcess->RegisterMe(theFTFPmodelToBeUsed); + theIonInelasticProcess->RegisterMe(theFTFPmodelToBeUsed); +} + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +HadronicGenerator::~HadronicGenerator() { fPartTable->DeleteAllParticles(); } + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +G4int HadronicGenerator::GenerateInteraction( + G4ParticleDefinition *projectileDefinition, G4Material *targetMaterial, + TH2D *TH2D_EpEpg, TH1D *TH1D_SigmaInelastic, TH2D *TH2D_GammaZ, + TH1D *TH1D_NrPG) { + // This is the most important method of the HadronicGenerator class: + // the method performs the specified hadronic interaction + // (by invoking the "PostStepDoIt" method of the corresponding hadronic + // process) and returns the final state, i.e. the secondaries produced by the + // collision. It is a relatively short method because the heavy load of + // setting up all possible hadronic processes - with their hadronic models, + // transition regions, and cross sections (the latter is needed for sampling + // the target nucleus from the target material) - was already done by the + // constructor of the class. + G4VParticleChange *aChange = nullptr; + G4int nbPG(0); + + if (projectileDefinition == nullptr) { + G4cerr << "ERROR: projectileDefinition is NULL !" << G4endl; + return 0; + } + + const G4Element *targetElement = targetMaterial->GetElement(0); // + G4double molarMass = targetElement->GetA() / (g / mole); + + // Geometry definition (not strictly needed) + const G4double dimX = 1.0 * mm; + const G4double dimY = 1.0 * mm; + const G4double dimZ = 1.0 * mm; + G4Box *sFrame = new G4Box("Box", dimX, dimY, dimZ); + G4LogicalVolume *lFrame = + new G4LogicalVolume(sFrame, targetMaterial, "Box", 0, 0, 0); + G4PVPlacement *pFrame = + new G4PVPlacement(0, G4ThreeVector(), "Box", lFrame, 0, false, 0); + G4TransportationManager::GetTransportationManager()->SetWorldForTracking( + pFrame); + + G4int numCollisions = 1e1; //***LOOKHERE*** NUMBER OF COLLISIONS + G4double rnd1(0); + G4double protonEnergy(0), GAMMAZPerRho(0), kappaPerRho(0), + kappaPerRhoMmCollisions(0); + G4int Ngamma(0); + G4double projectileEnergy; + G4ThreeVector projectileDirection = G4ThreeVector(0.0, 0.0, 1.0); + G4double CrossSection = 0; + + G4double energyStart = TH1D_SigmaInelastic->GetBinCenter(1) * CLHEP::MeV; + G4int nbBins = TH1D_SigmaInelastic->GetNbinsX(); + G4double energyEnd = TH1D_SigmaInelastic->GetBinCenter(nbBins) * CLHEP::MeV; + G4double energyIncrement = TH1D_SigmaInelastic->GetBinWidth(1) * CLHEP::MeV; + + // G4cout << energyStart << " " << energyEnd << " "<< nbBins << " " << + // energyIncrement << G4endl; + + // Loop on the energies + for (G4int f = 1; f <= nbBins; f++) { + + Ngamma = 0; + + // Projectile track & step + G4DynamicParticle *dParticle = new G4DynamicParticle( + projectileDefinition, projectileDirection, energyEnd); + const G4double aTime = 0.0; + const G4ThreeVector aPosition = G4ThreeVector(0.0, 0.0, 0.0); + gTrack = new G4Track(dParticle, aTime, aPosition); + + G4Step *step = new G4Step; + + step->SetTrack(gTrack); + gTrack->SetStep(step); + aPoint = new G4StepPoint; + + aPoint->SetPosition(aPosition); + aPoint->SetMaterial(targetMaterial); + step->SetPreStepPoint(aPoint); + gTrack->SetStep(step); + + for (G4int i = 0; i < numCollisions; ++i) { + + projectileEnergy = (TH1D_SigmaInelastic->GetBinCenter(f) + + (G4UniformRand() - 0.5) * energyIncrement) * + CLHEP::MeV; + // G4cout << "projectile energy = " << projectileEnergy << G4endl; + dParticle->SetKineticEnergy(projectileEnergy); + gTrack->SetKineticEnergy(projectileEnergy); + + // Loop on the collisions + + G4HadronicProcess *theProcess = nullptr; + G4ParticleDefinition *theProjectileDef = nullptr; + if (projectileDefinition->IsGeneralIon()) { + theProjectileDef = G4GenericIon::Definition(); + } else { + theProjectileDef = projectileDefinition; + } + if (projectileDefinition == G4Neutron::Neutron() && + projectileEnergy < 20 * MeV && projectileEnergy > 4.4 * MeV) { + // For the HP model, we use the neutron HP inelastic process + auto mapIndex = fProcessMapHP.find(theProjectileDef); + if (mapIndex != fProcessMapHP.end()) + theProcess = mapIndex->second; + if (theProcess == nullptr) { + G4cerr << "ERROR: theProcess is nullptr for neutron HP inelastic !" + << G4endl; + } + } else { + auto mapIndex = fProcessMap.find(theProjectileDef); + + if (mapIndex != fProcessMap.end()) + theProcess = mapIndex->second; + if (theProcess == nullptr) { + G4cerr << "ERROR: theProcess is nullptr for " + << theProjectileDef->GetParticleName() << " inelastic !" + << G4endl; + } + } + if (theProcess != nullptr) { + aChange = theProcess->PostStepDoIt(*gTrack, *step); + } else { + G4cerr << "ERROR: theProcess is nullptr !" << G4endl; + } + fLastHadronicProcess = theProcess; + CrossSection = GetCrossSection(dParticle, targetElement, targetMaterial); + kappaPerRho = (CrossSection * Avogadro) / molarMass; + // The assumption in the prompt-gamma TLE is to use the defaut distance + // unit, ie mm + // ($G4_DIR/src/source/externals/clhep/include/CLHEP/Units/SystemOfUnits.h) + kappaPerRhoMmCollisions = kappaPerRho / (numCollisions * cm); + + G4int nsec = aChange ? aChange->GetNumberOfSecondaries() : 0; + + // Loop over produced secondaries and eventually print out some + // information. + for (G4int j = 0; j < nsec; ++j) { + const G4DynamicParticle *sec = + aChange->GetSecondary(j)->GetDynamicParticle(); + const G4String g = sec->GetDefinition()->GetParticleName(); + Double_t E = 0; + + if (g == "gamma") { + Ngamma++; + nbPG++; + E = sec->GetKineticEnergy(); + if (E > 0.04 * MeV) { + TH2D_GammaZ->Fill(projectileEnergy, E, kappaPerRhoMmCollisions); + TH2D_EpEpg->Fill(projectileEnergy, E); + } + } + delete aChange->GetSecondary(j); + } + if (aChange) + aChange->Clear(); + // delete dynamicProjectile; + } + + dParticle->SetKineticEnergy(TH1D_SigmaInelastic->GetBinCenter(f) * + CLHEP::MeV); + CrossSection = GetCrossSection(dParticle, targetElement, targetMaterial); + kappaPerRho = (CrossSection * Avogadro) / molarMass; + TH1D_SigmaInelastic->SetBinContent(f, kappaPerRho); + TH1D_NrPG->SetBinContent(f, Ngamma); + + delete step; + delete dParticle; + } + + delete pFrame; + delete lFrame; + delete sFrame; + + return nbPG; +} + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +G4double HadronicGenerator::GetCrossSection(const G4DynamicParticle *part, + const G4Element *elm, + const G4Material *targetMaterial) { + G4double CrossSection = -999; + G4HadronicProcess *hadProcess = GetHadronicProcess(); + const G4double squareCentimeter = cm * cm; + const G4ParticleDefinition *particle = part->GetParticleDefinition(); + + // Récupérer la section efficace + CrossSection = hadProcess->GetCrossSectionDataStore()->GetCrossSection( + part, elm, targetMaterial); + + return CrossSection / squareCentimeter; +} + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +G4String HadronicGenerator::WeightCompute(TH2D *TH2D_GammaZ, TH1D *TH1D_weight, + G4double frac) { + // This method computes the weight of the interaction for ToF computing + // and returns a string with the result. + // The weight is computed as the ratio of the number of gammas per bin + // to the number of collisions per bin. + G4String result = "is counted, with a fraction of " + std::to_string(frac); + + G4int nbBins = TH2D_GammaZ->GetNbinsX(); + for (G4int i = 1; i <= nbBins; ++i) { + // Sum the column at index i + G4double columnSum = + TH2D_GammaZ->Integral(i, i, 1, TH2D_GammaZ->GetNbinsY()); + // Store the sum in the TH1D histogram + TH1D_weight->SetBinContent(i, TH1D_weight->GetBinContent(i) + + frac * columnSum); + std::cout << "Column " << i << " sum: " << TH1D_weight->GetBinContent(i) + << std::endl; + } + + return result; +} + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... \ No newline at end of file diff --git a/opengate/contrib/vpgTLE/stage0/History b/opengate/contrib/vpgTLE/stage0/History new file mode 100644 index 000000000..0ed7de964 --- /dev/null +++ b/opengate/contrib/vpgTLE/stage0/History @@ -0,0 +1,31 @@ +# Example Hadr09 History + +See `CONTRIBUTING.rst` for details of **required** info/format for each entry, +which **must** added in reverse chronological order (newest at the top). It must **not** +be used as a substitute for writing good git commit messages! + + +## 2023-10-26 Alberto Ribon (exhadr09-V11-01-00) +- README : documented macro hadr09.in + +## 2022-11-17 Alberto Ribon (exhadr09-V11-00-01) +- Hadr09, HadronicGenerator : extended to hypernuclei and anti-hypernuclei + +## 2021-12-10 Ben Morgan (exhadr09-V11-00-00) +- Change to new Markdown History format + +--- + +# History entries prior to 11.0 + +10-11-21 Alberto Ribon (exhadr09-V10-07-01) +- HadronicGenerator : extended interface and functionality; + used new methods available in the FTF model. + +04-03-21 Alberto Ribon (exhadr09-V10-07-00) +- HadronicGenerator : migrated per-particle inelastic processes + (that have been deleted) to G4HadronInelasticProcess. + +08-11-20 Alberto Ribon (exhadr09-V10-06-00) +- Created this example. + diff --git a/opengate/contrib/vpgTLE/stage0/Modified_G4HP_File/G4ParticleHPInelastic.cc b/opengate/contrib/vpgTLE/stage0/Modified_G4HP_File/G4ParticleHPInelastic.cc new file mode 100644 index 000000000..173173f82 --- /dev/null +++ b/opengate/contrib/vpgTLE/stage0/Modified_G4HP_File/G4ParticleHPInelastic.cc @@ -0,0 +1,318 @@ +// +// ******************************************************************** +// * License and Disclaimer * +// * * +// * The Geant4 software is copyright of the Copyright Holders of * +// * the Geant4 Collaboration. It is provided under the terms and * +// * conditions of the Geant4 Software License, included in the file * +// * LICENSE and available at http://cern.ch/geant4/license . These * +// * include a list of copyright holders. * +// * * +// * Neither the authors of this software system, nor their employing * +// * institutes,nor the agencies providing financial support for this * +// * work make any representation or warranty, express or implied, * +// * regarding this software system or assume any liability for its * +// * use. Please see the license in the file LICENSE and URL above * +// * for the full disclaimer and the limitation of liability. * +// * * +// * This code implementation is the result of the scientific and * +// * technical work of the GEANT4 collaboration. * +// * By using, copying, modifying or distributing the software (or * +// * any work based on the software) you agree to acknowledge its * +// * use in resulting scientific publications, and indicate your * +// * acceptance of all terms of the Geant4 Software license. * +// ******************************************************************** +// +// this code implementation is the intellectual property of +// neutron_hp -- source file +// J.P. Wellisch, Nov-1996 +// A prototype of the low energy neutron transport model. +// +// By copying, distributing or modifying the Program (or any work +// based on the Program) you indicate your acceptance of this statement, +// and all its terms. +// +// +// 070523 bug fix for G4FPE_DEBUG on by A. Howard (and T. Koi) +// 081203 limit maximum trial for creating final states add protection for 1H +// isotope case by T. Koi +// +// P. Arce, June-2014 Conversion neutron_hp to particle_hp +// V. Ivanchenko, July-2023 Basic revision of particle HP classes +// +#include "G4ParticleHPInelastic.hh" + +#include "G4AutoLock.hh" +#include "G4HadronicParameters.hh" +#include "G4ParticleHP2AInelasticFS.hh" +#include "G4ParticleHP2N2AInelasticFS.hh" +#include "G4ParticleHP2NAInelasticFS.hh" +#include "G4ParticleHP2NDInelasticFS.hh" +#include "G4ParticleHP2NInelasticFS.hh" +#include "G4ParticleHP2NPInelasticFS.hh" +#include "G4ParticleHP2PInelasticFS.hh" +#include "G4ParticleHP3AInelasticFS.hh" +#include "G4ParticleHP3NAInelasticFS.hh" +#include "G4ParticleHP3NInelasticFS.hh" +#include "G4ParticleHP3NPInelasticFS.hh" +#include "G4ParticleHP4NInelasticFS.hh" +#include "G4ParticleHPAInelasticFS.hh" +#include "G4ParticleHPD2AInelasticFS.hh" +#include "G4ParticleHPDAInelasticFS.hh" +#include "G4ParticleHPDInelasticFS.hh" +#include "G4ParticleHPHe3InelasticFS.hh" +#include "G4ParticleHPManager.hh" +#include "G4ParticleHPN2AInelasticFS.hh" +#include "G4ParticleHPN2PInelasticFS.hh" +#include "G4ParticleHPN3AInelasticFS.hh" +#include "G4ParticleHPNAInelasticFS.hh" +#include "G4ParticleHPND2AInelasticFS.hh" +#include "G4ParticleHPNDInelasticFS.hh" +#include "G4ParticleHPNHe3InelasticFS.hh" +#include "G4ParticleHPNInelasticFS.hh" +#include "G4ParticleHPNPAInelasticFS.hh" +#include "G4ParticleHPNPInelasticFS.hh" +#include "G4ParticleHPNT2AInelasticFS.hh" +#include "G4ParticleHPNTInelasticFS.hh" +#include "G4ParticleHPNXInelasticFS.hh" +#include "G4ParticleHPPAInelasticFS.hh" +#include "G4ParticleHPPDInelasticFS.hh" +#include "G4ParticleHPPInelasticFS.hh" +#include "G4ParticleHPPTInelasticFS.hh" +#include "G4ParticleHPT2AInelasticFS.hh" +#include "G4ParticleHPTInelasticFS.hh" +#include "G4ParticleHPThermalBoost.hh" +#include "G4SystemOfUnits.hh" + +G4bool G4ParticleHPInelastic::fLock[] = {true, true, true, true, true, true}; +std::vector *G4ParticleHPInelastic::theInelastic[] = + {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; + +namespace { +G4Mutex theHPInelastic = G4MUTEX_INITIALIZER; +} + +G4ParticleHPInelastic::G4ParticleHPInelastic(G4ParticleDefinition *p, + const char *name) + : G4HadronicInteraction(name), theProjectile(p) { + fManager = G4ParticleHPManager::GetInstance(); + dirName = fManager->GetParticleHPPath(theProjectile) + "/Inelastic"; + indexP = fManager->GetPHPIndex(theProjectile); +#ifdef G4VERBOSE + if (fManager->GetVerboseLevel() > 1) + G4cout << "@@@ G4ParticleHPInelastic instantiated for " + << p->GetParticleName() << " indexP=" << indexP + << "/n data directory " << dirName << G4endl; +#endif +} + +G4ParticleHPInelastic::~G4ParticleHPInelastic() { + // Vector is shared, only one delete + if (isFirst) { + ClearData(); + } +} + +void G4ParticleHPInelastic::ClearData() { + if (theInelastic[indexP] != nullptr) { + for (auto const &p : *(theInelastic[indexP])) { + delete p; + } + delete theInelastic[indexP]; + theInelastic[indexP] = nullptr; + } +} + +G4HadFinalState * +G4ParticleHPInelastic::ApplyYourself(const G4HadProjectile &aTrack, + G4Nucleus &aNucleus) { + G4ParticleHPManager::GetInstance()->OpenReactionWhiteBoard(); + const G4Material *theMaterial = aTrack.GetMaterial(); + auto n = (G4int)theMaterial->GetNumberOfElements(); + auto elm = theMaterial->GetElement(0); + std::size_t index = elm->GetIndex(); + G4int it = 0; + if (n != 1) { + auto xSec = new G4double[n]; + G4double sum = 0; + G4int i; + const G4double *NumAtomsPerVolume = theMaterial->GetVecNbOfAtomsPerVolume(); + G4double rWeight; + G4double xs; + G4ParticleHPThermalBoost aThermalE; + for (i = 0; i < n; ++i) { + elm = theMaterial->GetElement(i); + index = elm->GetIndex(); + + G4cout << "i=" << i << " index=" << index << " " << elm->GetName() + << " " << (*(theInelastic[indexP]))[index] << G4endl; + + rWeight = NumAtomsPerVolume[i]; + if (aTrack.GetDefinition() == G4Neutron::Neutron()) { + xs = (*(theInelastic[indexP]))[index]->GetXsec( + aThermalE.GetThermalEnergy(aTrack, elm, + theMaterial->GetTemperature())); + } else { + xs = (*(theInelastic[indexP]))[index]->GetXsec( + aTrack.GetKineticEnergy()); + } + xs *= rWeight; + sum += xs; + xSec[i] = sum; +#ifdef G4VERBOSE + if (fManager->GetDEBUG()) + G4cout << " G4ParticleHPInelastic XSEC ELEM " << i << " = " << xSec[i] + << G4endl; +#endif + } + sum *= G4UniformRand(); + for (it = 0; it < n; ++it) { + elm = theMaterial->GetElement(it); + index = elm->GetIndex(); + if (sum <= xSec[it]) + break; + } + delete[] xSec; + } +#ifdef G4VERBOSE + if (fManager->GetDEBUG()) + G4cout << " G4ParticleHPInelastic: Elem it=" << it << " " << elm->GetName() + << " index=" << index << " from material " << theMaterial->GetName() + << G4endl; +#endif + // a specific flag has been builded in order to deal with the case of the HP + // in stage 0. The problem rise with the method GetIndex() that returns the + // ElementTable size while the position of the element is needed. + if (GetHadrGenFlag()) { + G4double Z = elm->GetZ(); + if (Z <= 20) { + index = Z - 1; // Adjust index for elements with Z <= 22 + } + if (Z == 22) { + index = 20; + } + if (Z == 29) { + index = 21; + } + if (Z == 30) { + index = 22; + } + if (Z == 47) { + index = 23; + } + if (Z == 50) { + index = 24; + } + } + + G4HadFinalState *result = + (*(theInelastic[indexP]))[index]->ApplyYourself(elm, aTrack); + aNucleus.SetParameters(fManager->GetReactionWhiteBoard()->GetTargA(), + fManager->GetReactionWhiteBoard()->GetTargZ()); + const G4Element *target_element = (*G4Element::GetElementTable())[index]; + const G4Isotope *target_isotope = nullptr; + auto iele = (G4int)target_element->GetNumberOfIsotopes(); + for (G4int j = 0; j != iele; ++j) { + target_isotope = target_element->GetIsotope(j); + if (target_isotope->GetN() == fManager->GetReactionWhiteBoard()->GetTargA()) + break; + } + aNucleus.SetIsotope(target_isotope); + + G4ParticleHPManager::GetInstance()->CloseReactionWhiteBoard(); + + return result; +} + +const std::pair +G4ParticleHPInelastic::GetFatalEnergyCheckLevels() const { + // max energy non-conservation is mass of heavy nucleus + return std::pair(10.0 * perCent, 350.0 * CLHEP::GeV); +} + +void G4ParticleHPInelastic::BuildPhysicsTable( + const G4ParticleDefinition &projectile) { + if (fLock[indexP]) { + G4AutoLock l(&theHPInelastic); + if (fLock[indexP]) { + isFirst = true; + fLock[indexP] = false; + } + l.unlock(); + } + G4int nelm = (G4int)G4Element::GetNumberOfElements(); + G4int n0 = numEle; + numEle = nelm; + if (!isFirst || nelm == n0) { + return; + } + // extra elements should be initialized + G4AutoLock l(&theHPInelastic); + + if (nullptr == theInelastic[indexP]) { + theInelastic[indexP] = new std::vector; + } + + if (fManager->GetVerboseLevel() > 0 && isFirst) { + fManager->DumpSetting(); + G4cout << "@@@ G4ParticleHPInelastic instantiated for particle " + << theProjectile->GetParticleName() << "/n data directory is " + << dirName << G4endl; + } + + auto table = G4Element::GetElementTable(); + for (G4int i = n0; i < nelm; ++i) { + auto clist = new G4ParticleHPChannelList(36, theProjectile); + theInelastic[indexP]->push_back(clist); + clist->Init((*table)[i], dirName, theProjectile); + clist->Register(new G4ParticleHPNInelasticFS, "F01/"); // has + clist->Register(new G4ParticleHPNXInelasticFS, "F02/"); + clist->Register(new G4ParticleHP2NDInelasticFS, "F03/"); + clist->Register(new G4ParticleHP2NInelasticFS, "F04/"); // has, E Done + clist->Register(new G4ParticleHP3NInelasticFS, "F05/"); // has, E Done + clist->Register(new G4ParticleHPNAInelasticFS, "F06/"); + clist->Register(new G4ParticleHPN3AInelasticFS, "F07/"); + clist->Register(new G4ParticleHP2NAInelasticFS, "F08/"); + clist->Register(new G4ParticleHP3NAInelasticFS, "F09/"); + clist->Register(new G4ParticleHPNPInelasticFS, "F10/"); + clist->Register(new G4ParticleHPN2AInelasticFS, "F11/"); + clist->Register(new G4ParticleHP2N2AInelasticFS, "F12/"); + clist->Register(new G4ParticleHPNDInelasticFS, "F13/"); + clist->Register(new G4ParticleHPNTInelasticFS, "F14/"); + clist->Register(new G4ParticleHPNHe3InelasticFS, "F15/"); + clist->Register(new G4ParticleHPND2AInelasticFS, "F16/"); + clist->Register(new G4ParticleHPNT2AInelasticFS, "F17/"); + clist->Register(new G4ParticleHP4NInelasticFS, "F18/"); // has, E Done + clist->Register(new G4ParticleHP2NPInelasticFS, "F19/"); + clist->Register(new G4ParticleHP3NPInelasticFS, "F20/"); + clist->Register(new G4ParticleHPN2PInelasticFS, "F21/"); + clist->Register(new G4ParticleHPNPAInelasticFS, "F22/"); + clist->Register(new G4ParticleHPPInelasticFS, "F23/"); + clist->Register(new G4ParticleHPDInelasticFS, "F24/"); + clist->Register(new G4ParticleHPTInelasticFS, "F25/"); + clist->Register(new G4ParticleHPHe3InelasticFS, "F26/"); + clist->Register(new G4ParticleHPAInelasticFS, "F27/"); + clist->Register(new G4ParticleHP2AInelasticFS, "F28/"); + clist->Register(new G4ParticleHP3AInelasticFS, "F29/"); + clist->Register(new G4ParticleHP2PInelasticFS, "F30/"); + clist->Register(new G4ParticleHPPAInelasticFS, "F31/"); + clist->Register(new G4ParticleHPD2AInelasticFS, "F32/"); + clist->Register(new G4ParticleHPT2AInelasticFS, "F33/"); + clist->Register(new G4ParticleHPPDInelasticFS, "F34/"); + clist->Register(new G4ParticleHPPTInelasticFS, "F35/"); + clist->Register(new G4ParticleHPDAInelasticFS, "F36/"); +#ifdef G4VERBOSE + if (fManager->GetVerboseLevel() > 1) { + G4cout << "ParticleHP::Inelastic for " << theProjectile->GetParticleName() + << " off " << (*table)[i]->GetName() << G4endl; + } +#endif + } + fManager->RegisterInelasticFinalStates(&projectile, theInelastic[indexP]); + l.unlock(); +} + +void G4ParticleHPInelastic::ModelDescription(std::ostream &outFile) const { + outFile << "High Precision (HP) model for inelastic reaction of " + << theProjectile->GetParticleName() << " below 20MeV\n"; +} diff --git a/opengate/contrib/vpgTLE/stage0/Modified_G4HP_File/G4ParticleHPInelastic.hh b/opengate/contrib/vpgTLE/stage0/Modified_G4HP_File/G4ParticleHPInelastic.hh new file mode 100644 index 000000000..686479e68 --- /dev/null +++ b/opengate/contrib/vpgTLE/stage0/Modified_G4HP_File/G4ParticleHPInelastic.hh @@ -0,0 +1,90 @@ +// +// ******************************************************************** +// * License and Disclaimer * +// * * +// * The Geant4 software is copyright of the Copyright Holders of * +// * the Geant4 Collaboration. It is provided under the terms and * +// * conditions of the Geant4 Software License, included in the file * +// * LICENSE and available at http://cern.ch/geant4/license . These * +// * include a list of copyright holders. * +// * * +// * Neither the authors of this software system, nor their employing * +// * institutes,nor the agencies providing financial support for this * +// * work make any representation or warranty, express or implied, * +// * regarding this software system or assume any liability for its * +// * use. Please see the license in the file LICENSE and URL above * +// * for the full disclaimer and the limitation of liability. * +// * * +// * This code implementation is the result of the scientific and * +// * technical work of the GEANT4 collaboration. * +// * By using, copying, modifying or distributing the software (or * +// * any work based on the software) you agree to acknowledge its * +// * use in resulting scientific publications, and indicate your * +// * acceptance of all terms of the Geant4 Software license. * +// ******************************************************************** +// +// +// Hadronic Process: High Precision low E neutron tracking +// original by H.P. Wellisch, TRIUMF, 14-Feb-97 +// Builds and has the Cross-section data for one material. +// +// P. Arce, June-2014 Conversion neutron_hp to particle_hp +// V. Ivanchenko, July-2023 Basic revision of particle HP classes +// + +#ifndef G4ParticleHPInelastic_h +#define G4ParticleHPInelastic_h 1 + +// Final state production model for a high precision (based on evaluated data +// libraries) description of neutron inelastic scattering below 20 MeV or +// light ion inelastic interaction below 100 MeV. +// 36 exclusive final states are consideded. + +#include "G4HadronicInteraction.hh" +#include "G4ParticleDefinition.hh" +#include "G4ParticleHPChannel.hh" +#include "G4ParticleHPChannelList.hh" +#include "globals.hh" + +class G4ParticleHPInelastic : public G4HadronicInteraction { +public: + G4ParticleHPInelastic(G4ParticleDefinition *p = G4Neutron::Neutron(), + const char *name = "NeutronHPInelastic"); + + ~G4ParticleHPInelastic() override; + + G4HadFinalState *ApplyYourself(const G4HadProjectile &aTrack, + G4Nucleus &aTargetNucleus) override; + + const std::pair + GetFatalEnergyCheckLevels() const override; + + inline bool GetHadrGenFlag() const { return HadrGen; } + + inline void SetHadrGenFlag(const bool b) { HadrGen = b; } + + void BuildPhysicsTable(const G4ParticleDefinition &) override; + void ModelDescription(std::ostream &outFile) const override; + + G4ParticleHPInelastic(G4ParticleHPInelastic &) = delete; + G4ParticleHPInelastic &operator=(const G4ParticleHPInelastic &right) = delete; + // flag to indicate if hadronic generator is used + G4bool HadrGen{false}; + +private: + void ClearData(); + + G4ParticleDefinition *theProjectile; + G4bool isFirst{false}; + static G4bool fLock[6]; + +protected: + // one List per element + static std::vector *theInelastic[6]; + G4ParticleHPManager *fManager; + G4String dirName; + G4int numEle{0}; + G4int indexP; +}; + +#endif diff --git a/opengate/contrib/vpgTLE/stage0/README b/opengate/contrib/vpgTLE/stage0/README new file mode 100644 index 000000000..5ac6ee631 --- /dev/null +++ b/opengate/contrib/vpgTLE/stage0/README @@ -0,0 +1,43 @@ +This example shows how to use Geant4 as a generator for simulating +inelastic hadron-nuclear interactions. + +The class HadronicGenerator is the "generator". +The main hadronic models (FTFP, QGSP, BERT, BIC, IonBIC, INCL) +and some combinations of two of them - in a transition energy region, +similarly to what happens in physics lists - are available. +See include/HadronicGenerator.hh for more detailed information. + +The main, Hadr09.cc, shows an example of how to use it. +It samples randomly the projectile hadron, its energy, its direction +and the target material, and then it calls the generator. +Some information regarding the secondaries which are produced can be +printed out. +See the comments in Hadr09.cc for more information and how eventually +to change some of its configurations. +Notice that Hadr09.cc does nothing really useful: users should consider +to use eventually only the class HadronicGenerator. + +( + The file Hadr09.cc-ION_PROJECTILE shows an example of a ion-ion + collision, for fixed type of projectile ion, target ion, projectile + kinetic energy, and projectile direction. + This file is obtained from Hadr09.cc with minimal changes. +) + +Notice that the Geant4 run-manager is not used. + +To build this example: + mkdir Build; cd Build + cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DGeant4_DIR=/path-to-geant4-libraries ../. + make + +To run it: + ./Hadr09 + +which simulates 1000 hadron-nucleus collisions, randomnly selected, and +prints out some information about the secondaries produced in these +interactions. It takes only a few seconds to run. + +Note: this example has been included in Geant4 10.7, but it should work + also for early versions of Geant4, in particular 10.6, 10.5 and 10.4. diff --git a/opengate/contrib/vpgTLE/stage0/include/HadronicGenerator-vpgtle.hh b/opengate/contrib/vpgTLE/stage0/include/HadronicGenerator-vpgtle.hh new file mode 100644 index 000000000..9de4363a7 --- /dev/null +++ b/opengate/contrib/vpgTLE/stage0/include/HadronicGenerator-vpgtle.hh @@ -0,0 +1,168 @@ +// +// ******************************************************************** +// * License and Disclaimer * +// * * +// * The Geant4 software is copyright of the Copyright Holders of * +// * the Geant4 Collaboration. It is provided under the terms and * +// * conditions of the Geant4 Software License, included in the file * +// * LICENSE and available at http://cern.ch/geant4/license . These * +// * include a list of copyright holders. * +// * * +// * Neither the authors of this software system, nor their employing * +// * institutes, nor the agencies providing financial support for this * +// * work make any representation or warranty, express or implied, * +// * regarding this software system or assume any liability for its * +// * use. Please see the license in the file LICENSE and URL above * +// * for the full disclaimer and the limitation of liability. * +// * * +// * This code implementation is the result of the scientific and * +// * technical work of the GEANT4 collaboration. * +// * By using, copying, modifying or distributing the software (or * +// * any work based on the software) you agree to acknowledge its * +// * use in resulting scientific publications, and indicate your * +// * acceptance of all terms of the Geant4 Software license. * +// ******************************************************************** +// +/// \file HadronicGenerator-vpgtle.hh +/// \brief Definition of the HadronicGenerator class +// +//------------------------------------------------------------------------ +// Class: HadronicGenerator +// Author: Alberto Ribon (CERN EP/SFT) +// Date: May 2020 +// +// This class shows how to use Geant4 as a generator for simulating +// inelastic hadron-nuclear interactions. +// Some of the most used hadronic models are currently supported in +// this class: +// - the hadronic string models Fritiof (FTF) and Quark-Gluon-String (QGS) +// coupled with Precompound/de-excitation +// - the intranuclear cascade models: Bertini (BERT), Binary Cascade (BIC), +// and Liege (INCL) +// Combinations of two models - in a transition energy interval, with a +// linear probability as a function of the energy - are also available to +// "mimic" the transition between hadronic models as in the most common +// Geant4 reference physics lists. +// +// The current version of this class does NOT support: +// - hadron elastic interactions +// - neutron capture and fission +// - precise low-energy inelastic interactions of neutrons and +// charged particles (i.e. ParticleHP) +// - gamma/lepton-nuclear inelastic interactions +// +// This class does NOT use the Geant4 run-manager, and therefore should +// be usable in a multi-threaded application, with one instance of this +// class in each thread. +// +// This class has been inspired by test30 (whose author is Vladimir +// Ivanchenko), with various simplifications and restricted to hadronic +// inelastic interactions. +//------------------------------------------------------------------------ + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +#ifndef HadronicGenerator_h +#define HadronicGenerator_h 1 + +#include "G4HadronicProcess.hh" +#include "G4ThreeVector.hh" +#include "G4ios.hh" +#include "globals.hh" +#include +#include +#include +#include + +class G4ParticleDefinition; +class G4VParticleChange; +class G4ParticleTable; +class G4Material; +class G4HadronicInteraction; + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +class HadronicGenerator { + // This class provides the functionality of a "hadronic generator" + // for Geant4 final-state inelastic hadronic collisions. + // Only a few of the available Geant4 final-state hadronic inelastic + // "physics cases" are currently available in this class - but it can + // be extended to other cases if needed. + // It is important to notice that this class does NOT use the Geant4 + // run-manager, so it should work fine in a multi-threaded environment, + // with a separate instance of this class in each thread. +public: + explicit HadronicGenerator(const G4String physicsCase = "QGSP_BIC_HP"); + // Currently supported final-state hadronic inelastic "physics cases": + // - Hadronic models : BERT, BIC, IonBIC, INCL, FTFP, QGSP + // - "Physics-list proxies" : FTFP_BERT_ATL (default), FTFP_BERT, + // QGSP_BERT, QGSP_BIC, FTFP_INCLXX + // (i.e. they are not real, complete physics lists - for instance + // they do not have: transportation, electromagnetic physics, + // hadron elastic scattering, neutron fission and capture, etc. - + // however, they cover all hadron types and all energies by + // combining different hadronic models, i.e. there are transitions + // between two hadronic models in well-defined energy intervals, + // e.g. "FTFP_BERT" has the transition between BERT and FTFP + // hadronic models; moreover, the transition intervals used in + // our "physics cases" might not be the same as in the corresponding + // physics lists). + + ~HadronicGenerator(); + + inline G4bool IsPhysicsCaseSupported() const; + // Returns "true" if the physicsCase is supported; "false" otherwise. + + G4int GenerateInteraction(G4ParticleDefinition *projectileDefinition, + G4Material *targetMaterial, TH2D *TH2D_EpEpg, + TH1D *TH1D_SigmaInelastic, TH2D *TH2D_GammaZ, + TH1D *NrPG, G4int numCollisions); + // This is the main method provided by the class: + // in input it receives the projectile (either by name or particle + // definition), its energy, its direction and the target material, and it + // returns one sampled final-state of the inelastic hadron-nuclear collision + // as modelled by the final-state hadronic inelastic "physics case" specified + // in the constructor. If the required hadronic collision is not possible, + // then the method returns immediately an empty "G4VParticleChange", i.e. + // without secondaries produced. + + inline G4HadronicProcess *GetHadronicProcess() const; + inline G4HadronicInteraction *GetHadronicInteraction() const; + // Returns the hadronic process and the hadronic interaction, respectively, + // that handled the last call of "GenerateInteraction". + + G4double GetCrossSection(const G4DynamicParticle *part, const G4Element *elm, + const G4Material *targetMaterial); + // In the case of hadronic interactions handled by the FTF model, returns, + // respectively, the impact parameter, the number of target/projectile + // spectator nucleons, and the number of nucleon-nucleon collisions, + // else, returns a negative value (-999). + + G4String WeightCompute(TH2D *TH2D_GammaZ, TH1D *TH1D_weight, G4double frac); + +private: + G4String fPhysicsCase; + G4bool fPhysicsCaseIsSupported; + G4HadronicProcess *fLastHadronicProcess; + G4ParticleTable *fPartTable; + std::map fProcessMap; + std::map fProcessMapHP; + G4Track *gTrack; + G4StepPoint *aPoint; +}; + +inline G4HadronicProcess *HadronicGenerator::GetHadronicProcess() const { + return fLastHadronicProcess; +} + +inline G4HadronicInteraction * +HadronicGenerator::GetHadronicInteraction() const { + return fLastHadronicProcess == nullptr + ? nullptr + : fLastHadronicProcess->GetHadronicInteraction(); +} + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +#endif diff --git a/opengate/contrib/vpgTLE/stage0/merge_root.py b/opengate/contrib/vpgTLE/stage0/merge_root.py new file mode 100644 index 000000000..f26badc1e --- /dev/null +++ b/opengate/contrib/vpgTLE/stage0/merge_root.py @@ -0,0 +1,98 @@ +import numpy as np +import glob +import os +import SimpleITK as sitk +import uproot +import hist + + +Path = "/sps/creatis/vguittet/geant-stage0/output" +folders = [d for d in glob.glob(os.path.join(Path, "*/"))] +Norm = len(folders) +case = "output_507312_" +print(Norm) +# recover all the mha files +listofFiles = glob.glob(Path + "/" + case + "*/neutr-data.root", recursive=True) +Norm = len(folders) + +X = np.linspace(0, 200, 501) +Y = np.linspace(0, 10, 251) + +elements = [ + "standard_Weight", + "Al", + "Ar", + "Be", + "B", + "Ca", + "C", + "Cl", + "Cu", + "F", + "He", + "H", + "Li", + "Mg", + "Ne", + "N", + "O", + "P", + "K", + "Si", + "Ag", + "Na", + "S", + "Sn", + "Ti", + "Zn", +] +quantity = ["GammaZ", "Kapa inelastique", "NrPG", "EnEpg"] + + +def accumulation(el, q): + if q == "GammaZ" or q == "EnEpg": + mega = np.zeros((500, 250)) + else: + mega = np.zeros(500) + for rootFile in listofFiles: + img = uproot.open(rootFile) + if q == "GammaZ" or q == "EnEpg": + arr, x, y = img[el][q].to_hist().to_numpy() + else: + arr, x = img[el][q].to_hist().to_numpy() + mega += arr / Norm + # vider l'arr + return mega + + +# concatenation of the data +with uproot.recreate("data_merge.root") as f: + for el in elements: + if el == "standard_Weight": + somme = accumulation(el, "Weight") + histo_data = hist.Hist( + hist.axis.Variable(X, name="Neutrons Energy (MeV)"), + storage=hist.storage.Double(), + ) + histo_data[...] = somme + f[f"{el}/Weight"] = histo_data + print(el) + continue + for q in quantity: + print(el, q) + somme = accumulation(el, q) + if q == "GammaZ" or q == "EnEpg": + histo_data = hist.Hist( + hist.axis.Variable(X, name="Neutrons Energy (MeV)"), + hist.axis.Variable(Y, name="PGs energy [MeV]"), + storage=hist.storage.Double(), + ) + histo_data[...] = somme + f[f"{el}/{q}"] = histo_data + else: + histo_data = hist.Hist( + hist.axis.Variable(X, name="Neutrons Energy (MeV)"), + storage=hist.storage.Double(), + ) + histo_data[...] = somme + f[f"{el}/{q}"] = histo_data diff --git a/opengate/contrib/vpgTLE/stage0/run_stage0.slurm b/opengate/contrib/vpgTLE/stage0/run_stage0.slurm new file mode 100644 index 000000000..d3b4ee8f8 --- /dev/null +++ b/opengate/contrib/vpgTLE/stage0/run_stage0.slurm @@ -0,0 +1,24 @@ +#! /usr/bin/bash -lx +#SBATCH --output=Seq%j.out # nom du fichier de sortie +#SBATCH --time=1-00:00:00 +#SBATCH --ntasks=1 +#SBATCH --mem=2000 # Mémoire en MB par défaut +#SBATCH --error=Seq%A_%a.out # nom du fichier d'erreur (ici en commun avec la sortie) + +# sbatch --export=N=10000,P=proton,W=weight run_stage0.slurm + +module load /pbs/software/modulefiles/redhat-9-x86_64/Compilers/gcc/13.2.0 +module load /pbs/software/modulefiles/redhat-9-x86_64/Analysis/root/6.30.06 +source /path/to/theG4used/geant4.11.2.1/bin/geant4make.sh + +cd /path/to/your/outputfolder/geant-stage0/output +mkdir output_${SLURM_ARRAY_JOB_ID}_${SLURM_ARRAY_TASK_ID} +N=${1:-10000} +P=${2:-proton} +W=${3:-""} + +cd /path/to/your/compilation/folder/bin +./stage0-vpgtle ${SLURM_ARRAY_JOB_ID}_${SLURM_ARRAY_TASK_ID} -n ${N} -p ${P} ${W} + + + diff --git a/opengate/contrib/vpgTLE/stage0/src/HadronicGenerator-vpgtle.cc b/opengate/contrib/vpgTLE/stage0/src/HadronicGenerator-vpgtle.cc new file mode 100644 index 000000000..6330db8f6 --- /dev/null +++ b/opengate/contrib/vpgTLE/stage0/src/HadronicGenerator-vpgtle.cc @@ -0,0 +1,605 @@ +// +// ******************************************************************** +// * License and Disclaimer * +// * * +// * The Geant4 software is copyright of the Copyright Holders of * +// * the Geant4 Collaboration. It is provided under the terms and * +// * conditions of the Geant4 Software License, included in the file * +// * LICENSE and available at http://cern.ch/geant4/license . These * +// * include a list of copyright holders. * +// * * +// * Neither the authors of this software system, nor their employing * +// * institutes, nor the agencies providing financial support for this * +// * work make any representation or warranty, express or implied, * +// * regarding this software system or assume any liability for its * +// * use. Please see the license in the file LICENSE and URL above * +// * for the full disclaimer and the limitation of liability. * +// * * +// * This code implementation is the result of the scientific and * +// * technical work of the GEANT4 collaboration. * +// * By using, copying, modifying or distributing the software (or * +// * any work based on the software) you agree to acknowledge its * +// * use in resulting scientific publications, and indicate your * +// * acceptance of all terms of the Geant4 Software license. * +// ******************************************************************** +// +/// \file HadronicGenerator.cc +/// \brief Implementation of the HadronicGenerator class +// +//------------------------------------------------------------------------ +// Class: HadronicGenerator +// Author: Alberto Ribon (CERN EP/SFT) +// Date: May 2020 +//------------------------------------------------------------------------ + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +#include "HadronicGenerator-vpgtle.hh" + +#include +#include +#include + +#include "G4AblaInterface.hh" +#include "G4BGGNucleonInelasticXS.hh" +#include "G4BinaryCascade.hh" +#include "G4BinaryLightIonReaction.hh" +#include "G4Box.hh" +#include "G4CascadeInterface.hh" +#include "G4ComponentGGHadronNucleusXsc.hh" +#include "G4ComponentGGNuclNuclXsc.hh" +#include "G4CrossSectionDataStore.hh" +#include "G4CrossSectionInelastic.hh" +#include "G4DecayPhysics.hh" +#include "G4DynamicParticle.hh" +#include "G4ExcitationHandler.hh" +#include "G4ExcitedStringDecay.hh" +#include "G4FTFModel.hh" +#include "G4GeneratorPrecompoundInterface.hh" +#include "G4HadronInelasticProcess.hh" +#include "G4HadronicInteraction.hh" +#include "G4HadronicParameters.hh" +#include "G4HadronicProcessStore.hh" +#include "G4INCLXXInterface.hh" +#include "G4IonTable.hh" +#include "G4LundStringFragmentation.hh" +#include "G4Material.hh" +#include "G4NeutronInelasticXS.hh" +#include "G4PVPlacement.hh" +#include "G4ParticleTable.hh" +#include "G4PhysicalConstants.hh" +#include "G4PreCompoundModel.hh" +#include "G4ProcessManager.hh" +#include "G4QGSMFragmentation.hh" +#include "G4QGSModel.hh" +#include "G4QGSParticipants.hh" +#include "G4QuasiElasticChannel.hh" +#include "G4StateManager.hh" +#include "G4Step.hh" +#include "G4SystemOfUnits.hh" +#include "G4TheoFSGenerator.hh" +#include "G4TouchableHistory.hh" +#include "G4TransportationManager.hh" +#include "G4UnitsTable.hh" +#include "G4VCrossSectionDataSet.hh" +#include "G4VParticleChange.hh" +#include "G4ios.hh" +#include "globals.hh" + +// particle libraries +#include "G4Alpha.hh" +#include "G4Deuteron.hh" +#include "G4He3.hh" +#include "G4Neutron.hh" +#include "G4Proton.hh" +#include "G4Triton.hh" + +// HP libraries +#include "G4NeutronCaptureProcess.hh" +#include "G4NeutronHPCapture.hh" +#include "G4NeutronHPCaptureData.hh" +#include "G4NeutronHPElastic.hh" +#include "G4NeutronHPElasticData.hh" +#include "G4NeutronHPFission.hh" +#include "G4NeutronHPFissionData.hh" +#include "G4NeutronHPInelastic.hh" +#include "G4NeutronHPInelasticXS.hh" +#include "G4ParticleHPInelastic.hh" + +// root libraries +#include "TH1D.h" +#include "TH2D.h" + +#include "G4Element.hh" + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +HadronicGenerator::HadronicGenerator(const G4String physicsCase) + : fPhysicsCase(physicsCase), fLastHadronicProcess(nullptr), + fPartTable(nullptr) { + + // The constructor set-ups all the particles, models, cross sections and + // hadronic inelastic processes. + // This should be done only once for each application. + // In the case of a multi-threaded application using this class, + // the constructor should be invoked for each thread, + // i.e. one instance of the class should be kept per thread. + // The particles and processes that are created in this constructor + // will then be used by the method GenerateInteraction at each interaction. + // Notes: + // - Neither the hadronic models nor the cross sections are used directly + // by the method GenerateInteraction, but they are associated to the + // hadronic processes and used by Geant4 to simulate the collision; + // - Although the class generates only final states, but not free mean paths, + // inelastic hadron-nuclear cross sections are needed by Geant4 to sample + // the target nucleus from the target material. + + if (fPhysicsCase != "QGSP_BIC_HP") { + G4cerr + << "ERROR: Not supported final-state hadronic inelastic physics case !" + << fPhysicsCase << G4endl + << "\t Re-try by choosing one of the following:" << G4endl + << "\t - Hadronic models : QGSP" << G4endl + << "\t - \"Physics-list proxies\" : QGSP_BIC_HP" << G4endl; + } + + // Definition of particles + G4GenericIon *gion = G4GenericIon::Definition(); + gion->SetProcessManager(new G4ProcessManager(gion)); + G4DecayPhysics *decays = new G4DecayPhysics; + decays->ConstructParticle(); + fPartTable = G4ParticleTable::GetParticleTable(); + fPartTable->SetReadiness(); + G4IonTable *ions = fPartTable->GetIonTable(); + ions->CreateAllIon(); + ions->CreateAllIsomer(); + + // Building the ElementTable for the NeutronHP model + // TO BE MODIFIED :: there should be a method in Geant4 for this purpose + std::vector myElements; + myElements.push_back(new G4Element("Hydrogen", "H", 1., 1.01 * g / mole)); + myElements.push_back(new G4Element("Helium", "He", 2., 4.00 * g / mole)); + myElements.push_back(new G4Element("Lithium", "Li", 3., 6.94 * g / mole)); + myElements.push_back(new G4Element("Beryllium", "Be", 4., 9.01 * g / mole)); + myElements.push_back(new G4Element("Boron", "B", 5., 10.81 * g / mole)); + myElements.push_back(new G4Element("Carbon", "C", 6., 12.01 * g / mole)); + myElements.push_back(new G4Element("Nitrogen", "N", 7., 14.01 * g / mole)); + myElements.push_back(new G4Element("Oxygen", "O", 8., 16.00 * g / mole)); + myElements.push_back(new G4Element("Fluorine", "F", 9., 19.00 * g / mole)); + myElements.push_back(new G4Element("Neon", "Ne", 10., 20.18 * g / mole)); + myElements.push_back(new G4Element("Sodium", "Na", 11., 22.99 * g / mole)); + myElements.push_back(new G4Element("Magnesium", "Mg", 12., 24.31 * g / mole)); + myElements.push_back(new G4Element("Aluminium", "Al", 13., 26.98 * g / mole)); + myElements.push_back(new G4Element("Silicon", "Si", 14., 28.09 * g / mole)); + myElements.push_back(new G4Element("Phosphorus", "P", 15., 30.97 * g / mole)); + myElements.push_back(new G4Element("Sulfur", "S", 16., 32.07 * g / mole)); + myElements.push_back(new G4Element("Chlorine", "Cl", 17., 35.45 * g / mole)); + myElements.push_back(new G4Element("Argon", "Ar", 18., 39.95 * g / mole)); + myElements.push_back(new G4Element("Potassium", "K", 19., 39.10 * g / mole)); + myElements.push_back(new G4Element("Calcium", "Ca", 20., 40.08 * g / mole)); + myElements.push_back(new G4Element("Titanium", "Ti", 22., 47.87 * g / mole)); + myElements.push_back(new G4Element("Copper", "Cu", 29., 63.55 * g / mole)); + myElements.push_back(new G4Element("Zinc", "Zn", 30., 65.38 * g / mole)); + myElements.push_back(new G4Element("Silver", "Ag", 47., 107.87 * g / mole)); + myElements.push_back(new G4Element("Tin", "Sn", 50., 118.71 * g / mole)); + + // Build BIC model + G4BinaryCascade *theBICmodel = new G4BinaryCascade; + G4PreCompoundModel *thePreEquilib = + new G4PreCompoundModel(new G4ExcitationHandler); + theBICmodel->SetDeExcitation(thePreEquilib); + + // Build BinaryLightIon model + G4PreCompoundModel *thePreEquilibBis = + new G4PreCompoundModel(new G4ExcitationHandler); + G4BinaryLightIonReaction *theIonBICmodel = + new G4BinaryLightIonReaction(thePreEquilibBis); + + // HP model + G4NeutronHPInelastic *theNeutronHP = new G4NeutronHPInelastic; + theNeutronHP->BuildPhysicsTable(*G4Neutron::Neutron()); + theNeutronHP->SetHadrGenFlag( + true); // Enable hadronic generator flag :: due to the ElementTable + // building default :: TO BE MODIFIED + + // Model instance with constraint to be above a kinetic energy threshold. + // (Used for ions in all physics lists) + G4GeneratorPrecompoundInterface *theCascade = + new G4GeneratorPrecompoundInterface; + theCascade->SetDeExcitation(thePreEquilib); + G4LundStringFragmentation *theLundFragmentation = + new G4LundStringFragmentation; + G4ExcitedStringDecay *theStringDecay = + new G4ExcitedStringDecay(theLundFragmentation); + G4FTFModel *theStringModel = new G4FTFModel; + theStringModel->SetFragmentationModel(theStringDecay); + + G4TheoFSGenerator *theFTFPmodel_aboveThreshold = + new G4TheoFSGenerator("FTFP"); + theFTFPmodel_aboveThreshold->SetMaxEnergy( + G4HadronicParameters::Instance()->GetMaxEnergy()); + theFTFPmodel_aboveThreshold->SetTransport(theCascade); + theFTFPmodel_aboveThreshold->SetHighEnergyGenerator(theStringModel); + + // Model instance with constraint to be within two kinetic energy thresholds. + // (Used in the case of QGS-based physics lists for nucleons) + G4TheoFSGenerator *theFTFPmodel_constrained = new G4TheoFSGenerator("FTFP"); + theFTFPmodel_constrained->SetMaxEnergy( + G4HadronicParameters::Instance()->GetMaxEnergy()); + theFTFPmodel_constrained->SetTransport(theCascade); + theFTFPmodel_constrained->SetHighEnergyGenerator(theStringModel); + + // Build the QGSP model + G4TheoFSGenerator *theQGSPmodel = new G4TheoFSGenerator("QGSP"); + theQGSPmodel->SetMaxEnergy(G4HadronicParameters::Instance()->GetMaxEnergy()); + theQGSPmodel->SetTransport(theCascade); + G4QGSMFragmentation *theQgsmFragmentation = new G4QGSMFragmentation; + G4ExcitedStringDecay *theQgsmStringDecay = + new G4ExcitedStringDecay(theQgsmFragmentation); + G4VPartonStringModel *theQgsmStringModel = new G4QGSModel; + theQgsmStringModel->SetFragmentationModel(theQgsmStringDecay); + theQGSPmodel->SetHighEnergyGenerator(theQgsmStringModel); + G4QuasiElasticChannel *theQuasiElastic = + new G4QuasiElasticChannel; // QGSP uses quasi-elastic + theQGSPmodel->SetQuasiElasticChannel(theQuasiElastic); + + // For the case of "physics-list proxies", select the energy range for each + // hadronic model. + const G4double ftfpMinE = + G4HadronicParameters::Instance()->GetMinEnergyTransitionFTF_Cascade(); + const G4double BICMaxE = + G4HadronicParameters::Instance()->GetMaxEnergyTransitionFTF_Cascade(); + const G4double ftfpMaxE = + G4HadronicParameters::Instance()->GetMaxEnergyTransitionQGS_FTF(); + const G4double qgspMinE = + G4HadronicParameters::Instance()->GetMinEnergyTransitionQGS_FTF(); + + theBICmodel->SetMaxEnergy(BICMaxE); + theIonBICmodel->SetMaxEnergy(BICMaxE); + theFTFPmodel_aboveThreshold->SetMinEnergy(ftfpMinE); + theFTFPmodel_constrained->SetMinEnergy(ftfpMinE); + theFTFPmodel_constrained->SetMaxEnergy(ftfpMaxE); + theQGSPmodel->SetMinEnergy(qgspMinE); + + // Cross sections (needed by Geant4 to sample the target nucleus from the + // target material) + G4VCrossSectionDataSet *theProtonXSdata = + new G4BGGNucleonInelasticXS(G4Proton::Proton()); + theProtonXSdata->BuildPhysicsTable(*(G4Proton::Definition())); + + G4VCrossSectionDataSet *theNeutronXSdata = new G4NeutronInelasticXS; + theNeutronXSdata->BuildPhysicsTable(*(G4Neutron::Definition())); + + G4VCrossSectionDataSet *theNeutronHPXSdatainel = new G4NeutronHPInelasticXS; + theNeutronHPXSdatainel->BuildPhysicsTable(*G4Neutron::Definition()); + + G4VCrossSectionDataSet *theHyperonsXSdata = + new G4CrossSectionInelastic(new G4ComponentGGHadronNucleusXsc); + + G4VCrossSectionDataSet *theNuclNuclXSdata = + new G4CrossSectionInelastic(new G4ComponentGGNuclNuclXsc); + + // Set up inelastic processes : store them in a map (with particle definition + // as key) + // for convenience + + typedef std::pair ProcessPair; + + G4HadronicProcess *theProtonInelasticProcess = + new G4HadronInelasticProcess("protonInelastic", G4Proton::Definition()); + fProcessMap.insert( + ProcessPair(G4Proton::Definition(), theProtonInelasticProcess)); + + // a specific processmap is dedidcated to the HP physics for neutron + G4HadronicProcess *theNeutronHPInelasticProcess = + new G4HadronInelasticProcess("NeutronHPInelastic", + G4Neutron::Definition()); + fProcessMapHP.insert( + ProcessPair(G4Neutron::Definition(), theNeutronHPInelasticProcess)); + + G4HadronicProcess *theNeutronInelasticProcess = + new G4HadronInelasticProcess("neutronInelastic", G4Neutron::Definition()); + fProcessMap.insert( + ProcessPair(G4Neutron::Definition(), theNeutronInelasticProcess)); + + // For the HP model, we need to create a new process for the neutron + // Prompt-gamma timing with a track-length estimator in proton therapy, JM + // Létang et al, 2024 + + G4HadronicProcess *theDeuteronInelasticProcess = + new G4HadronInelasticProcess("dInelastic", G4Deuteron::Definition()); + fProcessMap.insert( + ProcessPair(G4Deuteron::Definition(), theDeuteronInelasticProcess)); + + G4HadronicProcess *theTritonInelasticProcess = + new G4HadronInelasticProcess("tInelastic", G4Triton::Definition()); + fProcessMap.insert( + ProcessPair(G4Triton::Definition(), theTritonInelasticProcess)); + + G4HadronicProcess *theHe3InelasticProcess = + new G4HadronInelasticProcess("he3Inelastic", G4He3::Definition()); + fProcessMap.insert(ProcessPair(G4He3::Definition(), theHe3InelasticProcess)); + + G4HadronicProcess *theAlphaInelasticProcess = + new G4HadronInelasticProcess("alphaInelastic", G4Alpha::Definition()); + fProcessMap.insert( + ProcessPair(G4Alpha::Definition(), theAlphaInelasticProcess)); + + G4HadronicProcess *theIonInelasticProcess = + new G4HadronInelasticProcess("ionInelastic", G4GenericIon::Definition()); + fProcessMap.insert( + ProcessPair(G4GenericIon::Definition(), theIonInelasticProcess)); + + // Add the cross sections to the corresponding hadronic processes + + // cross-sections for nucleons + theProtonInelasticProcess->AddDataSet(theProtonXSdata); + theNeutronInelasticProcess->AddDataSet(theNeutronXSdata); + + // specific cross-sections for neutrons in HP model + theNeutronHPInelasticProcess->AddDataSet(theNeutronHPXSdatainel); + + // cross-sections for ions + theDeuteronInelasticProcess->AddDataSet(theNuclNuclXSdata); + theTritonInelasticProcess->AddDataSet(theNuclNuclXSdata); + theHe3InelasticProcess->AddDataSet(theNuclNuclXSdata); + theAlphaInelasticProcess->AddDataSet(theNuclNuclXSdata); + theIonInelasticProcess->AddDataSet(theNuclNuclXSdata); + + // Register the proper hadronic model(s) to the corresponding hadronic + // processes. in the physics list QGSP_BIC, BIC is used only for nucleons + + // processes for BIC => mean-energy on nucleons + theProtonInelasticProcess->RegisterMe(theBICmodel); + theNeutronInelasticProcess->RegisterMe(theBICmodel); + + // processes for QGSP => High-energy on nucleons + theProtonInelasticProcess->RegisterMe(theQGSPmodel); + theNeutronInelasticProcess->RegisterMe(theQGSPmodel); + + // processes for HP => low-energy on neutrons + theNeutronHPInelasticProcess->RegisterMe(theNeutronHP); + + // processes for ion (BIC for ions) + theDeuteronInelasticProcess->RegisterMe(theIonBICmodel); + theTritonInelasticProcess->RegisterMe(theIonBICmodel); + theHe3InelasticProcess->RegisterMe(theIonBICmodel); + theAlphaInelasticProcess->RegisterMe(theIonBICmodel); + theIonInelasticProcess->RegisterMe(theIonBICmodel); + + G4TheoFSGenerator *theFTFPmodelToBeUsed = theFTFPmodel_aboveThreshold; + + theFTFPmodelToBeUsed = theFTFPmodel_constrained; + theNeutronInelasticProcess->RegisterMe(theFTFPmodelToBeUsed); + theProtonInelasticProcess->RegisterMe(theFTFPmodelToBeUsed); + + theFTFPmodelToBeUsed = theFTFPmodel_aboveThreshold; + theDeuteronInelasticProcess->RegisterMe(theFTFPmodelToBeUsed); + theTritonInelasticProcess->RegisterMe(theFTFPmodelToBeUsed); + theHe3InelasticProcess->RegisterMe(theFTFPmodelToBeUsed); + theAlphaInelasticProcess->RegisterMe(theFTFPmodelToBeUsed); + theIonInelasticProcess->RegisterMe(theFTFPmodelToBeUsed); +} + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +HadronicGenerator::~HadronicGenerator() { fPartTable->DeleteAllParticles(); } + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +G4int HadronicGenerator::GenerateInteraction( + G4ParticleDefinition *projectileDefinition, G4Material *targetMaterial, + TH2D *TH2D_EpEpg, TH1D *TH1D_SigmaInelastic, TH2D *TH2D_GammaZ, + TH1D *TH1D_NrPG, G4int nbCollisions) { + // This is the most important method of the HadronicGenerator class: + // the method performs the specified hadronic interaction + // (by invoking the "PostStepDoIt" method of the corresponding hadronic + // process) and returns the final state, i.e. the secondaries produced by the + // collision. It is a relatively short method because the heavy load of + // setting up all possible hadronic processes - with their hadronic models, + // transition regions, and cross sections (the latter is needed for sampling + // the target nucleus from the target material) - was already done by the + // constructor of the class. + G4VParticleChange *aChange = nullptr; + G4int nbPG(0); + + if (projectileDefinition == nullptr) { + G4cerr << "ERROR: projectileDefinition is NULL !" << G4endl; + return 0; + } + + const G4Element *targetElement = targetMaterial->GetElement(0); // + G4double molarMass = targetElement->GetA() / (g / mole); + + // Geometry definition (not strictly needed) + const G4double dimX = 1.0 * mm; + const G4double dimY = 1.0 * mm; + const G4double dimZ = 1.0 * mm; + G4Box *sFrame = new G4Box("Box", dimX, dimY, dimZ); + G4LogicalVolume *lFrame = + new G4LogicalVolume(sFrame, targetMaterial, "Box", 0, 0, 0); + G4PVPlacement *pFrame = + new G4PVPlacement(0, G4ThreeVector(), "Box", lFrame, 0, false, 0); + G4TransportationManager::GetTransportationManager()->SetWorldForTracking( + pFrame); + + G4int numCollisions = nbCollisions; //***LOOKHERE*** NUMBER OF COLLISIONS + G4double rnd1(0); + G4double protonEnergy(0), GAMMAZPerRho(0), kappaPerRho(0), + kappaPerRhoMmCollisions(0); + G4int Ngamma(0); + G4double projectileEnergy; + G4ThreeVector projectileDirection = G4ThreeVector(0.0, 0.0, 1.0); + G4double CrossSection = 0; + + G4double energyStart = TH1D_SigmaInelastic->GetBinCenter(1) * CLHEP::MeV; + G4int nbBins = TH1D_SigmaInelastic->GetNbinsX(); + G4double energyEnd = TH1D_SigmaInelastic->GetBinCenter(nbBins) * CLHEP::MeV; + G4double energyIncrement = TH1D_SigmaInelastic->GetBinWidth(1) * CLHEP::MeV; + + // G4cout << energyStart << " " << energyEnd << " "<< nbBins << " " << + // energyIncrement << G4endl; + + // Loop on the energies + for (G4int f = 1; f <= nbBins; f++) { + + Ngamma = 0; + + // Projectile track & step + G4DynamicParticle *dParticle = new G4DynamicParticle( + projectileDefinition, projectileDirection, energyEnd); + const G4double aTime = 0.0; + const G4ThreeVector aPosition = G4ThreeVector(0.0, 0.0, 0.0); + gTrack = new G4Track(dParticle, aTime, aPosition); + + G4Step *step = new G4Step; + + step->SetTrack(gTrack); + gTrack->SetStep(step); + aPoint = new G4StepPoint; + + aPoint->SetPosition(aPosition); + aPoint->SetMaterial(targetMaterial); + step->SetPreStepPoint(aPoint); + gTrack->SetStep(step); + + for (G4int i = 0; i < numCollisions; ++i) { + + projectileEnergy = (TH1D_SigmaInelastic->GetBinCenter(f) + + (G4UniformRand() - 0.5) * energyIncrement) * + CLHEP::MeV; + // G4cout << "projectile energy = " << projectileEnergy << G4endl; + dParticle->SetKineticEnergy(projectileEnergy); + gTrack->SetKineticEnergy(projectileEnergy); + + // Loop on the collisions + G4HadronicProcess *theProcess = nullptr; + G4ParticleDefinition *theProjectileDef = nullptr; + if (projectileDefinition->IsGeneralIon()) { + theProjectileDef = G4GenericIon::Definition(); + } else { + theProjectileDef = projectileDefinition; + } + // HP case ?? + if (projectileDefinition == G4Neutron::Neutron() && + projectileEnergy < 20 * MeV && projectileEnergy > 4.4 * MeV) { + // For the HP model, we use the neutron HP inelastic process => specific + // processmap is used + auto mapIndex = fProcessMapHP.find(theProjectileDef); + if (mapIndex != fProcessMapHP.end()) + theProcess = mapIndex->second; + if (theProcess == nullptr) { + G4cerr << "ERROR: theProcess is nullptr for neutron HP inelastic !" + << G4endl; + } + } else { + auto mapIndex = fProcessMap.find(theProjectileDef); + if (mapIndex != fProcessMap.end()) + theProcess = mapIndex->second; + if (theProcess == nullptr) { + G4cerr << "ERROR: theProcess is nullptr for " + << theProjectileDef->GetParticleName() << " inelastic !" + << G4endl; + } + } + if (theProcess != nullptr) { + aChange = theProcess->PostStepDoIt(*gTrack, *step); + } else { + G4cerr << "ERROR: theProcess is nullptr !" << G4endl; + } + fLastHadronicProcess = theProcess; + CrossSection = GetCrossSection(dParticle, targetElement, targetMaterial); + kappaPerRho = (CrossSection * Avogadro) / molarMass; + // The assumption in the prompt-gamma TLE is to use the defaut distance + // unit, ie mm + // ($G4_DIR/src/source/externals/clhep/include/CLHEP/Units/SystemOfUnits.h) + kappaPerRhoMmCollisions = kappaPerRho / (numCollisions * cm); + + G4int nsec = aChange ? aChange->GetNumberOfSecondaries() : 0; + + // Loop over produced secondaries and eventually print out some + // information. + for (G4int j = 0; j < nsec; ++j) { + const G4DynamicParticle *sec = + aChange->GetSecondary(j)->GetDynamicParticle(); + const G4String g = sec->GetDefinition()->GetParticleName(); + Double_t E = 0; + + if (g == "gamma") { + Ngamma++; + nbPG++; + E = sec->GetKineticEnergy(); + if (E > 0.04 * MeV) { + TH2D_GammaZ->Fill(projectileEnergy, E, kappaPerRhoMmCollisions); + TH2D_EpEpg->Fill(projectileEnergy, E); + } + } + delete aChange->GetSecondary(j); + } + if (aChange) + aChange->Clear(); + // delete dynamicProjectile; + } + + dParticle->SetKineticEnergy(TH1D_SigmaInelastic->GetBinCenter(f) * + CLHEP::MeV); + CrossSection = GetCrossSection(dParticle, targetElement, targetMaterial); + kappaPerRho = (CrossSection * Avogadro) / molarMass; + TH1D_SigmaInelastic->SetBinContent(f, kappaPerRho); + TH1D_NrPG->SetBinContent(f, Ngamma); + + delete step; + delete dParticle; + } + + delete pFrame; + delete lFrame; + delete sFrame; + + return nbPG; +} + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +G4double HadronicGenerator::GetCrossSection(const G4DynamicParticle *part, + const G4Element *elm, + const G4Material *targetMaterial) { + G4double CrossSection = -999; + G4HadronicProcess *hadProcess = GetHadronicProcess(); + const G4double squareCentimeter = cm * cm; + const G4ParticleDefinition *particle = part->GetParticleDefinition(); + + // Récupérer la section efficace + CrossSection = hadProcess->GetCrossSectionDataStore()->GetCrossSection( + part, elm, targetMaterial); + + return CrossSection / squareCentimeter; +} + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +G4String HadronicGenerator::WeightCompute(TH2D *TH2D_GammaZ, TH1D *TH1D_weight, + G4double frac) { + // This method computes the weight of the interaction for ToF computing + // and returns a string with the result. + // The weight is computed as the ratio of the number of gammas per bin + // to the number of collisions per bin. + G4String result = "is counted, with a fraction of " + std::to_string(frac); + + G4int nbBins = TH2D_GammaZ->GetNbinsX(); + for (G4int i = 1; i <= nbBins; ++i) { + // Sum the column at index i + G4double columnSum = + TH2D_GammaZ->Integral(i, i, 1, TH2D_GammaZ->GetNbinsY()); + // Store the sum in the TH1D histogram + TH1D_weight->SetBinContent(i, TH1D_weight->GetBinContent(i) + + frac * columnSum); + std::cout << "Column " << i << " sum: " << TH1D_weight->GetBinContent(i) + << std::endl; + } + + return result; +} + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... diff --git a/opengate/contrib/vpgTLE/stage0/stage0-vpgtle.cc b/opengate/contrib/vpgTLE/stage0/stage0-vpgtle.cc new file mode 100644 index 000000000..e410987ce --- /dev/null +++ b/opengate/contrib/vpgTLE/stage0/stage0-vpgtle.cc @@ -0,0 +1,343 @@ +// +// ******************************************************************** +// * License and Disclaimer * +// * * +// * The Geant4 software is copyright of the Copyright Holders of * +// * the Geant4 Collaboration. It is provided under the terms and * +// * conditions of the Geant4 Software License, included in the file * +// * LICENSE and available at http://cern.ch/geant4/license . These * +// * include a list of copyright holders. * +// * * +// * Neither the authors of this software system, nor their employing * +// * institutes,nor the agencies providing financial support for this * +// * work make any representation or warranty, express or implied, * +// * regarding this software system or assume any liability for its * +// * use. Please see the license in the file LICENSE and URL above * +// * for the full disclaimer and the limitation of liability. * +// * * +// * This code implementation is the result of the scientific and * +// * technical work of the GEANT4 collaboration. * +// * By using, copying, modifying or distributing the software (or * +// * any work based on the software) you agree to acknowledge its * +// * use in resulting scientific publications, and indicate your * +// * acceptance of all terms of the Geant4 Software license. * +// ******************************************************************** +// +// +/// \file Hadr09.cc +/// \brief Main program of the hadronic/Hadr09 example +// +//------------------------------------------------------------------------ +// This program shows how to use the class Hadronic Generator. +// The class HadronicGenerator is a kind of "hadronic generator", i.e. +// provides Geant4 final states (i.e. secondary particles) produced by +// hadron-nuclear inelastic collisions. +// Please see the class itself for more information. +// +// The use of the class Hadronic Generator is very simple: +// the constructor needs to be invoked only once - specifying the name +// of the Geant4 "physics case" to consider ("FTFP_BERT" will be +// considered as default is the name is not specified) - and then one +// method needs to be called at each collision, specifying the type of +// collision (hadron, energy, direction, material) to be simulated. +// The class HadronicGenerator is expected to work also in a +// multi-threaded environment with "external" threads (i.e. threads +// that are not necessarily managed by Geant4 run-manager): +// each thread should have its own instance of the class. +// +// See the string "***LOOKHERE***" below for the setting of parameters +// of this example: the "physics case", the set of possibilities from +// which to sample the projectile (i.e. whether the projectile is a +// hadron or an ion - in the case of hadron projectile, a list of hadrons +// is possible from which to sample at each collision; in the case of +// ion projectile, only one type of ion needs to be specified), +// the kinetic energy of the projectile (which can be sampled within +// an interval), whether the direction of the projectile is fixed or +// sampled at each collision, the target material (a list of materials +// is possible, from which the target material can be sampled at each +// collision, and then from this target material, the target nucleus +// will be chosen randomly by Geant4 itself), and whether to print out +// some information or not and how frequently. +// Once a well-defined type of hadron-nucleus or nucleus-nucleus +// inelastic collision has been chosen, the method +// HadronicGenerator::GenerateInteraction +// returns the secondaries produced by that interaction (in the form +// of a G4VParticleChange object). +// Some information about this final-state is printed out as an example. +// +// Usage: Hadr09 +//------------------------------------------------------------------------ + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +#include "CLHEP/Random/Randomize.h" +#include "CLHEP/Random/Ranlux64Engine.h" +#include "G4GenericIon.hh" +#include "G4HadronicParameters.hh" +#include "G4IonTable.hh" +#include "G4Material.hh" +#include "G4NistManager.hh" +#include "G4ParticleTable.hh" +#include "G4PhysicalConstants.hh" +#include "G4ProcessManager.hh" +#include "G4SystemOfUnits.hh" +#include "G4UnitsTable.hh" +#include "G4VParticleChange.hh" +#include "G4ios.hh" +#include "HadronicGenerator-vpgtle.hh" +#include "TAxis.h" +#include "TFile.h" +#include "TGraph.h" +#include "TTree.h" +#include "globals.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... + +int main(int argc, char *argv[]) { + + // G4cout << "=== Test of the HadronicGenerator ===" << G4endl; + + // Enable light hypernuclei and anti-hypernuclei + G4HadronicParameters::Instance()->SetEnableHyperNuclei(false); + + // stw, pro and num are bool that states if options for projectiles + // type/number and standard computation are activated + bool stw = false; + bool pro = false; + bool num = false; + + G4int numCollisions = 1e4; // Default number of collisions + G4String strprojectile = "proton"; // Default projectile type + + // to read the options + for (int i = 0; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "weight") { + stw = true; + } + if (arg == "-n") { + numCollisions = atoi(argv[i + 1]); + if (numCollisions <= 0) { + G4cout << "ERROR: Number of collisions must be a positive integer! 1e4 " + "will be used" + << G4endl; + numCollisions = 1e4; // Default value + } + num = true; + G4cout << "Number of collisions set to: " << numCollisions << G4endl; + } + if (arg == "-p") { + strprojectile = argv[i + 1]; + pro = true; + if (strprojectile != "proton" && strprojectile != "neutron") { + G4cout << "ERROR: Invalid projectile type! Proton will be used." + << G4endl; + strprojectile = "proton"; // Default value + } + G4cout << "Projectile type set to: " << strprojectile << G4endl; + } + } + + // See the HadronicGenerator class for the possibilities and meaning of the + // "physics cases". ( In short, it is the name of the Geant4 hadronic model + // used for the simulation of + // the collision, with the possibility of having a transition between two + // models in a given energy interval, as in physics lists. ) + const G4String namePhysics = "QGSP_BIC_HP"; + const TString physlist = namePhysics; + + // Create the ROOT file and histograms + TFile *file = new TFile("/path/to/your/stage0/output/test.root", "RECREATE"); + + // Set up the parameters for the standard computation + const std::vector Fraction = { + 0.65, 0.185, 0.103, 0.03, 0.015, 0.01, 0.002, 0.002, 0.001, 0.001, 0.001}; + const std::vector Body_approx = {"G4_O", "G4_C", "G4_H", "G4_N", + "G4_Ca", "G4_P", "G4_K", "G4_S", + "G4_Na", "G4_Cl", "G4_Mg"}; + + // List of elements in the root file. + const std::vector humanBodyElements = { + //"G4_Al", + //"G4_Ar", + //"G4_Be", + //"G4_B", + //"G4_Ca", + "G4_C", + }; + //"G4_Cl", + //"G4_Cu", + //"G4_F", + //"G4_He", + //"G4_H", + //"G4_Li", + //"G4_Mg", + //"G4_Ne", + //"G4_N", + //"G4_O",}; + //"G4_P", + //"G4_K", + //"G4_Si", + //"G4_Ag", + //"G4_Na", + //"G4_S", + //"G4_Sn", + //"G4_Ti", + //"G4_Zn"}; + + G4int humanbodyindex = + humanBodyElements.size(); //***LOOKHERE*** GAP IN PRINTING + + G4ParticleDefinition *projectileNucleus = nullptr; + G4GenericIon *gion = G4GenericIon::GenericIon(); + gion->SetProcessManager(new G4ProcessManager(gion)); + G4ParticleTable *partTable = G4ParticleTable::GetParticleTable(); + G4IonTable *ions = partTable->GetIonTable(); + partTable->SetReadiness(); + ions->CreateAllIon(); + ions->CreateAllIsomer(); + + // The kinetic energy of the projectile will be sampled randomly, with flat + // probability in the interval [minEnergy, maxEnergy]. + //***LOOKHERE*** HADRON PROJECTILE MAX Ekin + G4int protonNbBins = 500; + G4double protonMinEnergy = 0; // MeV + G4double protonMaxEnergy = 200.; // MeV + G4int gammaNbBins = 250; + G4double gammaMinEnergy = 0; // MeV + G4double gammaMaxEnergy = 10; // MeV + + // histogram for the standard weight + TH1D *TH1D_weight = + new TH1D("Weight", + "Weight of the interaction for ToF computing;Protons energy " + "[MeV];Weight [mm-1]", + protonNbBins, protonMinEnergy, protonMaxEnergy); + + HadronicGenerator *theHadronicGenerator = new HadronicGenerator(namePhysics); + + if (theHadronicGenerator == nullptr) { + G4cerr << "ERROR: theHadronicGenerator is NULL !" << G4endl; + return 1; + } + + // Loop on the element + for (G4int k = 0; k < humanbodyindex; k++) { + G4String done = "not counting in the approximation"; // pre-print for + // standard computation + + G4String nameMaterial = humanBodyElements[k]; + G4Material *targetMaterial = + G4NistManager::Instance()->FindOrBuildMaterial(nameMaterial); + if (targetMaterial == nullptr) { + G4cerr << "ERROR: Material " << nameMaterial << " is not found !" + << G4endl; + return 3; + } + const G4Element *targetElement = targetMaterial->GetElement(0); + + TDirectory *dir = file->mkdir( + targetElement + ->GetSymbol()); // Création du dossier pour chaque élément chimique + + TString PGbin = to_string(10. / 250); + TH2D *TH2D_EpEpg = + new TH2D("EpEpg", + "PGs energy as a function of protons;Protons energy [MeV];PGs " + "energy [MeV];Log(number of gammas)", + protonNbBins, protonMinEnergy, protonMaxEnergy, gammaNbBins, + gammaMinEnergy, gammaMaxEnergy); // Graph 2D + TH2D *TH2D_GammaZ = new TH2D( + "GammaZ", + "PG yield per [" + PGbin + + "MeV*cm];Protons energy [MeV];PGs energy [MeV];Log(GAMMAZ/1)", + protonNbBins, protonMinEnergy, protonMaxEnergy, gammaNbBins, + gammaMinEnergy, gammaMaxEnergy); // Graph 2D + TH1D *TH1D_SigmaInelastic = + new TH1D("Kapa inelastique", + "Linear attenuation coefficient [/cm];Protons energy " + "[MeV];kapa inelastique [cm-1]", + protonNbBins, protonMinEnergy, protonMaxEnergy); + TH1D *TH1D_NrPG = new TH1D( + "NrPG", + "Number of prompt gamma-rays;Protons energy [MeV];Number of PGs", + protonNbBins, protonMinEnergy, protonMaxEnergy); + + G4cout << "Loop on element: " << targetElement->GetSymbol() << G4endl; + + // G4DynamicParticle* dynamicProjectile = new G4DynamicParticle(projectile, + // aDirection, projectileEnergy); + + CLHEP::Ranlux64Engine defaultEngine(1234567, 4); + CLHEP::HepRandom::setTheEngine(&defaultEngine); + G4int seed = time(NULL); + CLHEP::HepRandom::setTheSeed(seed); + + G4int nbPG(0); + G4int nbCollisions = 1e4; // Default number of collisions + if (num) { + nbCollisions = numCollisions; + } + G4cout << "Number of collisions: " << nbCollisions << G4endl; + G4ParticleDefinition *projectile = nullptr; + if (pro) { + projectile = partTable->FindParticle(strprojectile); + } else { + projectile = partTable->FindParticle("proton"); + } + G4cout << "Projectile: " << projectile->GetParticleName() << G4endl; + nbPG = theHadronicGenerator->GenerateInteraction( + projectile, targetMaterial, TH2D_EpEpg, TH1D_SigmaInelastic, + TH2D_GammaZ, TH1D_NrPG, nbCollisions); + if (stw) { + auto ind = + std::find(Body_approx.begin(), Body_approx.end(), nameMaterial); + // Check if the element was found + if (ind != Body_approx.end()) { + // Calculate the index + size_t index = std::distance(Body_approx.begin(), ind); + done = theHadronicGenerator->WeightCompute(TH2D_GammaZ, TH1D_weight, + Fraction[index]); + } + } + + G4cout << "nr of PG: " << nbPG << G4endl; + G4cout << nameMaterial << " : " << done << G4endl; + dir->cd(); + TH2D_EpEpg->Write(); + TH1D_SigmaInelastic->Write(); + TH1D_NrPG->Write(); + TH2D_GammaZ->Write(); + + delete TH2D_EpEpg; + delete TH2D_GammaZ; + delete TH1D_SigmaInelastic; + delete TH1D_NrPG; + } + if (stw) { + TDirectory *dirw = file->mkdir("standard_Weight"); + dirw->cd(); + TH1D_weight->Write(); + } + delete TH1D_weight; + + file->Close(); + delete theHadronicGenerator; + + return 0; +} + +//....oooOO0OOooo........oooOO0OOooo........oooOO0OOooo........oooOO0OOooo...... diff --git a/opengate/contrib/vpgTLE/stageX/__init__.py b/opengate/contrib/vpgTLE/stageX/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaAnalogActor.cpp b/opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaAnalogActor.cpp new file mode 100644 index 000000000..9e327e3d3 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaAnalogActor.cpp @@ -0,0 +1,295 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + ------------------------------------ -------------- */ + +#include "G4EmCalculator.hh" +#include "G4Gamma.hh" +#include "G4ParticleDefinition.hh" +#include "G4RandomTools.hh" +#include "G4RunManager.hh" +#include "G4Threading.hh" +#include "G4Track.hh" + +#include "G4HadronInelasticProcess.hh" +#include "GateHelpers.h" +#include "GateHelpersDict.h" +#include "GateHelpersImage.h" +#include "GateMaterialMuHandler.h" +#include "GateVoxelizedPromptGammaAnalogActor.h" + +#include "CLHEP/Random/Randomize.h" +#include +#include +#include +#include +#include + +#include +#include +// #include +#include +#include + +GateVoxelizedPromptGammaAnalogActor::GateVoxelizedPromptGammaAnalogActor( + py::dict &user_info) + : GateVActor(user_info, true) { + fMultiThreadReady = + true; // But used as a single thread python side : nb pf runs = 1 +} + +GateVoxelizedPromptGammaAnalogActor::~GateVoxelizedPromptGammaAnalogActor() { + // not needed + cpp_tof_proton_image = nullptr; + cpp_E_proton_image = nullptr; + cpp_E_neutron_image = nullptr; + cpp_tof_neutron_image = nullptr; + + // Release the 3D volume + volume = nullptr; + std::cout << "GateVoxelizedPromptGammaAnalogActor destructor called. " + "Resources released." + << std::endl; +} + +void GateVoxelizedPromptGammaAnalogActor::InitializeUserInfo( + py::dict &user_info) { + GateVActor::InitializeUserInfo(user_info); + + // retrieve the python param here + timebins = py::int_(user_info["timebins"]); + timerange = py::float_(user_info["timerange"]); + energybins = py::int_(user_info["energybins"]); + energyrange = py::float_(user_info["energyrange"]); + + fTranslation = DictGetG4ThreeVector(user_info, "translation"); + fsize = DictGetG4ThreeVector(user_info, "size"); + fspacing = DictGetG4ThreeVector(user_info, "spacing"); +} + +void GateVoxelizedPromptGammaAnalogActor::InitializeCpp() { + GateVActor::InitializeCpp(); + // Create the image pointers + // (the size and allocation will be performed on the py side) + if (fProtonTimeFlag) { + cpp_tof_proton_image = ImageType::New(); + } + if (fProtonEnergyFlag) { + cpp_E_proton_image = ImageType::New(); + } + if (fNeutronEnergyFlag) { + cpp_E_neutron_image = ImageType::New(); + } + if (fNeutronTimeFlag) { + cpp_tof_neutron_image = ImageType::New(); + } + + // Construction of the 3D image with the same shape/mat that the voxel of the + // actor but is accepted by the method of volume_attach and "isInside" + volume = Image3DType::New(); + + Image3DType::RegionType region; + Image3DType::SizeType size; + Image3DType::SpacingType spacing; + + size[0] = fsize[0]; + size[1] = fsize[1]; + size[2] = fsize[2]; + region.SetSize(size); + + spacing[0] = fspacing[0]; + spacing[1] = fspacing[1]; + spacing[2] = fspacing[2]; + + volume->SetRegions(region); + volume->SetSpacing(spacing); + volume->Allocate(); + volume->FillBuffer(0); + + incidentParticles = + 0; // initiate the conuter of incidente protons - scaling factor +} + +void GateVoxelizedPromptGammaAnalogActor::BeginOfRunActionMasterThread( + int run_id) { + // Attach the 3D volume used to + + // Fill the 4D volume of interest with 0 to ensure that it is well initiated + if (fProtonTimeFlag) { + cpp_tof_proton_image->FillBuffer(0); + } + if (fProtonEnergyFlag) { + cpp_E_proton_image->FillBuffer(0); + } + if (fNeutronEnergyFlag) { + cpp_E_neutron_image->FillBuffer(0); + } + if (fNeutronTimeFlag) { + cpp_tof_neutron_image->FillBuffer(0); + } + AttachImageToVolume(volume, fPhysicalVolumeName, fTranslation); +} + +void GateVoxelizedPromptGammaAnalogActor::BeginOfRunAction(const G4Run *run) {} + +void GateVoxelizedPromptGammaAnalogActor::BeginOfEventAction( + const G4Event *event) { + T0 = event->GetPrimaryVertex()->GetT0(); + incidentParticles++; +} + +void GateVoxelizedPromptGammaAnalogActor::SteppingAction(G4Step *step) { + if (step->GetTrack()->GetParticleDefinition() != G4Neutron::Neutron() && + (step->GetTrack()->GetParticleDefinition() != G4Proton::Proton())) { + return; + } + static G4HadronicProcessStore *store = G4HadronicProcessStore::Instance(); + static G4VProcess *protonInelastic = + store->FindProcess(G4Proton::Proton(), fHadronInelastic); + static G4VProcess *neutronInelastic = + store->FindProcess(G4Neutron::Neutron(), fHadronInelastic); + if (step->GetPostStepPoint()->GetProcessDefinedStep() != protonInelastic && + step->GetPostStepPoint()->GetProcessDefinedStep() != neutronInelastic) { + return; + } + + auto position = step->GetPostStepPoint()->GetPosition(); + auto touchable = step->GetPreStepPoint()->GetTouchable(); + // Get the voxel index + auto localPosition = + touchable->GetHistory()->GetTransform(0).TransformPoint(position); + + // convert G4ThreeVector to itk PointType + Image3DType::PointType point; + point[0] = localPosition[0]; + point[1] = localPosition[1]; + point[2] = localPosition[2]; + + Image3DType::IndexType index; + G4bool isInside = volume->TransformPhysicalPointToIndex(point, index); + if (!isInside) { // verification + return; // Skip if not inside the volume + } + + // Get the spatial index from the index obtained with the 3D volume and th + // emethod GetStepVoxelPosition() + ImageType::IndexType ind; + ind[0] = index[0]; + ind[1] = index[1]; + ind[2] = index[2]; + + auto secondaries = step->GetSecondary(); + for (size_t i = 0; i < secondaries->size(); i++) { + auto secondary = secondaries->at(i); + auto secondary_def = secondary->GetParticleDefinition(); + if (secondary_def != G4Gamma::Gamma()) { + continue; + } + G4double gammaEnergy = secondary->GetKineticEnergy(); // in MeV + // thershold with a minimum energy of 40 keV + if (gammaEnergy < 0.04 * CLHEP::MeV) { + continue; + } + + if (fProtonTimeFlag || + fNeutronTimeFlag) { // If the quantity of interest is the time of flight + // Get the time of flight + // G4double randomtime = G4UniformRand(); + // G4double pretime = step->GetPreStepPoint()->GetGlobalTime()- T0; //ns + // G4double posttime = step->GetPostStepPoint()->GetGlobalTime()- T0;//ns + G4double time = secondary->GetGlobalTime() - T0; // ns + + // Get the voxel index (fourth dim) corresponding to the time of flight + G4int bin = static_cast( + time / (timerange / timebins)); // Always the left bin + + if (bin >= timebins) { + bin = timebins; + } + ind[3] = bin; + // Store the value in the volume for neutrons OR protons -> LEFT BINNING + if (fProtonTimeFlag && + step->GetTrack()->GetParticleDefinition()->GetParticleName() == + "proton") { + ImageAddValue(cpp_tof_proton_image, ind, 1); + } + if (fNeutronTimeFlag && + step->GetTrack()->GetParticleDefinition()->GetParticleName() == + "neutron") { + ImageAddValue(cpp_tof_neutron_image, ind, 1); + } + } + if (fProtonEnergyFlag || + fNeutronEnergyFlag) { // when the quantity of interest is the energy + // Get the voxel index (fourth dim) corresponding to the energy of the + // projectile + G4int bin = static_cast( + gammaEnergy / (energyrange / energybins)); // Always the left bin + if (bin >= energybins) { + bin = energybins; + } + if (bin < 0) { + bin = 0; // underflow + } + ind[3] = bin; + // Store the value in the volume for neutrons OR protons -> LEFT BINNING + if (fProtonEnergyFlag && + step->GetTrack()->GetParticleDefinition()->GetParticleName() == + "proton") { + ImageAddValue(cpp_E_proton_image, ind, 1); + } + if (fNeutronEnergyFlag && + step->GetTrack()->GetParticleDefinition()->GetParticleName() == + "neutron") { + ImageAddValue(cpp_E_neutron_image, ind, 1); + } + } + } +} + +void GateVoxelizedPromptGammaAnalogActor::EndOfRunAction(const G4Run *run) { + std::cout << "incident particles : " << incidentParticles << std::endl; + if (incidentParticles == 0) { + std::cerr << "Error: incidentParticles is zero. Skipping scaling." + << std::endl; + return; + } + // scaling all the 4D voxels with th enumber of incident protons (= number of + // event) + if (fProtonTimeFlag) { + itk::ImageRegionIterator it( + cpp_tof_proton_image, cpp_tof_proton_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } + if (fProtonEnergyFlag) { + itk::ImageRegionIterator it( + cpp_E_proton_image, cpp_E_proton_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } + if (fNeutronEnergyFlag) { + itk::ImageRegionIterator it( + cpp_E_neutron_image, cpp_E_neutron_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } + if (fNeutronTimeFlag) { + itk::ImageRegionIterator it( + cpp_tof_neutron_image, + cpp_tof_neutron_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } +} + +int GateVoxelizedPromptGammaAnalogActor::EndOfRunActionMasterThread( + int run_id) { + return 0; +} diff --git a/opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaAnalogActor.h b/opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaAnalogActor.h new file mode 100644 index 000000000..f207a5846 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaAnalogActor.h @@ -0,0 +1,98 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + -------------------------------------------------- */ + +#ifndef GateVoxelizedPromptGammaAnalogActor_h +#define GateVoxelizedPromptGammaAnalogActor_h + +#include "G4Cache.hh" +#include "G4EmCalculator.hh" +#include "G4NistManager.hh" +#include "G4VPrimitiveScorer.hh" +#include "GateDoseActor.h" +#include "GateMaterialMuHandler.h" + +#include + +namespace py = pybind11; + +class GateVoxelizedPromptGammaAnalogActor : public GateVActor { + +public: + // destructor + ~GateVoxelizedPromptGammaAnalogActor() override; + + explicit GateVoxelizedPromptGammaAnalogActor(py::dict &user_info); + + void InitializeUserInfo(py::dict &user_info) override; + + void InitializeCpp() override; + + void BeginOfRunActionMasterThread(int run_id); + + int EndOfRunActionMasterThread(int run_id) override; + + void EndOfRunAction(const G4Run *run); + + void BeginOfRunAction(const G4Run *run); + + void BeginOfEventAction(const G4Event *event) override; + + void SteppingAction(G4Step *) override; + + inline bool GetProtonTimeFlag() const { return fProtonTimeFlag; } + + inline void SetProtonTimeFlag(const bool b) { fProtonTimeFlag = b; } + + inline bool GetNeutronTimeFlag() const { return fNeutronTimeFlag; } + + inline void SetNeutronTimeFlag(const bool b) { fNeutronTimeFlag = b; } + + inline bool GetProtonEnergyFlag() const { return fProtonEnergyFlag; } + + inline void SetProtonEnergyFlag(const bool b) { fProtonEnergyFlag = b; } + + inline bool GetNeutronEnergyFlag() const { return fNeutronEnergyFlag; } + + inline void SetNeutronEnergyFlag(const bool b) { fNeutronEnergyFlag = b; } + + inline std::string GetPhysicalVolumeName() const { + return fPhysicalVolumeName; + } + + inline void SetPhysicalVolumeName(std::string s) { fPhysicalVolumeName = s; } + + std::string fPhysicalVolumeName; + + // Image type + typedef itk::Image ImageType; + ImageType::Pointer cpp_tof_neutron_image; + ImageType::Pointer cpp_tof_proton_image; + ImageType::Pointer cpp_E_proton_image; + ImageType::Pointer cpp_E_neutron_image; + + typedef itk::Image Image3DType; + Image3DType::Pointer volume; + + G4double T0; + G4int incidentParticles; + + G4int timebins; + G4double timerange; + G4int energybins; + G4double energyrange; + + G4bool fProtonTimeFlag{}; + G4bool fProtonEnergyFlag{}; + G4bool fNeutronTimeFlag{}; + G4bool fNeutronEnergyFlag{}; + + G4ThreeVector fsize; + G4ThreeVector fspacing; + G4ThreeVector fTranslation; +}; + +#endif // GateVoxelizedPromptGammaAnalogActor_h diff --git a/opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaTLEActor.cpp b/opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaTLEActor.cpp new file mode 100644 index 000000000..b7d729652 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaTLEActor.cpp @@ -0,0 +1,282 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + ------------------------------------ -------------- */ + +#include "G4EmCalculator.hh" +#include "G4Gamma.hh" +#include "G4ParticleDefinition.hh" +#include "G4RandomTools.hh" +#include "G4RunManager.hh" +#include "G4Threading.hh" +#include "G4Track.hh" + +#include "G4HadronInelasticProcess.hh" +#include "GateHelpers.h" +#include "GateHelpersDict.h" +#include "GateHelpersImage.h" +#include "GateMaterialMuHandler.h" +#include "GateVoxelizedPromptGammaTLEActor.h" + +#include "CLHEP/Random/Randomize.h" +#include +#include +#include +#include +#include + +GateVoxelizedPromptGammaTLEActor::GateVoxelizedPromptGammaTLEActor( + py::dict &user_info) + : GateVActor(user_info, true) { + fMultiThreadReady = + true; // But used as a single thread python side : nb pf runs = 1 +} + +GateVoxelizedPromptGammaTLEActor::~GateVoxelizedPromptGammaTLEActor() { + // not needed +} + +void GateVoxelizedPromptGammaTLEActor::InitializeUserInfo(py::dict &user_info) { + GateVActor::InitializeUserInfo(user_info); + + // retrieve the python param here + timebins = py::int_(user_info["timebins"]); + timerange = py::float_(user_info["timerange"]); + energybins = py::int_(user_info["energybins"]); + energyrange = py::float_(user_info["energyrange"]); + weight = py::bool_(user_info["weight"]); + fTranslation = DictGetG4ThreeVector(user_info, "translation"); + fsize = DictGetG4ThreeVector(user_info, "size"); + fspacing = DictGetG4ThreeVector(user_info, "spacing"); +} + +void GateVoxelizedPromptGammaTLEActor::InitializeCpp() { + GateVActor::InitializeCpp(); + // Create the image pointers + // (the size and allocation will be performed on the py side) + if (fProtonTimeFlag) { + cpp_tof_proton_image = ImageType::New(); + } + if (fProtonEnergyFlag) { + cpp_E_proton_image = ImageType::New(); + } + if (fNeutronEnergyFlag) { + cpp_E_neutron_image = ImageType::New(); + } + if (fNeutronTimeFlag) { + cpp_tof_neutron_image = ImageType::New(); + } + + // Construction of the 3D image with the same shape/mat that the voxel of the + // actor but is accepted by the method of volume_attach and "isInside" + volume = Image3DType::New(); + + Image3DType::RegionType region; + Image3DType::SizeType size; + Image3DType::SpacingType spacing; + + size[0] = fsize[0]; + size[1] = fsize[1]; + size[2] = fsize[2]; + region.SetSize(size); + + spacing[0] = fspacing[0]; + spacing[1] = fspacing[1]; + spacing[2] = fspacing[2]; + + volume->SetRegions(region); + volume->SetSpacing(spacing); + volume->Allocate(); + volume->FillBuffer(0); + + incidentParticles = + 0; // initiate the conuter of incidente protons - scaling factor +} + +void GateVoxelizedPromptGammaTLEActor::BeginOfRunActionMasterThread( + int run_id) { + // Fill the 4D volume of interest with 0 to ensure that it is well initiated + if (fProtonTimeFlag) { + cpp_tof_proton_image->FillBuffer(0); + } + if (fProtonEnergyFlag) { + cpp_E_proton_image->FillBuffer(0); + } + if (fNeutronEnergyFlag) { + cpp_E_neutron_image->FillBuffer(0); + } + if (fNeutronTimeFlag) { + cpp_tof_neutron_image->FillBuffer(0); + } + // Attach the 3D volume to verify if the particle is inside the ct volume + AttachImageToVolume(volume, fPhysicalVolumeName, fTranslation); +} + +void GateVoxelizedPromptGammaTLEActor::BeginOfRunAction(const G4Run *run) {} + +void GateVoxelizedPromptGammaTLEActor::BeginOfEventAction( + + const G4Event *event) { + T0 = event->GetPrimaryVertex()->GetT0(); + incidentParticles++; +} + +void GateVoxelizedPromptGammaTLEActor::SteppingAction(G4Step *step) { + const G4ParticleDefinition *particle = + step->GetTrack()->GetParticleDefinition(); + if ((particle != G4Neutron::Neutron()) && (particle != G4Proton::Proton())) { + return; + } + auto position = step->GetPostStepPoint()->GetPosition(); + auto touchable = step->GetPreStepPoint()->GetTouchable(); + // Get the voxel index + + auto localPosition = + touchable->GetHistory()->GetTransform(0).TransformPoint(position); + + // convert G4ThreeVector to itk PointType + Image3DType::PointType point; + point[0] = localPosition[0]; + point[1] = localPosition[1]; + point[2] = localPosition[2]; + + Image3DType::IndexType index; + G4bool isInside = volume->TransformPhysicalPointToIndex(point, index); + if (!isInside) { // verification + return; // Skip if not inside the volume + } + + // Convert the index to a 4D index + ImageType::IndexType ind; + ind[0] = index[0]; + ind[1] = index[1]; + ind[2] = index[2]; + + // Get the step lenght + const G4double &l = step->GetStepLength(); + G4Material *mat = step->GetPreStepPoint()->GetMaterial(); + G4double rho = mat->GetDensity() / (CLHEP::g / CLHEP::cm3); + auto w = step->GetTrack() + ->GetWeight(); // Get the weight of the track (particle history) + // for potential russian roulette or splitting + + // Get the energy of the projectile + const G4double &preE = step->GetPreStepPoint()->GetKineticEnergy(); + const G4double &postE = step->GetPostStepPoint()->GetKineticEnergy(); + G4double projectileEnergy = preE; + if (postE != 0) { + const G4double &randomenergy = G4UniformRand(); + projectileEnergy = postE + randomenergy * (preE - postE); // MeV + } + // Get the energy bin index + G4int binE = static_cast( + projectileEnergy / (energyrange / energybins)); // Always the left bin + if (binE >= energybins) { + binE = energybins; // last bin = overflow + } + + if (binE < 0) { + binE = 0; // underflow + } + + if ((fProtonTimeFlag) || + (fNeutronTimeFlag)) { // If the quantity of interest is the time of flight + // Get the time of flight + const G4double &randomtime = G4UniformRand(); + const G4double &pretime = + step->GetPreStepPoint()->GetGlobalTime() - T0; // ns + const G4double &posttime = + step->GetPostStepPoint()->GetGlobalTime() - T0; // ns + G4double time = (posttime + randomtime * (pretime - posttime)); // ns + // Get the voxel index (fourth dim) corresponding to the time of flight + G4int bin = static_cast(time / (timerange / timebins)); + if (bin >= timebins) { + bin = timebins; // overflow + } + if (bin < 0) { + bin = 0; // underflow + } + ind[3] = bin; + G4double pg_sum = 1; + // Store the value in the volume for neutrons OR protons -> LEFT BINNING + if (fProtonTimeFlag && particle == G4Proton::Proton()) { + if (weight) { + pg_sum = fProtonVector[binE]; + } + ImageAddValue(cpp_tof_proton_image, ind, pg_sum * l * rho * w); + } + if (fNeutronTimeFlag && particle == G4Neutron::Neutron()) { + if (weight) { + pg_sum = fProtonVector[binE]; + } + ImageAddValue(cpp_tof_neutron_image, ind, + pg_sum * l * rho * w); + } + } + if (fProtonEnergyFlag || + fNeutronEnergyFlag) { // when the quantity of interest is the energy + ind[3] = binE; + // Store the value in the volume for neutrons OR protons -> LEFT BINNING + if (fProtonEnergyFlag && particle == G4Proton::Proton()) { + ImageAddValue(cpp_E_proton_image, ind, l * rho * w); + } + if (fNeutronEnergyFlag && particle == G4Neutron::Neutron()) { + ImageAddValue(cpp_E_neutron_image, ind, l * rho * w); + } + } +} + +void GateVoxelizedPromptGammaTLEActor::EndOfRunAction(const G4Run *run) { + std::cout << "incident particles : " << incidentParticles << std::endl; + if (incidentParticles == 0) { + std::cerr << "Error: incidentParticles is zero. Skipping scaling." + << std::endl; + return; + } + // scaling all the 4D voxels with the number of incident protons (= number of + // event) + if (fProtonTimeFlag) { + itk::ImageRegionIterator it( + cpp_tof_proton_image, cpp_tof_proton_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } + if (fProtonEnergyFlag) { + itk::ImageRegionIterator it( + cpp_E_proton_image, cpp_E_proton_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } + if (fNeutronEnergyFlag) { + itk::ImageRegionIterator it( + cpp_E_neutron_image, cpp_E_neutron_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } + if (fNeutronTimeFlag) { + itk::ImageRegionIterator it( + cpp_tof_neutron_image, + cpp_tof_neutron_image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) { + it.Set(it.Get() / incidentParticles); + } + } +} + +int GateVoxelizedPromptGammaTLEActor::EndOfRunActionMasterThread(int run_id) { + return 0; +} + +// Standarsized Vectors For ToF weights +void GateVoxelizedPromptGammaTLEActor::SetVector(py::array_t vect_p, + py::array_t vect_n) { + fProtonVector = + std::vector(vect_p.data(), vect_p.data() + vect_p.size()); + fNeutronVector = + std::vector(vect_n.data(), vect_n.data() + vect_n.size()); +} diff --git a/opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaTLEActor.h b/opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaTLEActor.h new file mode 100644 index 000000000..202e5b3c5 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/actor/GateVoxelizedPromptGammaTLEActor.h @@ -0,0 +1,107 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + -------------------------------------------------- */ + +#ifndef GateVoxelizedPromptGammaTLEActor_h +#define GateVoxelizedPromptGammaTLEActor_h + +#include "G4Cache.hh" +#include "G4EmCalculator.hh" +#include "G4NistManager.hh" +#include "G4VPrimitiveScorer.hh" +#include "GateDoseActor.h" +#include "GateMaterialMuHandler.h" +#include +#include +#include + +namespace py = pybind11; + +class GateVoxelizedPromptGammaTLEActor : public GateVActor { + +public: + // destructor + ~GateVoxelizedPromptGammaTLEActor() override; + + explicit GateVoxelizedPromptGammaTLEActor(py::dict &user_info); + + void InitializeUserInfo(py::dict &user_info) override; + + void InitializeCpp() override; + + void BeginOfRunActionMasterThread(int run_id); + + int EndOfRunActionMasterThread(int run_id) override; + + void EndOfRunAction(const G4Run *run); + + void BeginOfRunAction(const G4Run *run); + + void BeginOfEventAction(const G4Event *event) override; + + void SteppingAction(G4Step *) override; + + inline bool GetProtonTimeFlag() const { return fProtonTimeFlag; } + + inline void SetProtonTimeFlag(const bool b) { fProtonTimeFlag = b; } + + inline bool GetNeutronTimeFlag() const { return fNeutronTimeFlag; } + + inline void SetNeutronTimeFlag(const bool b) { fNeutronTimeFlag = b; } + + inline bool GetProtonEnergyFlag() const { return fProtonEnergyFlag; } + + inline void SetProtonEnergyFlag(const bool b) { fProtonEnergyFlag = b; } + + inline bool GetNeutronEnergyFlag() const { return fNeutronEnergyFlag; } + + inline void SetNeutronEnergyFlag(const bool b) { fNeutronEnergyFlag = b; } + + void SetVector(py::array_t vect_p, py::array_t vect_n); + + inline std::string GetPhysicalVolumeName() const { + return fPhysicalVolumeName; + } + + // void SetVector(pybind11::array_t vect_p, pybind11::array_t + // vect_n ); + + inline void SetPhysicalVolumeName(std::string s) { fPhysicalVolumeName = s; } + + std::string fPhysicalVolumeName; + + // Image type + typedef itk::Image ImageType; + ImageType::Pointer cpp_tof_neutron_image; + ImageType::Pointer cpp_tof_proton_image; + ImageType::Pointer cpp_E_proton_image; + ImageType::Pointer cpp_E_neutron_image; + + typedef itk::Image Image3DType; + Image3DType::Pointer volume; + + G4double T0; + G4int incidentParticles; + + G4int timebins; + G4double timerange; + G4int energybins; + G4double energyrange; + + G4bool fProtonTimeFlag{}; + G4bool fProtonEnergyFlag{}; + G4bool fNeutronTimeFlag{}; + G4bool fNeutronEnergyFlag{}; + G4bool weight; + + G4ThreeVector fsize; + G4ThreeVector fspacing; + G4ThreeVector fTranslation; + std::vector fProtonVector; + std::vector fNeutronVector; +}; + +#endif // GateVoxelizedPromptGammaTLEActor_h diff --git a/opengate/contrib/vpgTLE/stageX/actor/pyGateVoxelizedPromptGammaAnalogActor.cpp b/opengate/contrib/vpgTLE/stageX/actor/pyGateVoxelizedPromptGammaAnalogActor.cpp new file mode 100644 index 000000000..bf197db05 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/actor/pyGateVoxelizedPromptGammaAnalogActor.cpp @@ -0,0 +1,81 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + -------------------------------------------------- */ + +#include +#include + +namespace py = pybind11; + +#include "GateVoxelizedPromptGammaAnalogActor.h" + +class PyGateVoxelizedPromptGammaAnalogActor + : public GateVoxelizedPromptGammaAnalogActor { +public: + // Inherit the constructors + using GateVoxelizedPromptGammaAnalogActor:: + GateVoxelizedPromptGammaAnalogActor; + + void BeginOfRunActionMasterThread(int run_id) override { + PYBIND11_OVERLOAD(void, GateVoxelizedPromptGammaAnalogActor, + BeginOfRunActionMasterThread, run_id); + } + + int EndOfRunActionMasterThread(int run_id) override { + PYBIND11_OVERLOAD(int, GateVoxelizedPromptGammaAnalogActor, + EndOfRunActionMasterThread, run_id); + } +}; + +void init_GateVoxelizedPromptGammaAnalogActor(py::module &m) { + py::class_, + GateVActor>(m, "GateVoxelizedPromptGammaAnalogActor") + .def(py::init()) + .def("InitializeUserInfo", + &GateVoxelizedPromptGammaAnalogActor::InitializeUserInfo) + .def("BeginOfRunActionMasterThread", + &GateVoxelizedPromptGammaAnalogActor::BeginOfRunActionMasterThread) + .def("EndOfRunActionMasterThread", + &GateVoxelizedPromptGammaAnalogActor::EndOfRunActionMasterThread) + .def("SetPhysicalVolumeName", + &GateVoxelizedPromptGammaAnalogActor::SetPhysicalVolumeName) + .def("GetPhysicalVolumeName", + &GateVoxelizedPromptGammaAnalogActor::GetPhysicalVolumeName) + + .def("SetProtonTimeFlag", + &GateVoxelizedPromptGammaAnalogActor::SetProtonTimeFlag) + .def("GetProtonTimeFlag", + &GateVoxelizedPromptGammaAnalogActor::GetProtonTimeFlag) + + .def("SetProtonEnergyFlag", + &GateVoxelizedPromptGammaAnalogActor::SetProtonEnergyFlag) + .def("GetProtonEnergyFlag", + &GateVoxelizedPromptGammaAnalogActor::GetProtonEnergyFlag) + + .def("SetNeutronEnergyFlag", + &GateVoxelizedPromptGammaAnalogActor::SetNeutronEnergyFlag) + .def("GetNeutronEnergyFlag", + &GateVoxelizedPromptGammaAnalogActor::GetNeutronEnergyFlag) + + .def("SetNeutronTimeFlag", + &GateVoxelizedPromptGammaAnalogActor::SetNeutronTimeFlag) + .def("GetNeutronTimeFlag", + &GateVoxelizedPromptGammaAnalogActor::GetNeutronTimeFlag) + + .def_readwrite("fPhysicalVolumeName", + &GateVoxelizedPromptGammaAnalogActor::fPhysicalVolumeName) + .def_readwrite( + "cpp_tof_neutron_image", + &GateVoxelizedPromptGammaAnalogActor::cpp_tof_neutron_image) + .def_readwrite("cpp_tof_proton_image", + &GateVoxelizedPromptGammaAnalogActor::cpp_tof_proton_image) + .def_readwrite("cpp_E_neutron_image", + &GateVoxelizedPromptGammaAnalogActor::cpp_E_neutron_image) + .def_readwrite("cpp_E_proton_image", + &GateVoxelizedPromptGammaAnalogActor::cpp_E_proton_image); +} diff --git a/opengate/contrib/vpgTLE/stageX/actor/pyGateVoxelizedPromptGammaTLEActor.cpp b/opengate/contrib/vpgTLE/stageX/actor/pyGateVoxelizedPromptGammaTLEActor.cpp new file mode 100644 index 000000000..64458959f --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/actor/pyGateVoxelizedPromptGammaTLEActor.cpp @@ -0,0 +1,79 @@ +/* -------------------------------------------------- + Copyright (C): OpenGATE Collaboration + This software is distributed under the terms + of the GNU Lesser General Public Licence (LGPL) + See LICENSE.md for further details + -------------------------------------------------- */ + +#include +#include + +namespace py = pybind11; + +#include "GateVoxelizedPromptGammaTLEActor.h" + +class PyGateVoxelizedPromptGammaTLEActor + : public GateVoxelizedPromptGammaTLEActor { +public: + // Inherit the constructors + using GateVoxelizedPromptGammaTLEActor::GateVoxelizedPromptGammaTLEActor; + + void BeginOfRunActionMasterThread(int run_id) override { + PYBIND11_OVERLOAD(void, GateVoxelizedPromptGammaTLEActor, + BeginOfRunActionMasterThread, run_id); + } + + int EndOfRunActionMasterThread(int run_id) override { + PYBIND11_OVERLOAD(int, GateVoxelizedPromptGammaTLEActor, + EndOfRunActionMasterThread, run_id); + } +}; + +void init_GateVoxelizedPromptGammaTLEActor(py::module &m) { + py::class_, + GateVActor>(m, "GateVoxelizedPromptGammaTLEActor") + .def(py::init()) + .def("InitializeUserInfo", + &GateVoxelizedPromptGammaTLEActor::InitializeUserInfo) + .def("BeginOfRunActionMasterThread", + &GateVoxelizedPromptGammaTLEActor::BeginOfRunActionMasterThread) + .def("EndOfRunActionMasterThread", + &GateVoxelizedPromptGammaTLEActor::EndOfRunActionMasterThread) + .def("SetPhysicalVolumeName", + &GateVoxelizedPromptGammaTLEActor::SetPhysicalVolumeName) + .def("GetPhysicalVolumeName", + &GateVoxelizedPromptGammaTLEActor::GetPhysicalVolumeName) + .def("SetVector", &GateVoxelizedPromptGammaTLEActor::SetVector) + .def("SetProtonTimeFlag", + &GateVoxelizedPromptGammaTLEActor::SetProtonTimeFlag) + .def("GetProtonTimeFlag", + &GateVoxelizedPromptGammaTLEActor::GetProtonTimeFlag) + + .def("SetProtonEnergyFlag", + &GateVoxelizedPromptGammaTLEActor::SetProtonEnergyFlag) + .def("GetProtonEnergyFlag", + &GateVoxelizedPromptGammaTLEActor::GetProtonEnergyFlag) + + .def("SetNeutronEnergyFlag", + &GateVoxelizedPromptGammaTLEActor::SetNeutronEnergyFlag) + .def("GetNeutronEnergyFlag", + &GateVoxelizedPromptGammaTLEActor::GetNeutronEnergyFlag) + + .def("SetNeutronTimeFlag", + &GateVoxelizedPromptGammaTLEActor::SetNeutronTimeFlag) + .def("GetNeutronTimeFlag", + &GateVoxelizedPromptGammaTLEActor::GetNeutronTimeFlag) + + .def_readwrite("fPhysicalVolumeName", + &GateVoxelizedPromptGammaTLEActor::fPhysicalVolumeName) + .def_readwrite("cpp_tof_neutron_image", + &GateVoxelizedPromptGammaTLEActor::cpp_tof_neutron_image) + .def_readwrite("cpp_tof_proton_image", + &GateVoxelizedPromptGammaTLEActor::cpp_tof_proton_image) + .def_readwrite("cpp_E_neutron_image", + &GateVoxelizedPromptGammaTLEActor::cpp_E_neutron_image) + .def_readwrite("cpp_E_proton_image", + &GateVoxelizedPromptGammaTLEActor::cpp_E_proton_image); +} diff --git a/opengate/contrib/vpgTLE/stageX/data/GateMaterials.db b/opengate/contrib/vpgTLE/stageX/data/GateMaterials.db new file mode 100644 index 000000000..d62192708 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/data/GateMaterials.db @@ -0,0 +1,478 @@ +[Elements] +Hydrogen: S= H ; Z= 1. ; A= 1.01 g/mole +Helium: S= He ; Z= 2. ; A= 4.003 g/mole +Lithium: S= Li ; Z= 3. ; A= 6.941 g/mole +Beryllium: S= Be ; Z= 4. ; A= 9.012 g/mole +Boron: S= B ; Z= 5. ; A= 10.811 g/mole +Carbon: S= C ; Z= 6. ; A= 12.01 g/mole +Nitrogen: S= N ; Z= 7. ; A= 14.01 g/mole +Oxygen: S= O ; Z= 8. ; A= 16.00 g/mole +Fluorine: S= F ; Z= 9. ; A= 18.998 g/mole +Neon: S= Ne ; Z= 10. ; A= 20.180 g/mole +Sodium: S= Na ; Z= 11. ; A= 22.99 g/mole +Magnesium: S= Mg ; Z= 12. ; A= 24.305 g/mole +Aluminium: S= Al ; Z= 13. ; A= 26.98 g/mole +Silicon: S= Si ; Z= 14. ; A= 28.09 g/mole +Phosphor: S= P ; Z= 15. ; A= 30.97 g/mole +Sulfur: S= S ; Z= 16. ; A= 32.066 g/mole +Chlorine: S= Cl ; Z= 17. ; A= 35.45 g/mole +Argon: S= Ar ; Z= 18. ; A= 39.95 g/mole +Potassium: S= K ; Z= 19. ; A= 39.098 g/mole +Calcium: S= Ca ; Z= 20. ; A= 40.08 g/mole +Scandium: S= Sc ; Z= 21. ; A= 44.956 g/mole +Titanium: S= Ti ; Z= 22. ; A= 47.867 g/mole +Vandium: S= V ; Z= 23. ; A= 50.942 g/mole +Chromium: S= Cr ; Z= 24. ; A= 51.996 g/mole +Manganese: S= Mn ; Z= 25. ; A= 54.938 g/mole +Iron: S= Fe ; Z= 26. ; A= 55.845 g/mole +Cobalt: S= Co ; Z= 27. ; A= 58.933 g/mole +Nickel: S= Ni ; Z= 28. ; A= 58.693 g/mole +Copper: S= Cu ; Z= 29. ; A= 63.39 g/mole +Zinc: S= Zn ; Z= 30. ; A= 65.39 g/mole +Gallium: S= Ga ; Z= 31. ; A= 69.723 g/mole +Germanium: S= Ge ; Z= 32. ; A= 72.61 g/mole +Yttrium: S= Y ; Z= 39. ; A= 88.91 g/mole +Cadmium: S= Cd ; Z= 48. ; A= 112.41 g/mole +Tellurium: S= Te ; Z= 52. ; A= 127.6 g/mole +Iodine: S= I ; Z= 53. ; A= 126.90 g/mole +Cesium: S= Cs ; Z= 55. ; A= 132.905 g/mole +Gadolinium: S= Gd ; Z= 64. ; A= 157.25 g/mole +Lutetium: S= Lu ; Z= 71. ; A= 174.97 g/mole +Tungsten: S= W ; Z= 74. ; A= 183.84 g/mole +Platin: S= Pt ; Z= 78. ; A= 195.078 g/mole +Thallium: S= Tl ; Z= 81. ; A= 204.37 g/mole +Lead: S= Pb ; Z= 82. ; A= 207.20 g/mole +Bismuth: S= Bi ; Z= 83. ; A= 208.98 g/mole +Uranium: S= U ; Z= 92. ; A= 238.03 g/mole + +[Materials] + +made_up_Ge: d=1000 g/cm3 ; n = 1 + +el: name=Germanium; f=1.000000 + +Water1ppmPt: d=1 g/cm3 ; n = 3 + +el: name=Hydrogen; f=0.111898 + +el: name=Oxygen; f=0.888101 + +el: name=Platin; f=0.000001 + +Water3ppmPt: d=1 g/cm3 ; n = 3 + +el: name=Hydrogen; f=0.111898 + +el: name=Oxygen; f=0.888099 + +el: name=Platin; f=0.000003 + +ppmPtry: d=1 g/cm3 ; n = 3 + +el: name=Hydrogen; f=0.100000 + +el: name=Oxygen; f=0.897000 + +el: name=Platin; f=0.003000 + +Water_3mgI: d=1 g/cm3 ; n = 3 + +el: name=Hydrogen; f=0.111562 + +el: name=Oxygen; f=0.885438 + +el: name=Iodine; f=0.003000 + +Water_50mgI: d=1 g/cm3 ; n = 3 + +el: name=Hydrogen; f=0.106303 + +el: name=Oxygen; f=0.843697 + +el: name=Iodine; f=0.050000 + +Water_300mgI: d=1 g/cm3 ; n = 3 + +el: name=Hydrogen; f=0.078329 + +el: name=Oxygen; f=0.621671 + +el: name=Iodine; f=0.300000 + +Water_3mgGd: d=1 g/cm3 ; n = 3 + +el: name=Hydrogen; f=0.111562 + +el: name=Oxygen; f=0.885438 + +el: name=Gadolinium; f=0.003000 + +H2O_I_Gd_mix: d=1 g/cm3 ; n = 4 + +el: name=Hydrogen; f=0.111562 + +el: name=Oxygen; f=0.885438 + +el: name=Iodine; f=0.001500 + +el: name=Gadolinium; f=0.001500 + +H2O_I_Gd_mix_2: d=1 g/cm3 ; n = 4 + +el: name=Hydrogen; f=0.111227 + +el: name=Oxygen; f=0.882773 + +el: name=Iodine; f=0.003000 + +el: name=Gadolinium; f=0.003000 + +H2O_I_Gd_mix_3: d=1 g/cm3 ; n = 4 + +el: name=Hydrogen; f=0.110779 + +el: name=Oxygen; f=0.879221 + +el: name=Iodine; f=0.005000 + +el: name=Gadolinium; f=0.005000 + +H2O_I_Gd_mix_4: d=1 g/cm3 ; n = 4 + +el: name=Hydrogen; f=0.110220 + +el: name=Oxygen; f=0.874780 + +el: name=Iodine; f=0.007500 + +el: name=Gadolinium; f=0.007500 + +H2O_I_Gd_mix_5: d=1 g/cm3 ; n = 4 + +el: name=Hydrogen; f=0.109660 + +el: name=Oxygen; f=0.870340 + +el: name=Iodine; f=0.010000 + +el: name=Gadolinium; f=0.010000 + +Vacuum: d=0.000001 mg/cm3 ; n=1 + +el: name=Hydrogen ; n=1 + +Aluminium: d=2.7 g/cm3 ; n=1 ; state=solid + +el: name=auto ; n=1 + +Uranium: d=18.90 g/cm3 ; n=1 ; state=solid + +el: name=auto ; n=1 + +Silicon: d=2.33 g/cm3 ; n=1 ; state=solid + +el: name=auto ; n=1 + +Germanium: d=5.32 g/cm3 ; n=1 ; state=solid + +el: name=auto ; n=1 + +Yttrium: d=4.47 g/cm3 ; n=1 + +el: name=auto ; n=1 + +Gadolinium: d=7.9 g/cm3 ; n=1 + +el: name=auto ; n=1 + +Lutetium: d=9.84 g/cm3 ; n=1 + +el: name=auto ; n=1 + +Tungsten: d=19.3 g/cm3 ; n=1 ; state=solid + +el: name=auto ; n=1 + +Lead: d=11.4 g/cm3 ; n=1 ; state=solid + +el: name=auto ; n=1 + +Bismuth: d=9.75 g/cm3 ; n=1 ; state=solid + +el: name=auto ; n=1 + +NaI: d=3.67 g/cm3; n=2; state=solid + +el: name=Sodium ; n=1 + +el: name=Iodine ; n=1 + +NaITl: d=3.67 g/cm3; n=3; state=solid + +el: name=Sodium ; f=0.152 + +el: name=Iodine ; f=0.838 + +el: name=Thallium ; f=0.010 + +PWO: d=8.28 g/cm3; n=3 ; state=Solid + +el: name=Lead; n=1 + +el: name=Tungsten; n=1 + +el: name=Oxygen; n=4 + +BGO: d=7.13 g/cm3; n= 3 ; state=solid + +el: name=Bismuth; n=4 + +el: name=Germanium; n=3 + +el: name=Oxygen; n=12 + +LSO: d=7.4 g/cm3; n=3 ; state=Solid + +el: name=Lutetium ; n=2 + +el: name=Silicon; n=1 + +el: name=Oxygen; n=5 + +Plexiglass: d=1.19 g/cm3; n=3; state=solid + +el: name=Hydrogen; f=0.080538 + +el: name=Carbon; f=0.599848 + +el: name=Oxygen; f=0.319614 + + +GSO: d=6.7 g/cm3; n=3 ; state=Solid + +el: name=Gadolinium ; n=2 + +el: name=Silicon; n=1 + +el: name=Oxygen; n=5 + +LuAP: d=8.34 g/cm3; n=3 ; state=Solid + +el: name=Lutetium ; n=1 + +el: name=Aluminium; n=1 + +el: name=Oxygen; n=3 + +YAP: d=5.55 g/cm3; n=3 ; state=Solid + +el: name=Yttrium ; n=1 + +el: name=Aluminium; n=1 + +el: name=Oxygen; n=3 + +Water: d=1.00 g/cm3; n=2 ; state=liquid + +el: name=Hydrogen ; n=2 + +el: name=Oxygen; n=1 + +Quartz: d=2.2 g/cm3; n=2 ; state=Solid + +el: name=Silicon ; n=1 + +el: name=Oxygen ; n=2 + +Breast: d=1.020 g/cm3 ; n = 8 + +el: name=Oxygen; f=0.5270 + +el: name=Carbon; f=0.3320 + +el: name=Hydrogen ; f=0.1060 + +el: name=Nitrogen; f=0.0300 + +el: name=Sulfur ; f=0.0020 + +el: name=Sodium ; f=0.0010 + +el: name=Phosphor; f=0.0010 + +el: name=Chlorine ; f=0.0010 + +Air: d=1.29 mg/cm3 ; n=4 ; state=gas + +el: name=Nitrogen; f=0.755268 + +el: name=Oxygen; f=0.231781 + +el: name=Argon; f=0.012827 + +el: name=Carbon; f=0.000124 + +Glass: d=2.5 g/cm3; n=4; state=solid + +el: name=Sodium ; f=0.1020 + +el: name=Calcium; f=0.0510 + +el: name=Silicon; f=0.2480 + +el: name=Oxygen; f=0.5990 + +Scinti-C9H10: d=1.032 g/cm3 ; n=2 + +el: name=Carbon; n=9 + +el: name=Hydrogen; n=10 + +LuYAP-70: d=7.1 g/cm3 ; n=4 + +el: name=Lutetium ; n= 7 + +el: name=Yttrium ; n= 3 + +el: name=Aluminium; n=10 + +el: name=Oxygen; n=30 + +LuYAP-80: d=7.5 g/cm3 ; n=4 + +el: name=Lutetium ; n= 8 + +el: name=Yttrium ; n= 2 + +el: name=Aluminium; n=10 + +el: name=Oxygen; n=30 + +Plastic: d=1.18 g/cm3 ; n=3; state=solid + +el: name=Carbon ; n=5 + +el: name=Hydrogen ; n=8 + +el: name=Oxygen ; n=2 + +CZT: d=5.68 g/cm3 ; n=3; state=solid + +el: name=Cadmium ; n=9 + +el: name=Zinc ; n=1 + +el: name=Tellurium ; n=10 + +Lung: d=0.26 g/cm3 ; n=9 + +el: name=Hydrogen ; f=0.103 + +el: name=Carbon ; f=0.105 + +el: name=Nitrogen ; f=0.031 + +el: name=Oxygen ; f=0.749 + +el: name=Sodium ; f=0.002 + +el: name=Phosphor ; f=0.002 + +el: name=Sulfur ; f=0.003 + +el: name=Chlorine ; f=0.003 + +el: name=Potassium ; f=0.002 + +Polyethylene: d=0.96 g/cm3 ; n=2 + +el: name=Hydrogen ; n=2 + +el: name=Carbon ; n=1 + +PVC: d=1.65 g/cm3 ; n=3 ; state=solid + +el: name=Hydrogen ; n=3 + +el: name=Carbon ; n=2 + +el: name=Chlorine ; n=1 + +SS304: d=7.92 g/cm3 ; n=4 ; state=solid + +el: name=Iron ; f=0.695 + +el: name=Chromium ; f=0.190 + +el: name=Nickel ; f=0.095 + +el: name=Manganese ; f=0.020 + +PTFE: d= 2.18 g/cm3 ; n=2 ; state=solid + +el: name=Carbon ; n=1 + +el: name=Fluorine ; n=2 + + +LYSO: d=5.37 g/cm3; n=4 ; state=Solid + +el: name=Lutetium ; f=0.31101534 + +el: name=Yttrium ; f=0.368765605 + +el: name=Silicon; f=0.083209699 + +el: name=Oxygen; f=0.237009356 + +Body: d=1.00 g/cm3 ; n=2 + +el: name=Hydrogen ; f=0.112 + +el: name=Oxygen ; f=0.888 + +Muscle: d=1.05 g/cm3 ; n=9 + +el: name=Hydrogen ; f=0.102 + +el: name=Carbon ; f=0.143 + +el: name=Nitrogen ; f=0.034 + +el: name=Oxygen ; f=0.71 + +el: name=Sodium ; f=0.001 + +el: name=Phosphor ; f=0.002 + +el: name=Sulfur ; f=0.003 + +el: name=Chlorine ; f=0.001 + +el: name=Potassium ; f=0.004 + +LungMoby: d=0.30 g/cm3 ; n=6 + +el: name=Hydrogen ; f=0.099 + +el: name=Carbon ; f=0.100 + +el: name=Nitrogen ; f=0.028 + +el: name=Oxygen ; f=0.740 + +el: name=Phosphor ; f=0.001 + +el: name=Calcium ; f=0.032 + +SpineBone: d=1.42 g/cm3 ; n=11 + +el: name=Hydrogen ; f=0.063 + +el: name=Carbon ; f=0.261 + +el: name=Nitrogen ; f=0.039 + +el: name=Oxygen ; f=0.436 + +el: name=Sodium ; f=0.001 + +el: name=Magnesium ; f=0.001 + +el: name=Phosphor ; f=0.061 + +el: name=Sulfur ; f=0.003 + +el: name=Chlorine ; f=0.001 + +el: name=Potassium ; f=0.001 + +el: name=Calcium ; f=0.133 + +RibBone: d=1.92 g/cm3 ; n=9 + +el: name=Hydrogen ; f=0.034 + +el: name=Carbon ; f=0.155 + +el: name=Nitrogen ; f=0.042 + +el: name=Oxygen ; f=0.435 + +el: name=Sodium ; f=0.001 + +el: name=Magnesium ; f=0.002 + +el: name=Phosphor ; f=0.103 + +el: name=Sulfur ; f=0.003 + +el: name=Calcium ; f=0.225 + +Adipose: d=0.92 g/cm3 ; n=6 + +el: name=Hydrogen ; f=0.120 + +el: name=Carbon ; f=0.640 + +el: name=Nitrogen ; f=0.008 + +el: name=Oxygen ; f=0.229 + +el: name=Phosphor ; f=0.002 + +el: name=Calcium ; f=0.001 + +Blood: d=1.06 g/cm3 ; n=10 + +el: name=Hydrogen ; f=0.102 + +el: name=Carbon ; f=0.11 + +el: name=Nitrogen ; f=0.033 + +el: name=Oxygen ; f=0.745 + +el: name=Sodium ; f=0.001 + +el: name=Phosphor ; f=0.001 + +el: name=Sulfur ; f=0.002 + +el: name=Chlorine ; f=0.003 + +el: name=Potassium ; f=0.002 + +el: name=Iron ; f=0.001 + +Heart: d=1.05 g/cm3 ; n=9 + +el: name=Hydrogen ; f=0.104 + +el: name=Carbon ; f=0.139 + +el: name=Nitrogen ; f=0.029 + +el: name=Oxygen ; f=0.718 + +el: name=Sodium ; f=0.001 + +el: name=Phosphor ; f=0.002 + +el: name=Sulfur ; f=0.002 + +el: name=Chlorine ; f=0.002 + +el: name=Potassium ; f=0.003 + +Kidney: d=1.05 g/cm3 ; n=10 + +el: name=Hydrogen ; f=0.103 + +el: name=Carbon ; f=0.132 + +el: name=Nitrogen ; f=0.03 + +el: name=Oxygen ; f=0.724 + +el: name=Sodium ; f=0.002 + +el: name=Phosphor ; f=0.002 + +el: name=Sulfur ; f=0.002 + +el: name=Chlorine ; f=0.002 + +el: name=Potassium ; f=0.002 + +el: name=Calcium ; f=0.001 + +Liver: d=1.06 g/cm3 ; n=9 + +el: name=Hydrogen ; f=0.102 + +el: name=Carbon ; f=0.139 + +el: name=Nitrogen ; f=0.03 + +el: name=Oxygen ; f=0.716 + +el: name=Sodium ; f=0.002 + +el: name=Phosphor ; f=0.003 + +el: name=Sulfur ; f=0.003 + +el: name=Chlorine ; f=0.002 + +el: name=Potassium ; f=0.003 + +Lymph: d=1.03 g/cm3 ; n=7 + +el: name=Hydrogen ; f=0.108 + +el: name=Carbon ; f=0.041 + +el: name=Nitrogen ; f=0.011 + +el: name=Oxygen ; f=0.832 + +el: name=Sodium ; f=0.003 + +el: name=Sulfur ; f=0.001 + +el: name=Chlorine ; f=0.004 + +Pancreas: d=1.04 g/cm3 ; n=9 + +el: name=Hydrogen ; f=0.106 + +el: name=Carbon ; f=0.169 + +el: name=Nitrogen ; f=0.022 + +el: name=Oxygen ; f=0.694 + +el: name=Sodium ; f=0.002 + +el: name=Phosphor ; f=0.002 + +el: name=Sulfur ; f=0.001 + +el: name=Chlorine ; f=0.002 + +el: name=Potassium ; f=0.002 + +Intestine: d=1.03 g/cm3 ; n=9 + +el: name=Hydrogen ; f=0.106 + +el: name=Carbon ; f=0.115 + +el: name=Nitrogen ; f=0.022 + +el: name=Oxygen ; f=0.751 + +el: name=Sodium ; f=0.001 + +el: name=Phosphor ; f=0.001 + +el: name=Sulfur ; f=0.001 + +el: name=Chlorine ; f=0.002 + +el: name=Potassium ; f=0.001 + +Skull: d=1.61 g/cm3 ; n=9 + +el: name=Hydrogen ; f=0.05 + +el: name=Carbon ; f=0.212 + +el: name=Nitrogen ; f=0.04 + +el: name=Oxygen ; f=0.435 + +el: name=Sodium ; f=0.001 + +el: name=Magnesium ; f=0.002 + +el: name=Phosphor ; f=0.081 + +el: name=Sulfur ; f=0.003 + +el: name=Calcium ; f=0.176 + +Cartilage: d=1.10 g/cm3 ; n=8 + +el: name=Hydrogen ; f=0.096 + +el: name=Carbon ; f=0.099 + +el: name=Nitrogen ; f=0.022 + +el: name=Oxygen ; f=0.744 + +el: name=Sodium ; f=0.005 + +el: name=Phosphor ; f=0.022 + +el: name=Sulfur ; f=0.009 + +el: name=Chlorine ; f=0.003 + +Brain: d=1.04 g/cm3 ; n=9 + +el: name=Hydrogen ; f=0.107 + +el: name=Carbon ; f=0.145 + +el: name=Nitrogen ; f=0.022 + +el: name=Oxygen ; f=0.712 + +el: name=Sodium ; f=0.002 + +el: name=Phosphor ; f=0.004 + +el: name=Sulfur ; f=0.002 + +el: name=Chlorine ; f=0.003 + +el: name=Potassium ; f=0.003 + +Spleen: d=1.06 g/cm3 ; n=9 + +el: name=Hydrogen ; f=0.103 + +el: name=Carbon ; f=0.113 + +el: name=Nitrogen ; f=0.032 + +el: name=Oxygen ; f=0.741 + +el: name=Sodium ; f=0.001 + +el: name=Phosphor ; f=0.003 + +el: name=Sulfur ; f=0.002 + +el: name=Chlorine ; f=0.002 + +el: name=Potassium ; f=0.003 + +Testis: d=1.04 g/cm3 ; n=9 + +el: name=Hydrogen ; f=0.106000 + +el: name=Carbon ; f=0.099000 + +el: name=Nitrogen ; f=0.020000 + +el: name=Oxygen ; f=0.766000 + +el: name=Sodium ; f=0.002000 + +el: name=Phosphor ; f=0.001000 + +el: name=Sulfur ; f=0.002000 + +el: name=Chlorine ; f=0.002000 + +el: name=Potassium ; f=0.002000 + +PMMA: d=1.195 g/cm3; n=3 ; state=Solid + +el: name=Hydrogen ; f=0.080541 + +el: name=Carbon ; f=0.599846 + +el: name=Sulfur; f=0.319613 diff --git a/opengate/contrib/vpgTLE/stageX/data/GateMaterialsElements.db b/opengate/contrib/vpgTLE/stageX/data/GateMaterialsElements.db new file mode 100644 index 000000000..3bfc007bb --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/data/GateMaterialsElements.db @@ -0,0 +1,52 @@ +[Materials] + +Hydrogen: d=1.0 g/cm3; n=1; state=Solid + +el: name=Hydrogen; n=1 +Helium: d=1.0 g/cm3; n=1; state=Solid + +el: name=Helium; n=1 +Lithium: d=1.0 g/cm3; n=1; state=Solid + +el: name=Lithium; n=1 +Beryllium: d=1.0 g/cm3; n=1; state=Solid + +el: name=Beryllium; n=1 +Boron: d=1.0 g/cm3; n=1; state=Solid + +el: name=Boron; n=1 +Carbon: d=1.0 g/cm3; n=1; state=Solid + +el: name=Carbon; n=1 +Nitrogen: d=1.0 g/cm3; n=1; state=Solid + +el: name=Nitrogen; n=1 +Oxygen: d=1.0 g/cm3; n=1; state=Solid + +el: name=Oxygen; n=1 +Fluorine: d=1.0 g/cm3; n=1; state=Solid + +el: name=Fluorine; n=1 +Neon: d=1.0 g/cm3; n=1; state=Solid + +el: name=Neon; n=1 +Sodium: d=1.0 g/cm3; n=1; state=Solid + +el: name=Sodium; n=1 +Magnesium: d=1.0 g/cm3; n=1; state=Solid + +el: name=Magnesium; n=1 +Aluminium: d=1.0 g/cm3; n=1; state=Solid + +el: name=Aluminium; n=1 +Silicon: d=1.0 g/cm3; n=1; state=Solid + +el: name=Silicon; n=1 +Phosphor: d=1.0 g/cm3; n=1; state=Solid + +el: name=Phosphor; n=1 +Sulfur: d=1.0 g/cm3; n=1; state=Solid + +el: name=Sulfur; n=1 +Chlorine: d=1.0 g/cm3; n=1; state=Solid + +el: name=Chlorine; n=1 +Argon: d=1.0 g/cm3; n=1; state=Solid + +el: name=Argon; n=1 +Potassium: d=1.0 g/cm3; n=1; state=Solid + +el: name=Potassium; n=1 +Calcium: d=1.0 g/cm3; n=1; state=Solid + +el: name=Calcium; n=1 +Titanium: d=1.0 g/cm3; n=1; state=Solid + +el: name=Titanium; n=1 +Copper: d=1.0 g/cm3; n=1; state=Solid + +el: name=Copper; n=1 +Zinc: d=1.0 g/cm3; n=1; state=Solid + +el: name=Zinc; n=1 +Silver: d=1.0 g/cm3; n=1; state=Solid + +el: name=Silver; n=1 +Tin: d=1.0 g/cm3; n=1; state=Solid + +el: name=Tin; n=1 diff --git a/opengate/contrib/vpgTLE/stageX/data/Materials.xml b/opengate/contrib/vpgTLE/stageX/data/Materials.xml new file mode 100644 index 000000000..403917efd --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/data/Materials.xml @@ -0,0 +1,849 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opengate/contrib/vpgTLE/stageX/data/Schneider2000DensitiesTable.txt b/opengate/contrib/vpgTLE/stageX/data/Schneider2000DensitiesTable.txt new file mode 100644 index 000000000..1e2ad7436 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/data/Schneider2000DensitiesTable.txt @@ -0,0 +1,14 @@ +# =================== +# HU density g/cm3 +# =================== +-1000 1.21e-3 +-98 0.93 +-97 0.930486 +0 1.0 +14 1.03 +23 1.031 +100 1.119900 +101 1.076200 +1600 1.964200 +3000 2.8 + diff --git a/opengate/contrib/vpgTLE/stageX/data/Schneider2000MaterialsTable.txt b/opengate/contrib/vpgTLE/stageX/data/Schneider2000MaterialsTable.txt new file mode 100644 index 000000000..fd77c5ea5 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/data/Schneider2000MaterialsTable.txt @@ -0,0 +1,36 @@ +[Elements] +Hydrogen Carbon Nitrogen Oxygen Sodium Magnesium Phosphor Sulfur +Chlorine Argon Potassium Calcium +Titanium Copper Zinc Silver Tin Lithium Neon Aluminium Beryllium Boron Fluorine Helium Silicon +[/Elements] +# =================================================================================================================== +# HU H C N O Na Mg P S Cl Ar K Ca Ti Cu Zn Ag Sn Li Ne Al Be B F He Si +# =================================================================================================================== + -1050 0 0 75.5 23.2 0 0 0 0 0 1.3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 Air + -950 10.3 10.5 3.1 74.9 0.2 0 0.2 0.3 0.3 0 0.2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 Lung + -120 11.6 68.1 0.2 19.8 0.1 0 0 0.1 0.1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 AT_AG_SI1 + -82 11.3 56.7 0.9 30.8 0.1 0 0 0.1 0.1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 AT_AG_SI2 + -52 11.0 45.8 1.5 41.1 0.1 0 0.1 0.2 0.2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 AT_AG_SI3 + -22 10.8 35.6 2.2 50.9 0 0 0.1 0.2 0.2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 AT_AG_SI4 + 0 66.6 0 0 33.4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 Water + 8 10.6 28.4 2.6 57.8 0 0 0.1 0.2 0.2 0 0.1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 AT_AG_SI5 + 19 10.3 13.4 3.0 72.3 0.2 0 0.2 0.2 0.2 0 0.2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 SoftTissus + 80 9.4 20.7 6.2 62.2 0.6 0 0 0.6 0.3 0 0.0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ConnectiveTissue + 120 9.5 45.5 2.5 35.5 0.1 0 2.1 0.1 0.1 0 0.1 4.5 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone01 + 200 8.9 42.3 2.7 36.3 0.1 0 3.0 0.1 0.1 0 0.1 6.4 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone02 + 300 8.2 39.1 2.9 37.2 0.1 0 3.9 0.1 0.1 0 0.1 8.3 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone03 + 400 7.6 36.1 3.0 38.0 0.1 0.1 4.7 0.2 0.1 0 0 10.1 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone04 + 500 7.1 33.5 3.2 38.7 0.1 0.1 5.4 0.2 0 0 0 11.7 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone05 + 600 6.6 31.0 3.3 39.4 0.1 0.1 6.1 0.2 0 0 0 13.2 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone06 + 700 6.1 28.7 3.5 40.0 0.1 0.1 6.7 0.2 0 0 0 14.6 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone07 + 800 5.6 26.5 3.6 40.5 0.1 0.2 7.3 0.3 0 0 0 15.9 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone08 + 900 5.2 24.6 3.7 41.1 0.1 0.2 7.8 0.3 0 0 0 17.0 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone09 + 1000 4.9 22.7 3.8 41.6 0.1 0.2 8.3 0.3 0 0 0 18.1 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone10 + 1100 4.5 21.0 3.9 42.0 0.1 0.2 8.8 0.3 0 0 0 19.2 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone11 + 1200 4.2 19.4 4.0 42.5 0.1 0.2 9.2 0.3 0 0 0 20.1 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone12 + 1300 3.9 17.9 4.1 42.9 0.1 0.2 9.6 0.3 0 0 0 21.0 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone13 + 1400 3.6 16.5 4.2 43.2 0.1 0.2 10.0 0.3 0 0 0 21.9 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone14 + 1500 3.4 15.5 4.2 43.5 0.1 0.2 10.3 0.3 0 0 0 22.5 0 0 0 0 0 0 0 0 0 0 0 0 0 Marrow_Bone15 + 1640 0 0 0 0 0 0 0 0 0 0 0 0 0 4 2 65 29 0 0 0 0 0 0 0 0 AmalgamTooth + 2300 0 0 0 0 0 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 0 0 0 0 0 MetallImplants + 3000 0 0 0 0 0 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 0 0 0 0 0 MetallImplants \ No newline at end of file diff --git a/opengate/contrib/vpgTLE/stageX/data/database.db b/opengate/contrib/vpgTLE/stageX/data/database.db new file mode 100644 index 000000000..4b2fb20fd --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/data/database.db @@ -0,0 +1,690 @@ +[Materials] +Air_0: d=1.21 mg/cm3; n=3 ++el: name=N; f=0.755 ++el: name=O; f=0.23199999999999998 ++el: name=Ar; f=0.013000000000000001 + +Lung_1: d=77.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_2: d=127.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_3: d=177.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_4: d=227.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_5: d=277.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_6: d=327.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_7: d=377.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_8: d=427.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_9: d=477.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_10: d=527.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_11: d=577.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_12: d=627.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_13: d=677.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_14: d=727.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_15: d=777.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_16: d=827.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_17: d=877.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_18: d=905.021 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +AT_AG_SI1_19: d=926.911 mg/cm3; n=7 ++el: name=H; f=0.116 ++el: name=C; f=0.681 ++el: name=N; f=0.002 ++el: name=O; f=0.198 ++el: name=Na; f=0.001 ++el: name=S; f=0.001 ++el: name=Cl; f=0.001 + +AT_AG_SI2_20: d=951.985 mg/cm3; n=7 ++el: name=H; f=0.11300000000000002 ++el: name=C; f=0.5670000000000001 ++el: name=N; f=0.009000000000000001 ++el: name=O; f=0.30800000000000005 ++el: name=Na; f=0.0010000000000000002 ++el: name=S; f=0.0010000000000000002 ++el: name=Cl; f=0.0010000000000000002 + +AT_AG_SI3_21: d=973.484 mg/cm3; n=8 ++el: name=H; f=0.11 ++el: name=C; f=0.45799999999999996 ++el: name=N; f=0.015 ++el: name=O; f=0.41100000000000003 ++el: name=Na; f=0.001 ++el: name=P; f=0.001 ++el: name=S; f=0.002 ++el: name=Cl; f=0.002 + +AT_AG_SI4_22: d=992.117 mg/cm3; n=7 ++el: name=H; f=0.10800000000000001 ++el: name=C; f=0.35600000000000004 ++el: name=N; f=0.022000000000000002 ++el: name=O; f=0.509 ++el: name=P; f=0.001 ++el: name=S; f=0.002 ++el: name=Cl; f=0.002 + +Water_23: d=1.00857 g/cm3 ; n=2 ++el: name=H; f=0.666 ++el: name=O; f=0.334 + +AT_AG_SI5_24: d=1.02893 g/cm3 ; n=8 ++el: name=H; f=0.106 ++el: name=C; f=0.284 ++el: name=N; f=0.026000000000000002 ++el: name=O; f=0.578 ++el: name=P; f=0.001 ++el: name=S; f=0.002 ++el: name=Cl; f=0.002 ++el: name=K; f=0.001 + +SoftTissus_25: d=1.05296 g/cm3 ; n=9 ++el: name=H; f=0.10300000000000002 ++el: name=C; f=0.134 ++el: name=N; f=0.030000000000000002 ++el: name=O; f=0.723 ++el: name=Na; f=0.0020000000000000005 ++el: name=P; f=0.0020000000000000005 ++el: name=S; f=0.0020000000000000005 ++el: name=Cl; f=0.0020000000000000005 ++el: name=K; f=0.0020000000000000005 + +SoftTissus_26: d=1.08817 g/cm3 ; n=9 ++el: name=H; f=0.10300000000000002 ++el: name=C; f=0.134 ++el: name=N; f=0.030000000000000002 ++el: name=O; f=0.723 ++el: name=Na; f=0.0020000000000000005 ++el: name=P; f=0.0020000000000000005 ++el: name=S; f=0.0020000000000000005 ++el: name=Cl; f=0.0020000000000000005 ++el: name=K; f=0.0020000000000000005 + +ConnectiveTissue_27: d=1.1199 g/cm3 ; n=7 ++el: name=H; f=0.09400000000000001 ++el: name=C; f=0.20700000000000002 ++el: name=N; f=0.06200000000000001 ++el: name=O; f=0.6220000000000001 ++el: name=Na; f=0.006000000000000001 ++el: name=S; f=0.006000000000000001 ++el: name=Cl; f=0.0030000000000000005 + +Marrow_Bone01_28: d=1.11115 g/cm3 ; n=10 ++el: name=H; f=0.09499999999999997 ++el: name=C; f=0.4549999999999999 ++el: name=N; f=0.024999999999999994 ++el: name=O; f=0.355 ++el: name=Na; f=0.0009999999999999998 ++el: name=P; f=0.021 ++el: name=S; f=0.0009999999999999998 ++el: name=Cl; f=0.0009999999999999998 ++el: name=K; f=0.0009999999999999998 ++el: name=Ca; f=0.045 + +Marrow_Bone02_29: d=1.15985 g/cm3 ; n=10 ++el: name=H; f=0.089 ++el: name=C; f=0.4229999999999999 ++el: name=N; f=0.026999999999999996 ++el: name=O; f=0.363 ++el: name=Na; f=0.0009999999999999998 ++el: name=P; f=0.029999999999999995 ++el: name=S; f=0.0009999999999999998 ++el: name=Cl; f=0.0009999999999999998 ++el: name=K; f=0.0009999999999999998 ++el: name=Ca; f=0.06399999999999999 + +Marrow_Bone02_30: d=1.18947 g/cm3 ; n=10 ++el: name=H; f=0.089 ++el: name=C; f=0.4229999999999999 ++el: name=N; f=0.026999999999999996 ++el: name=O; f=0.363 ++el: name=Na; f=0.0009999999999999998 ++el: name=P; f=0.029999999999999995 ++el: name=S; f=0.0009999999999999998 ++el: name=Cl; f=0.0009999999999999998 ++el: name=K; f=0.0009999999999999998 ++el: name=Ca; f=0.06399999999999999 + +Marrow_Bone03_31: d=1.21909 g/cm3 ; n=10 ++el: name=H; f=0.08199999999999998 ++el: name=C; f=0.39099999999999985 ++el: name=N; f=0.02899999999999999 ++el: name=O; f=0.372 ++el: name=Na; f=0.0009999999999999998 ++el: name=P; f=0.03899999999999999 ++el: name=S; f=0.0009999999999999998 ++el: name=Cl; f=0.0009999999999999998 ++el: name=K; f=0.0009999999999999998 ++el: name=Ca; f=0.08299999999999999 + +Marrow_Bone03_32: d=1.24871 g/cm3 ; n=10 ++el: name=H; f=0.08199999999999998 ++el: name=C; f=0.39099999999999985 ++el: name=N; f=0.02899999999999999 ++el: name=O; f=0.372 ++el: name=Na; f=0.0009999999999999998 ++el: name=P; f=0.03899999999999999 ++el: name=S; f=0.0009999999999999998 ++el: name=Cl; f=0.0009999999999999998 ++el: name=K; f=0.0009999999999999998 ++el: name=Ca; f=0.08299999999999999 + +Marrow_Bone04_33: d=1.27833 g/cm3 ; n=10 ++el: name=H; f=0.076 ++el: name=C; f=0.361 ++el: name=N; f=0.03 ++el: name=O; f=0.37999999999999995 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.001 ++el: name=P; f=0.04699999999999999 ++el: name=S; f=0.002 ++el: name=Cl; f=0.001 ++el: name=Ca; f=0.10099999999999998 + +Marrow_Bone04_34: d=1.30795 g/cm3 ; n=10 ++el: name=H; f=0.076 ++el: name=C; f=0.361 ++el: name=N; f=0.03 ++el: name=O; f=0.37999999999999995 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.001 ++el: name=P; f=0.04699999999999999 ++el: name=S; f=0.002 ++el: name=Cl; f=0.001 ++el: name=Ca; f=0.10099999999999998 + +Marrow_Bone05_35: d=1.33757 g/cm3 ; n=9 ++el: name=H; f=0.071 ++el: name=C; f=0.335 ++el: name=N; f=0.032 ++el: name=O; f=0.387 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.001 ++el: name=P; f=0.054000000000000006 ++el: name=S; f=0.002 ++el: name=Ca; f=0.11699999999999999 + +Marrow_Bone05_36: d=1.36719 g/cm3 ; n=9 ++el: name=H; f=0.071 ++el: name=C; f=0.335 ++el: name=N; f=0.032 ++el: name=O; f=0.387 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.001 ++el: name=P; f=0.054000000000000006 ++el: name=S; f=0.002 ++el: name=Ca; f=0.11699999999999999 + +Marrow_Bone06_37: d=1.39681 g/cm3 ; n=9 ++el: name=H; f=0.066 ++el: name=C; f=0.31000000000000005 ++el: name=N; f=0.033 ++el: name=O; f=0.394 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0010000000000000002 ++el: name=P; f=0.061000000000000006 ++el: name=S; f=0.0020000000000000005 ++el: name=Ca; f=0.132 + +Marrow_Bone06_38: d=1.42642 g/cm3 ; n=9 ++el: name=H; f=0.066 ++el: name=C; f=0.31000000000000005 ++el: name=N; f=0.033 ++el: name=O; f=0.394 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0010000000000000002 ++el: name=P; f=0.061000000000000006 ++el: name=S; f=0.0020000000000000005 ++el: name=Ca; f=0.132 + +Marrow_Bone07_39: d=1.45604 g/cm3 ; n=9 ++el: name=H; f=0.061000000000000006 ++el: name=C; f=0.28700000000000003 ++el: name=N; f=0.035 ++el: name=O; f=0.4000000000000001 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0010000000000000002 ++el: name=P; f=0.06700000000000002 ++el: name=S; f=0.0020000000000000005 ++el: name=Ca; f=0.14600000000000002 + +Marrow_Bone07_40: d=1.48566 g/cm3 ; n=9 ++el: name=H; f=0.061000000000000006 ++el: name=C; f=0.28700000000000003 ++el: name=N; f=0.035 ++el: name=O; f=0.4000000000000001 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0010000000000000002 ++el: name=P; f=0.06700000000000002 ++el: name=S; f=0.0020000000000000005 ++el: name=Ca; f=0.14600000000000002 + +Marrow_Bone08_41: d=1.51528 g/cm3 ; n=9 ++el: name=H; f=0.055999999999999994 ++el: name=C; f=0.265 ++el: name=N; f=0.036000000000000004 ++el: name=O; f=0.405 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.073 ++el: name=S; f=0.003 ++el: name=Ca; f=0.159 + +Marrow_Bone08_42: d=1.5449 g/cm3 ; n=9 ++el: name=H; f=0.055999999999999994 ++el: name=C; f=0.265 ++el: name=N; f=0.036000000000000004 ++el: name=O; f=0.405 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.073 ++el: name=S; f=0.003 ++el: name=Ca; f=0.159 + +Marrow_Bone09_43: d=1.57452 g/cm3 ; n=9 ++el: name=H; f=0.052 ++el: name=C; f=0.246 ++el: name=N; f=0.037 ++el: name=O; f=0.411 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.078 ++el: name=S; f=0.0029999999999999996 ++el: name=Ca; f=0.16999999999999998 + +Marrow_Bone09_44: d=1.60414 g/cm3 ; n=9 ++el: name=H; f=0.052 ++el: name=C; f=0.246 ++el: name=N; f=0.037 ++el: name=O; f=0.411 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.078 ++el: name=S; f=0.0029999999999999996 ++el: name=Ca; f=0.16999999999999998 + +Marrow_Bone10_45: d=1.63376 g/cm3 ; n=9 ++el: name=H; f=0.049 ++el: name=C; f=0.22699999999999998 ++el: name=N; f=0.038 ++el: name=O; f=0.41600000000000004 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.083 ++el: name=S; f=0.003 ++el: name=Ca; f=0.18100000000000002 + +Marrow_Bone10_46: d=1.66338 g/cm3 ; n=9 ++el: name=H; f=0.049 ++el: name=C; f=0.22699999999999998 ++el: name=N; f=0.038 ++el: name=O; f=0.41600000000000004 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.083 ++el: name=S; f=0.003 ++el: name=Ca; f=0.18100000000000002 + +Marrow_Bone11_47: d=1.693 g/cm3 ; n=9 ++el: name=H; f=0.045 ++el: name=C; f=0.21 ++el: name=N; f=0.039 ++el: name=O; f=0.42 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.08800000000000001 ++el: name=S; f=0.003 ++el: name=Ca; f=0.192 + +Marrow_Bone11_48: d=1.72262 g/cm3 ; n=9 ++el: name=H; f=0.045 ++el: name=C; f=0.21 ++el: name=N; f=0.039 ++el: name=O; f=0.42 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.08800000000000001 ++el: name=S; f=0.003 ++el: name=Ca; f=0.192 + +Marrow_Bone12_49: d=1.75224 g/cm3 ; n=9 ++el: name=H; f=0.042 ++el: name=C; f=0.19399999999999998 ++el: name=N; f=0.04 ++el: name=O; f=0.425 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.092 ++el: name=S; f=0.003 ++el: name=Ca; f=0.201 + +Marrow_Bone12_50: d=1.78186 g/cm3 ; n=9 ++el: name=H; f=0.042 ++el: name=C; f=0.19399999999999998 ++el: name=N; f=0.04 ++el: name=O; f=0.425 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.092 ++el: name=S; f=0.003 ++el: name=Ca; f=0.201 + +Marrow_Bone13_51: d=1.81148 g/cm3 ; n=9 ++el: name=H; f=0.03900000000000001 ++el: name=C; f=0.17900000000000002 ++el: name=N; f=0.041 ++el: name=O; f=0.42900000000000005 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0020000000000000005 ++el: name=P; f=0.09600000000000002 ++el: name=S; f=0.0030000000000000005 ++el: name=Ca; f=0.21000000000000002 + +Marrow_Bone13_52: d=1.8411 g/cm3 ; n=9 ++el: name=H; f=0.03900000000000001 ++el: name=C; f=0.17900000000000002 ++el: name=N; f=0.041 ++el: name=O; f=0.42900000000000005 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0020000000000000005 ++el: name=P; f=0.09600000000000002 ++el: name=S; f=0.0030000000000000005 ++el: name=Ca; f=0.21000000000000002 + +Marrow_Bone14_53: d=1.87072 g/cm3 ; n=9 ++el: name=H; f=0.036000000000000004 ++el: name=C; f=0.165 ++el: name=N; f=0.042 ++el: name=O; f=0.43200000000000005 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.1 ++el: name=S; f=0.003 ++el: name=Ca; f=0.21899999999999997 + +Marrow_Bone14_54: d=1.90034 g/cm3 ; n=9 ++el: name=H; f=0.036000000000000004 ++el: name=C; f=0.165 ++el: name=N; f=0.042 ++el: name=O; f=0.43200000000000005 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.1 ++el: name=S; f=0.003 ++el: name=Ca; f=0.21899999999999997 + +Marrow_Bone15_55: d=1.92991 g/cm3 ; n=9 ++el: name=H; f=0.034 ++el: name=C; f=0.15500000000000003 ++el: name=N; f=0.04200000000000001 ++el: name=O; f=0.43500000000000005 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0020000000000000005 ++el: name=P; f=0.10300000000000002 ++el: name=S; f=0.0030000000000000005 ++el: name=Ca; f=0.22500000000000003 + +Marrow_Bone15_56: d=1.97143 g/cm3 ; n=9 ++el: name=H; f=0.034 ++el: name=C; f=0.15500000000000003 ++el: name=N; f=0.04200000000000001 ++el: name=O; f=0.43500000000000005 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0020000000000000005 ++el: name=P; f=0.10300000000000002 ++el: name=S; f=0.0030000000000000005 ++el: name=Ca; f=0.22500000000000003 + +AmalgamTooth_57: d=2.01308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_58: d=2.06308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_59: d=2.11308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_60: d=2.16308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_61: d=2.21308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_62: d=2.26308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_63: d=2.31308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_64: d=2.36009 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +MetallImplants_65: d=2.4071 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_66: d=2.4571 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_67: d=2.5071 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_68: d=2.5571 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_69: d=2.6071 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_70: d=2.6571 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_71: d=2.7071 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_72: d=2.7571 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_73: d=2.79105 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_74: d=2.8 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + diff --git a/opengate/contrib/vpgTLE/stageX/data/database.txt b/opengate/contrib/vpgTLE/stageX/data/database.txt new file mode 100644 index 000000000..d7d94a8bf --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/data/database.txt @@ -0,0 +1,686 @@ +[Materials] +Air_0: d=1.21 mg/cm3; n=3 ++el: name=N; f=0.755 ++el: name=O; f=0.23199999999999998 ++el: name=Ar; f=0.013000000000000001 + +Lung_1: d=77.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_2: d=127.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_3: d=177.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_4: d=227.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_5: d=277.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_6: d=327.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_7: d=377.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_8: d=427.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_9: d=477.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_10: d=527.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_11: d=577.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_12: d=627.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_13: d=677.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_14: d=727.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_15: d=777.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_16: d=827.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_17: d=877.695 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +Lung_18: d=905.021 mg/cm3; n=9 ++el: name=H; f=0.103 ++el: name=C; f=0.10499999999999998 ++el: name=N; f=0.030999999999999996 ++el: name=O; f=0.749 ++el: name=Na; f=0.002 ++el: name=P; f=0.002 ++el: name=S; f=0.0029999999999999996 ++el: name=Cl; f=0.0029999999999999996 ++el: name=K; f=0.002 + +AT_AG_SI1_19: d=926.911 mg/cm3; n=7 ++el: name=H; f=0.116 ++el: name=C; f=0.681 ++el: name=N; f=0.002 ++el: name=O; f=0.198 ++el: name=Na; f=0.001 ++el: name=S; f=0.001 ++el: name=Cl; f=0.001 + +AT_AG_SI2_20: d=957.382 mg/cm3; n=7 ++el: name=H; f=0.11300000000000002 ++el: name=C; f=0.5670000000000001 ++el: name=N; f=0.009000000000000001 ++el: name=O; f=0.30800000000000005 ++el: name=Na; f=0.0010000000000000002 ++el: name=S; f=0.0010000000000000002 ++el: name=Cl; f=0.0010000000000000002 + +AT_AG_SI3_21: d=984.277 mg/cm3; n=8 ++el: name=H; f=0.11 ++el: name=C; f=0.45799999999999996 ++el: name=N; f=0.015 ++el: name=O; f=0.41100000000000003 ++el: name=Na; f=0.001 ++el: name=P; f=0.001 ++el: name=S; f=0.002 ++el: name=Cl; f=0.002 + +AT_AG_SI4_22: d=1.01117 g/cm3 ; n=7 ++el: name=H; f=0.10800000000000001 ++el: name=C; f=0.35600000000000004 ++el: name=N; f=0.022000000000000002 ++el: name=O; f=0.509 ++el: name=P; f=0.001 ++el: name=S; f=0.002 ++el: name=Cl; f=0.002 + +AT_AG_SI5_23: d=1.02955 g/cm3 ; n=8 ++el: name=H; f=0.106 ++el: name=C; f=0.284 ++el: name=N; f=0.026000000000000002 ++el: name=O; f=0.578 ++el: name=P; f=0.001 ++el: name=S; f=0.002 ++el: name=Cl; f=0.002 ++el: name=K; f=0.001 + +SoftTissus_24: d=1.05296 g/cm3 ; n=9 ++el: name=H; f=0.10300000000000002 ++el: name=C; f=0.134 ++el: name=N; f=0.030000000000000002 ++el: name=O; f=0.723 ++el: name=Na; f=0.0020000000000000005 ++el: name=P; f=0.0020000000000000005 ++el: name=S; f=0.0020000000000000005 ++el: name=Cl; f=0.0020000000000000005 ++el: name=K; f=0.0020000000000000005 + +SoftTissus_25: d=1.08817 g/cm3 ; n=9 ++el: name=H; f=0.10300000000000002 ++el: name=C; f=0.134 ++el: name=N; f=0.030000000000000002 ++el: name=O; f=0.723 ++el: name=Na; f=0.0020000000000000005 ++el: name=P; f=0.0020000000000000005 ++el: name=S; f=0.0020000000000000005 ++el: name=Cl; f=0.0020000000000000005 ++el: name=K; f=0.0020000000000000005 + +ConnectiveTissue_26: d=1.1199 g/cm3 ; n=7 ++el: name=H; f=0.09400000000000001 ++el: name=C; f=0.20700000000000002 ++el: name=N; f=0.06200000000000001 ++el: name=O; f=0.6220000000000001 ++el: name=Na; f=0.006000000000000001 ++el: name=S; f=0.006000000000000001 ++el: name=Cl; f=0.0030000000000000005 + +Marrow_Bone01_27: d=1.11115 g/cm3 ; n=10 ++el: name=H; f=0.09499999999999997 ++el: name=C; f=0.4549999999999999 ++el: name=N; f=0.024999999999999994 ++el: name=O; f=0.355 ++el: name=Na; f=0.0009999999999999998 ++el: name=P; f=0.021 ++el: name=S; f=0.0009999999999999998 ++el: name=Cl; f=0.0009999999999999998 ++el: name=K; f=0.0009999999999999998 ++el: name=Ca; f=0.045 + +Marrow_Bone02_28: d=1.15985 g/cm3 ; n=10 ++el: name=H; f=0.089 ++el: name=C; f=0.4229999999999999 ++el: name=N; f=0.026999999999999996 ++el: name=O; f=0.363 ++el: name=Na; f=0.0009999999999999998 ++el: name=P; f=0.029999999999999995 ++el: name=S; f=0.0009999999999999998 ++el: name=Cl; f=0.0009999999999999998 ++el: name=K; f=0.0009999999999999998 ++el: name=Ca; f=0.06399999999999999 + +Marrow_Bone02_29: d=1.18947 g/cm3 ; n=10 ++el: name=H; f=0.089 ++el: name=C; f=0.4229999999999999 ++el: name=N; f=0.026999999999999996 ++el: name=O; f=0.363 ++el: name=Na; f=0.0009999999999999998 ++el: name=P; f=0.029999999999999995 ++el: name=S; f=0.0009999999999999998 ++el: name=Cl; f=0.0009999999999999998 ++el: name=K; f=0.0009999999999999998 ++el: name=Ca; f=0.06399999999999999 + +Marrow_Bone03_30: d=1.21909 g/cm3 ; n=10 ++el: name=H; f=0.08199999999999998 ++el: name=C; f=0.39099999999999985 ++el: name=N; f=0.02899999999999999 ++el: name=O; f=0.372 ++el: name=Na; f=0.0009999999999999998 ++el: name=P; f=0.03899999999999999 ++el: name=S; f=0.0009999999999999998 ++el: name=Cl; f=0.0009999999999999998 ++el: name=K; f=0.0009999999999999998 ++el: name=Ca; f=0.08299999999999999 + +Marrow_Bone03_31: d=1.24871 g/cm3 ; n=10 ++el: name=H; f=0.08199999999999998 ++el: name=C; f=0.39099999999999985 ++el: name=N; f=0.02899999999999999 ++el: name=O; f=0.372 ++el: name=Na; f=0.0009999999999999998 ++el: name=P; f=0.03899999999999999 ++el: name=S; f=0.0009999999999999998 ++el: name=Cl; f=0.0009999999999999998 ++el: name=K; f=0.0009999999999999998 ++el: name=Ca; f=0.08299999999999999 + +Marrow_Bone04_32: d=1.27833 g/cm3 ; n=10 ++el: name=H; f=0.076 ++el: name=C; f=0.361 ++el: name=N; f=0.03 ++el: name=O; f=0.37999999999999995 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.001 ++el: name=P; f=0.04699999999999999 ++el: name=S; f=0.002 ++el: name=Cl; f=0.001 ++el: name=Ca; f=0.10099999999999998 + +Marrow_Bone04_33: d=1.30795 g/cm3 ; n=10 ++el: name=H; f=0.076 ++el: name=C; f=0.361 ++el: name=N; f=0.03 ++el: name=O; f=0.37999999999999995 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.001 ++el: name=P; f=0.04699999999999999 ++el: name=S; f=0.002 ++el: name=Cl; f=0.001 ++el: name=Ca; f=0.10099999999999998 + +Marrow_Bone05_34: d=1.33757 g/cm3 ; n=9 ++el: name=H; f=0.071 ++el: name=C; f=0.335 ++el: name=N; f=0.032 ++el: name=O; f=0.387 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.001 ++el: name=P; f=0.054000000000000006 ++el: name=S; f=0.002 ++el: name=Ca; f=0.11699999999999999 + +Marrow_Bone05_35: d=1.36719 g/cm3 ; n=9 ++el: name=H; f=0.071 ++el: name=C; f=0.335 ++el: name=N; f=0.032 ++el: name=O; f=0.387 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.001 ++el: name=P; f=0.054000000000000006 ++el: name=S; f=0.002 ++el: name=Ca; f=0.11699999999999999 + +Marrow_Bone06_36: d=1.39681 g/cm3 ; n=9 ++el: name=H; f=0.066 ++el: name=C; f=0.31000000000000005 ++el: name=N; f=0.033 ++el: name=O; f=0.394 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0010000000000000002 ++el: name=P; f=0.061000000000000006 ++el: name=S; f=0.0020000000000000005 ++el: name=Ca; f=0.132 + +Marrow_Bone06_37: d=1.42642 g/cm3 ; n=9 ++el: name=H; f=0.066 ++el: name=C; f=0.31000000000000005 ++el: name=N; f=0.033 ++el: name=O; f=0.394 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0010000000000000002 ++el: name=P; f=0.061000000000000006 ++el: name=S; f=0.0020000000000000005 ++el: name=Ca; f=0.132 + +Marrow_Bone07_38: d=1.45604 g/cm3 ; n=9 ++el: name=H; f=0.061000000000000006 ++el: name=C; f=0.28700000000000003 ++el: name=N; f=0.035 ++el: name=O; f=0.4000000000000001 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0010000000000000002 ++el: name=P; f=0.06700000000000002 ++el: name=S; f=0.0020000000000000005 ++el: name=Ca; f=0.14600000000000002 + +Marrow_Bone07_39: d=1.48566 g/cm3 ; n=9 ++el: name=H; f=0.061000000000000006 ++el: name=C; f=0.28700000000000003 ++el: name=N; f=0.035 ++el: name=O; f=0.4000000000000001 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0010000000000000002 ++el: name=P; f=0.06700000000000002 ++el: name=S; f=0.0020000000000000005 ++el: name=Ca; f=0.14600000000000002 + +Marrow_Bone08_40: d=1.51528 g/cm3 ; n=9 ++el: name=H; f=0.055999999999999994 ++el: name=C; f=0.265 ++el: name=N; f=0.036000000000000004 ++el: name=O; f=0.405 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.073 ++el: name=S; f=0.003 ++el: name=Ca; f=0.159 + +Marrow_Bone08_41: d=1.5449 g/cm3 ; n=9 ++el: name=H; f=0.055999999999999994 ++el: name=C; f=0.265 ++el: name=N; f=0.036000000000000004 ++el: name=O; f=0.405 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.073 ++el: name=S; f=0.003 ++el: name=Ca; f=0.159 + +Marrow_Bone09_42: d=1.57452 g/cm3 ; n=9 ++el: name=H; f=0.052 ++el: name=C; f=0.246 ++el: name=N; f=0.037 ++el: name=O; f=0.411 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.078 ++el: name=S; f=0.0029999999999999996 ++el: name=Ca; f=0.16999999999999998 + +Marrow_Bone09_43: d=1.60414 g/cm3 ; n=9 ++el: name=H; f=0.052 ++el: name=C; f=0.246 ++el: name=N; f=0.037 ++el: name=O; f=0.411 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.078 ++el: name=S; f=0.0029999999999999996 ++el: name=Ca; f=0.16999999999999998 + +Marrow_Bone10_44: d=1.63376 g/cm3 ; n=9 ++el: name=H; f=0.049 ++el: name=C; f=0.22699999999999998 ++el: name=N; f=0.038 ++el: name=O; f=0.41600000000000004 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.083 ++el: name=S; f=0.003 ++el: name=Ca; f=0.18100000000000002 + +Marrow_Bone10_45: d=1.66338 g/cm3 ; n=9 ++el: name=H; f=0.049 ++el: name=C; f=0.22699999999999998 ++el: name=N; f=0.038 ++el: name=O; f=0.41600000000000004 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.083 ++el: name=S; f=0.003 ++el: name=Ca; f=0.18100000000000002 + +Marrow_Bone11_46: d=1.693 g/cm3 ; n=9 ++el: name=H; f=0.045 ++el: name=C; f=0.21 ++el: name=N; f=0.039 ++el: name=O; f=0.42 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.08800000000000001 ++el: name=S; f=0.003 ++el: name=Ca; f=0.192 + +Marrow_Bone11_47: d=1.72262 g/cm3 ; n=9 ++el: name=H; f=0.045 ++el: name=C; f=0.21 ++el: name=N; f=0.039 ++el: name=O; f=0.42 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.08800000000000001 ++el: name=S; f=0.003 ++el: name=Ca; f=0.192 + +Marrow_Bone12_48: d=1.75224 g/cm3 ; n=9 ++el: name=H; f=0.042 ++el: name=C; f=0.19399999999999998 ++el: name=N; f=0.04 ++el: name=O; f=0.425 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.092 ++el: name=S; f=0.003 ++el: name=Ca; f=0.201 + +Marrow_Bone12_49: d=1.78186 g/cm3 ; n=9 ++el: name=H; f=0.042 ++el: name=C; f=0.19399999999999998 ++el: name=N; f=0.04 ++el: name=O; f=0.425 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.092 ++el: name=S; f=0.003 ++el: name=Ca; f=0.201 + +Marrow_Bone13_50: d=1.81148 g/cm3 ; n=9 ++el: name=H; f=0.03900000000000001 ++el: name=C; f=0.17900000000000002 ++el: name=N; f=0.041 ++el: name=O; f=0.42900000000000005 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0020000000000000005 ++el: name=P; f=0.09600000000000002 ++el: name=S; f=0.0030000000000000005 ++el: name=Ca; f=0.21000000000000002 + +Marrow_Bone13_51: d=1.8411 g/cm3 ; n=9 ++el: name=H; f=0.03900000000000001 ++el: name=C; f=0.17900000000000002 ++el: name=N; f=0.041 ++el: name=O; f=0.42900000000000005 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0020000000000000005 ++el: name=P; f=0.09600000000000002 ++el: name=S; f=0.0030000000000000005 ++el: name=Ca; f=0.21000000000000002 + +Marrow_Bone14_52: d=1.87072 g/cm3 ; n=9 ++el: name=H; f=0.036000000000000004 ++el: name=C; f=0.165 ++el: name=N; f=0.042 ++el: name=O; f=0.43200000000000005 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.1 ++el: name=S; f=0.003 ++el: name=Ca; f=0.21899999999999997 + +Marrow_Bone14_53: d=1.90034 g/cm3 ; n=9 ++el: name=H; f=0.036000000000000004 ++el: name=C; f=0.165 ++el: name=N; f=0.042 ++el: name=O; f=0.43200000000000005 ++el: name=Na; f=0.001 ++el: name=Mg; f=0.002 ++el: name=P; f=0.1 ++el: name=S; f=0.003 ++el: name=Ca; f=0.21899999999999997 + +Marrow_Bone15_54: d=1.92991 g/cm3 ; n=9 ++el: name=H; f=0.034 ++el: name=C; f=0.15500000000000003 ++el: name=N; f=0.04200000000000001 ++el: name=O; f=0.43500000000000005 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0020000000000000005 ++el: name=P; f=0.10300000000000002 ++el: name=S; f=0.0030000000000000005 ++el: name=Ca; f=0.22500000000000003 + +Marrow_Bone15_55: d=1.97143 g/cm3 ; n=9 ++el: name=H; f=0.034 ++el: name=C; f=0.15500000000000003 ++el: name=N; f=0.04200000000000001 ++el: name=O; f=0.43500000000000005 ++el: name=Na; f=0.0010000000000000002 ++el: name=Mg; f=0.0020000000000000005 ++el: name=P; f=0.10300000000000002 ++el: name=S; f=0.0030000000000000005 ++el: name=Ca; f=0.22500000000000003 + +AmalgamTooth_56: d=2.01308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_57: d=2.06308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_58: d=2.11308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_59: d=2.16308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_60: d=2.21308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_61: d=2.26308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_62: d=2.31308 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +AmalgamTooth_63: d=2.36009 g/cm3 ; n=4 ++el: name=Cu; f=0.04 ++el: name=Zn; f=0.02 ++el: name=Ag; f=0.65 ++el: name=Sn; f=0.29 + +MetallImplants_64: d=2.4071 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_65: d=2.4571 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_66: d=2.5071 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_67: d=2.5571 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_68: d=2.6071 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_69: d=2.6571 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_70: d=2.7071 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_71: d=2.7571 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_72: d=2.79105 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + +MetallImplants_73: d=2.8 g/cm3 ; n=1 ++el: name=Ti; f=1.0 + diff --git a/opengate/contrib/vpgTLE/stageX/energy_patient.py b/opengate/contrib/vpgTLE/stageX/energy_patient.py new file mode 100644 index 000000000..6acbfc95e --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/energy_patient.py @@ -0,0 +1,71 @@ +import itk +import numpy as np +import matplotlib.pyplot as plt +import uproot + +dossier = "patient" +part = "prot" +X = 62 +Y = 66 +Z = 94 +case = "try" +# Read input image +itk_image = itk.imread( + "output/" + dossier + "/optiVPG_tle_patient_0_" + part + "_e.nii.gz" +) +# Copy of itk.Image, pixel data is copied +array = itk.array_from_image(itk_image) + +# Read input image +itk_image = itk.imread("output/" + dossier + "/ana_patient_0_" + part + "_e.nii.gz") +# Copy of itk.Image, pixel data is copied +ana_array = itk.array_from_image(itk_image) +N = 1e7 + +histogram = np.zeros(array.shape[0]) +ana_histogram = np.zeros(ana_array.shape[0]) +ana_histogram = ana_array[:, Z, Y, X] +histogram = array[:, Z, Y, X] + +ana_histogram = ana_histogram[:-1] +inc = np.sqrt(ana_histogram / N) + +energy_axis = np.linspace(0, 10, len(ana_histogram)) +# Plot the histogram +plt.figure(figsize=(10, 6)) +plt.plot(energy_axis, histogram, label=f"vpgTLE MC", color="blue", linewidth=1) +plt.plot(energy_axis, ana_histogram, label=f"Analog MC", color="red", linewidth=1) +plt.fill_between( + energy_axis, + histogram + 3 * inc, + histogram - 3 * inc, + color="red", + alpha=0.1, + linewidth=1, +) + +# Add labels, title, and grid +plt.xlabel("PG energy (MeV)", fontsize=14) +plt.ylabel("PG yield / 40 keV / " + part + "ons", fontsize=14) + +# Enable minor ticks for a more precise grid +plt.minorticks_on() +plt.title(f"PG yield from " + part + "ons energy [" + str(4 * Y) + " mm]", fontsize=16) +# Customize the grid +plt.grid(which="major", linestyle="-", linewidth=0.8, alpha=0.8) # Major grid lines +plt.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) # Minor grid lines +plt.ticklabel_format(axis="y", style="sci", scilimits=(0, 0)) + + +# Add a legend +plt.legend(fontsize=12) +plt.savefig( + "output/" + dossier + "/Energy_" + part + "on_" + case + ".pdf", + dpi=300, + format="pdf", + bbox_inches="tight", +) + +# Show the plot + +plt.show() diff --git a/opengate/contrib/vpgTLE/stageX/lecture4Dcoupetheta_prot.py b/opengate/contrib/vpgTLE/stageX/lecture4Dcoupetheta_prot.py new file mode 100644 index 000000000..42dce726e --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/lecture4Dcoupetheta_prot.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import itk +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.ticker import ScalarFormatter +from matplotlib.colors import LogNorm + + +# Read input image +itk_image = itk.imread("output/box/map_analogu_0_prot_e.nii.gz") +spacing = itk.spacing(itk_image) +# Copy of itk.Image, pixel data is copied +array = itk.array_from_image(itk_image) + +# on stocke les dimensions de l'image +x_size = array.shape[3] +y_size = array.shape[2] +z_size = array.shape[1] + +# Create a 3D coordinate grid with proper axis handling +z, y, x = np.meshgrid( + np.linspace( + -z_size * spacing[1] / 20, z_size * spacing[1] / 20, z_size + ), # z-axis (first dimension in the array) + np.linspace( + 0, y_size * spacing[2] / 10, y_size + ), # y-axis (second dimension in the array) + np.linspace( + -x_size * spacing[3] / 20, x_size * spacing[3] / 20, x_size + ), # x-axis (third dimension in the array) + indexing="ij", # Use 'ij' indexing to match the array's shape +) + +x = x.ravel() +y = y.ravel() +z = z.ravel() + +# La couleur prise par les voxels dans la vue globale se fera par rapport à la moyenne du nombre d'occurence dans les histogrammes + +array_intensity = np.sum(array, axis=0) + +# Define the radius and height bins +Max = np.sqrt(x[x_size - 1] ** 2 + z[z_size - 1] ** 2) +R = np.linspace(0, Max, z_size) +H = np.linspace(0, y_size * spacing[2] / 10, y_size) +theta = np.linspace(0, np.pi / 2, 100) # Angle for cylindrical coordinates +# Initialize the graphs +graph = np.zeros((len(R), len(H)), dtype=float) + +# Calculate the bin width +width = spacing[1] / 10 +center = int(x_size / 2) # Center index for the x-axis +dh = spacing[2] / 10 +dr = width +for h in range(len(H)): + Y = h + for r in range(len(R)): + for t in range(len(theta)): + X = int(R[r] * np.cos(theta[t]) / width) + Z = int(R[r] * np.sin(theta[t]) / width) + # si hors du cylindre d'intérêt (dans les coins du carré de 14 cm de côté) + if X >= 100 or Z >= 100: + continue + D = R[r] + dr / 2 + quotient = 2 * np.pi * D * dr * dh + # premier quart + graph[r, h] += array_intensity[center + Z, Y, center + X] / quotient + # deuxième quart + graph[r, h] += array_intensity[center + Z, Y, center - X] / quotient + # troisième quart + graph[r, h] += array_intensity[center - Z, Y, center - X] / quotient + # quatrième quart + graph[r, h] += array_intensity[center - Z, Y, center + X] / quotient + + +s, h = np.meshgrid( + np.linspace(0, 15, x_size), # r-axis (first dimension in the array) + H, # h-axis (second dimension in the array) + indexing="ij", # Use 'ij' indexing to match the array's shape +) + +s = s.ravel() +h = h.ravel() +values = graph.ravel() +mask = values != 0 # Create a boolean mask for non-zero values +s = s[mask] +h = h[mask] +vmax = 1e-2 # Find the maximum value for normalization +values = values[mask] + +fig, ax = plt.subplots(figsize=(8, 6)) + +# Create a scatter plot for the proton data +p_scatter = ax.scatter( + s, + h, # r and h coordinates + c=values, # Color represents the intensity + marker="o", # Marker style + cmap="Reds", + s=8, + norm=LogNorm(vmin=1e-7, vmax=vmax), +) # Normalize color scale + +formatter = ScalarFormatter(useMathText=True) # Create a ScalarFormatter +formatter.set_scientific(True) +formatter.set_powerlimits((-3, 3)) # Set the range for scientific notation + +# Add a colorbar +cbar_p = plt.colorbar(p_scatter, ax=ax, shrink=1, pad=0.1) +cbar_p.set_label("cts/protons/mm²") # Label for the colorbar +# Explicitly set ticks for the colorbar +ticks_p = [1e-7, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2] +cbar_p.set_ticks(ticks_p) +cbar_p.set_ticklabels( + [f"{tick:.0e}" for tick in ticks_p] +) # Format ticks as scientific notation + +# labels and limits +ax.set_xlabel("radius (cm)") +ax.set_ylabel("depth (cm)") +ax.set_xlim(0, 15) # R-axis: cm +ax.set_ylim(0, 20) # H-axis: cm +ax.set_title("2D map of PGs emission by protons", fontsize=16) +plt.grid(True) +plt.savefig("coupe-protons-analog.pdf", bbox_inches="tight") + +plt.show() diff --git a/opengate/contrib/vpgTLE/stageX/profile12.py b/opengate/contrib/vpgTLE/stageX/profile12.py new file mode 100644 index 000000000..a3f0d8016 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/profile12.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import itk +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.ticker import ScalarFormatter + +# Read input image +itk_image = itk.imread("output/box/map_analogu_0_prot_e.nii.gz") +spacing = itk.spacing(itk_image) +# Copy of itk.Image, pixel data is copied +array = itk.array_from_image(itk_image) + +# Read input image +Nitk_image = itk.imread("output/box/map_analogu_0_neutr_e.nii.gz") +Nspacing = itk.spacing(Nitk_image) +# Copy of itk.Image, pixel data is copied +Narray = itk.array_from_image(Nitk_image) + +# on stocke les dimensions de l'image +x_size = array.shape[3] +y_size = array.shape[2] +z_size = array.shape[1] +H = np.linspace(0, y_size * spacing[2] / 10, y_size) + +# Create a 3D coordinate grid with proper axis handling +z, y, x = np.meshgrid( + np.linspace( + -z_size * spacing[1] / 20, z_size * spacing[1] / 20, z_size + ), # z-axis (first dimension in the array) + np.linspace( + 0, y_size * spacing[2] / 10, y_size + ), # y-axis (second dimension in the array) + np.linspace( + -x_size * spacing[3] / 20, x_size * spacing[3] / 20, x_size + ), # x-axis (third dimension in the array) + indexing="ij", # Use 'ij' indexing to match the array's shape +) +x = x.ravel() +y = y.ravel() +z = z.ravel() + +# La couleur prise par les voxels dans la vue globale se fera par rapport à la moyenne du nombre d'occurence dans les histogrammes +Narray_intensity = np.sum(Narray, axis=0) +array_intensity = np.sum(array, axis=0) + +N = 1e7 +Ninc = np.sqrt(Narray_intensity / N) +inc = np.sqrt(array_intensity / N) +# Define the radius and height bins +Max = np.sqrt(x[x_size - 1] ** 2 + z[z_size - 1] ** 2) +R = np.linspace(0, Max, z_size) +H = np.linspace(0, y_size * spacing[2] / 10, y_size) +theta = np.linspace(0, np.pi / 2, 100) # Angle for cylindrical coordinates +# Initialize the graphs +graph = np.zeros((len(R)), dtype=float) +Ngraph = np.zeros((len(R)), dtype=float) + +sig = np.zeros(graph.shape) +Nsig = np.zeros(Ngraph.shape) + +tot = np.zeros((len(R)), dtype=float) +# Calculate the bin width +width = spacing[1] / 10 +center = int(x_size / 2) # Center index for the x-axis +dh = spacing[2] / 10 +dr = width + +for r in range(len(R)): + for t in range(len(theta)): + for h in range(y_size): + X = int(R[r] * np.cos(theta[t]) / width) + Z = int(R[r] * np.sin(theta[t]) / width) + # si hors du cylindre d'intérêt (dans les coins du carré de 14 cm de côté) + if X >= 100 or Z >= 100: + continue + D = R[r] + dr / 2 + quotient = 2 * np.pi * D * dr * dh + # premier quart + graph[r] += array_intensity[center + Z, h, center + X] / quotient + Ngraph[r] += Narray_intensity[center + Z, h, center + X] / quotient + sig[r] += (inc[center + Z, h, center + X] / quotient) ** 2 + Nsig[r] += (Ninc[center + Z, h, center + X] / quotient) ** 2 + # deuxième quart + graph[r] += array_intensity[center + Z, h, center - X] / quotient + Ngraph[r] += Narray_intensity[center + Z, h, center - X] / quotient + sig[r] += (inc[center + Z, h, center - X] / quotient) ** 2 + Nsig[r] += (Ninc[center + Z, h, center - X] / quotient) ** 2 + # troisième quart + graph[r] += array_intensity[center - Z, h, center - X] / quotient + Ngraph[r] += Narray_intensity[center - Z, h, center - X] / quotient + sig[r] += (inc[center - Z, h, center - X] / quotient) ** 2 + Nsig[r] += (Ninc[center - Z, h, center - X] / quotient) ** 2 + # quatrième quart + graph[r] += array_intensity[center - Z, h, center + X] / quotient + Ngraph[r] += Narray_intensity[center - Z, h, center + X] / quotient + sig[r] += (inc[center - Z, h, center + X] / quotient) ** 2 + Nsig[r] += (Ninc[center - Z, h, center + X] / quotient) ** 2 + tot[r] = graph[r] + Ngraph[r] +R = R.ravel() +sig = np.sqrt(sig) +Nsig = np.sqrt(Nsig) +Ngraph = Ngraph.ravel() +graph = graph.ravel() +sig = sig.ravel() +Nsig = Nsig.ravel() +tot = tot.ravel() +print("min inc prot", np.min(sig)) +print("max inc prot", np.max(sig)) +print("min inc neut", np.min(Nsig)) +print("max Ninc neutr", np.max(Nsig)) +# Create a figure and axis +fig = plt.figure(figsize=(10, 6)) +# Plot graph and Ngraph against R +plt.plot(R, graph, label="Protons", color="red", linewidth=1) +plt.fill_between(R, graph - 3 * sig, graph + 3 * sig, color="red", alpha=0.08) +plt.plot(R, Ngraph, label="Neutrons", color="blue", linewidth=1) +plt.fill_between(R, Ngraph - 3 * Nsig, Ngraph + 3 * Nsig, color="blue", alpha=0.08) +plt.plot(R, tot, label="total", color="black", linewidth=1, linestyle="--") +# Add labels, legend, and title +plt.xlabel("Radius (cm)", fontsize=14) +plt.ylabel("cts / protons / (cm3)", fontsize=14) +plt.title("PGs emission against the radius", fontsize=20) +plt.legend(fontsize=12) + +ax = plt.gca() +# Set the y-axis to scientific notation +formatter = ScalarFormatter(useMathText=True) +formatter.set_scientific(True) +formatter.set_powerlimits((-2, 3)) # Set limits for scientific notation +ax.yaxis.set_major_formatter(formatter) + +# Enable minor ticks for a more precise grid +plt.minorticks_on() + +# Customize the grid +plt.grid(which="major", linestyle="-", linewidth=0.8, alpha=0.8) # Major grid lines +plt.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) # Minor grid lines +# Add more ticks to the y-axis +# plt.gca().yaxis.set_major_locator(MultipleLocator(1)) # Major ticks every 1 unit +# plt.gca().yaxis.set_minor_locator(MultipleLocator(0.2)) # Minor ticks every 0.2 units + +plt.yscale("log") +plt.xlim(0, 14) +# Show the plot +plt.savefig("analogprofileradial.pdf", format="pdf", bbox_inches="tight", dpi=300) +plt.show() diff --git a/opengate/contrib/vpgTLE/stageX/profilelongitude.py b/opengate/contrib/vpgTLE/stageX/profilelongitude.py new file mode 100644 index 000000000..8f9cc6425 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/profilelongitude.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import itk +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.ticker import ScalarFormatter + + +# Read input image +itk_image = itk.imread("output/box/map_analogu_0_prot_e.nii.gz") +spacing = itk.spacing(itk_image) +# Copy of itk.Image, pixel data is copied +array = itk.array_from_image(itk_image) + +# Read input image +Nitk_image = itk.imread("output/box/map_analogu_0_neutr_e.nii.gz") +Nspacing = itk.spacing(Nitk_image) +# Copy of itk.Image, pixel data is copied +Narray = itk.array_from_image(Nitk_image) + +# on stocke les dimensions de l'image +x_size = array.shape[3] +y_size = array.shape[2] +z_size = array.shape[1] +H = np.linspace(0, y_size * spacing[2] / 10, y_size) +# Create a 3D coordinate grid with proper axis handling +z, y, x = np.meshgrid( + np.linspace( + -z_size * spacing[1] / 20, z_size * spacing[1] / 20, z_size + ), # z-axis (first dimension in the array) + np.linspace( + 0, y_size * spacing[2] / 10, y_size + ), # y-axis (second dimension in the array) + np.linspace( + -x_size * spacing[3] / 20, x_size * spacing[3] / 20, x_size + ), # x-axis (third dimension in the array) + indexing="ij", # Use 'ij' indexing to match the array's shape +) +x = x.ravel() +y = y.ravel() +z = z.ravel() + +# La couleur prise par les voxels dans la vue globale se fera par rapport à la moyenne du nombre d'occurence dans les histogrammes +Narray_intensity = np.sum(Narray, axis=0) +array_intensity = np.sum(array, axis=0) + +N = 1e7 +Ninc = np.sqrt(Narray_intensity / N) +inc = np.sqrt(array_intensity / N) + +H = np.linspace(0, y_size * spacing[2] / 10, y_size) +# Initialize the graphs +graph = np.zeros((len(H)), dtype=float) +Ngraph = np.zeros((len(H)), dtype=float) +tot = np.zeros((len(H)), dtype=float) + +sig = np.zeros(graph.shape) +Nsig = np.zeros(Ngraph.shape) + +for h in range(len(H)): + N = 0 + T = 0 + Ni = 0 + Ti = 0 + for i in range(x_size): + for j in range(z_size): + N = N + Narray_intensity[j, h, i] + T = T + array_intensity[j, h, i] + Ni = Ni + Ninc[j, h, i] ** 2 + Ti = Ti + inc[j, h, i] ** 2 + graph[h] = T + Ngraph[h] = N + sig[h] = Ti + Nsig[h] = Ni + tot[h] = T + N + +graph = graph[1:] +Ngraph = Ngraph[1:] +tot = tot[1:] +sig = sig[1:] +Nsig = Nsig[1:] + +sig = np.sqrt(sig) +Nsig = np.sqrt(Nsig) + +H = H[1:] +H = H.ravel() +Ngraph = Ngraph.ravel() +graph = graph.ravel() + +sig = sig.ravel() +Nsig = Nsig.ravel() + +tot = tot.ravel() +print("min inc prot", np.min(sig)) +print("max inc prot", np.max(sig)) +print("min inc neut", np.min(Nsig)) +print("max Ninc neutr", np.max(Nsig)) +# Create a figure and axis +fig = plt.figure(figsize=(10, 6)) +# Plot graph and Ngraph against R +plt.plot(H, graph, label="Protons", color="red", linewidth=1) +plt.fill_between(H, graph - 3 * sig, graph + 3 * sig, color="red", alpha=0.08) +plt.plot(H, Ngraph, label="Neutrons", color="blue", linewidth=1) +plt.fill_between(H, Ngraph - 3 * Nsig, Ngraph + 3 * Nsig, color="blue", alpha=0.08) +plt.plot(H, tot, label="total", color="black", linewidth=1, linestyle="--") +# Add labels, legend, and title +plt.xlabel("depth (cm)", fontsize=14) +plt.ylabel("cts/ protons / (cm3)", fontsize=14) +plt.title("PGs emission against the depth", fontsize=20) +plt.legend(fontsize=12) + +ax = plt.gca() +# Set the y-axis to scientific notation +formatter = ScalarFormatter(useMathText=True) +formatter.set_scientific(True) +formatter.set_powerlimits((-2, 3)) # Set limits for scientific notation +ax.yaxis.set_major_formatter(formatter) + +plt.minorticks_on() + +# Customize the grid +plt.grid(which="major", linestyle="-", linewidth=0.8, alpha=0.8) # Major grid lines +plt.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) # Minor grid lines +# Add more ticks to the y-axis +# plt.gca().yaxis.set_major_locator(MultipleLocator(1)) # Major ticks every 1 unit +# plt.gca().yaxis.set_minor_locator(MultipleLocator(0.2)) # Minor ticks every 0.2 units + +# plt.yscale('log') +plt.xlim(-1, 20) +# Show the plot +plt.savefig("analog-profilelongitude.pdf", format="pdf", bbox_inches="tight", dpi=300) +plt.show() diff --git a/opengate/contrib/vpgTLE/stageX/readme.md b/opengate/contrib/vpgTLE/stageX/readme.md new file mode 100644 index 000000000..a62d1a703 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/readme.md @@ -0,0 +1,8 @@ + +TESTS + +Run all tests with ```opengate_tests``` + +Every tests can be run with, e.g., ```./src/test008_dose_actor.py``` + +(documentation in progress : https://opengate-python.readthedocs.io) diff --git a/opengate/contrib/vpgTLE/stageX/remerge_data.py b/opengate/contrib/vpgTLE/stageX/remerge_data.py new file mode 100644 index 000000000..29f2ea03c --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/remerge_data.py @@ -0,0 +1,38 @@ +import uproot + +case = "neutron" + +# Open the source ROOT file (data_merge_neutron.root) and extract the histogram +input_A = uproot.open("data/data_" + case + "_A.root") +input_B = uproot.open("data/data_" + case + "_B.root") + +# Create a new ROOT file with the updated histogram +with uproot.recreate("data/data_merge_" + case + ".root") as new_file: + for key, obj in input_A.items(): + # Remove the first ";1" from the key name + clean_key = key.split(";")[0] # Remove the cycle suffix + if isinstance(obj, uproot.models.TH.Model_TH2D_v4): + # Copy other TH2D histograms as they are + bin_contents = obj.values() + edges_x = obj.axis(0).edges() + edges_y = obj.axis(1).edges() + new_file[key] = (bin_contents, (edges_x, edges_y)) + elif isinstance(obj, uproot.models.TH.Model_TH1D_v3): + # Handle TH1D histograms + bin_contents = obj.values() + edges_x = obj.axis(0).edges() + new_file[key] = (bin_contents, edges_x) + for key, obj in input_B.items(): + # Remove the first ";1" from the key name + clean_key = key.split(";")[0] # Remove the cycle suffix + if isinstance(obj, uproot.models.TH.Model_TH2D_v4): + # Copy other TH2D histograms as they are + bin_contents = obj.values() + edges_x = obj.axis(0).edges() + edges_y = obj.axis(1).edges() + new_file[key] = (bin_contents, (edges_x, edges_y)) + elif isinstance(obj, uproot.models.TH.Model_TH1D_v3): + # Handle TH1D histograms + bin_contents = obj.values() + edges_x = obj.axis(0).edges() + new_file[key] = (bin_contents, edges_x) diff --git a/opengate/contrib/vpgTLE/stageX/split_data.py b/opengate/contrib/vpgTLE/stageX/split_data.py new file mode 100644 index 000000000..edc68311d --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/split_data.py @@ -0,0 +1,74 @@ +import uproot +import numpy as np + +case = "proton" +# Open the source ROOT file (data_merge_neutron.root) and extract the histogram +source_file = uproot.open("data/data_merge_" + case + ".root") + +elements_A = [ + "standard_Weight", + "Al", + "Ar", + "Be", + "B", + "Ca", + "C", + "Cl", + "Cu", + "F", + "He", + "H", + "Li", +] +elements_B = ["Mg", "Ne", "N", "O", "P", "K", "Si", "Ag", "Na", "S", "Sn", "Ti", "Zn"] + +if case == "neutron": + quantity = ["GammaZ", "Kapa inelastique", "NrPG", "EnEpg"] +else: + quantity = ["GammaZ", "Kapa inelastique", "NrPG", "EpEpg"] + +# Fichiers de sortie +output_A = uproot.recreate("data/data_" + case + "_A.root") +output_B = uproot.recreate("data/data_" + case + "_B.root") + +for el in elements_A: + if el == "standard_Weight": + q = "Weight" + key = f"{el}/{q}" + obj = source_file[key] + bin_contents = obj.values() + edges_x = obj.axis(0).edges() + output_A[key] = (bin_contents, edges_x) + continue + for q in quantity: + # Check if the object is a histogram (TH1D or TH2D) + key = f"{el}/{q}" + obj = source_file[key] + if isinstance(obj, uproot.models.TH.Model_TH2D_v4): + # Copy other TH2D histograms as they are + bin_contents = obj.values() + edges_x = obj.axis(0).edges() + edges_y = obj.axis(1).edges() + output_A[key] = (bin_contents, (edges_x, edges_y)) + elif isinstance(obj, uproot.models.TH.Model_TH1D_v3): + # Handle TH1D histograms + bin_contents = obj.values() + edges_x = obj.axis(0).edges() + output_A[key] = (bin_contents, edges_x) + +for el in elements_B: + for q in quantity: + # Check if the object is a histogram (TH1D or TH2D) + key = f"{el}/{q}" + obj = source_file[key] + if isinstance(obj, uproot.models.TH.Model_TH2D_v4): + # Copy other TH2D histograms as they are + bin_contents = obj.values() + edges_x = obj.axis(0).edges() + edges_y = obj.axis(1).edges() + output_B[key] = (bin_contents, (edges_x, edges_y)) + elif isinstance(obj, uproot.models.TH.Model_TH1D_v3): + # Handle TH1D histograms + bin_contents = obj.values() + edges_x = obj.axis(0).edges() + output_B[key] = (bin_contents, edges_x) diff --git a/opengate/contrib/vpgTLE/stageX/stage1/multijobs_stage1.py b/opengate/contrib/vpgTLE/stageX/stage1/multijobs_stage1.py new file mode 100644 index 000000000..df7b39d12 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/stage1/multijobs_stage1.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python +import argparse +import os +import itk +from multiprocessing import Pool + +import opengate as gate +import glob +import numpy as np +from stage_1a import * + +import sys + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +DEFAULT_OUTPUT = "debog" # file name where single root file are stocked +DEFAULT_NUMBER_OF_JOBS = 1 +DEFAULT_NUMBER_OF_PARTICLES = 1e6 +DEFAULT_POSITION = [0, 0, 0] +DEFAULT_STEP_SIZE = float(1) +DEFAULT_WORLD = "G4_Galactic" # default material of the world volume +DEFAULT_SAMPLE = "Water3ppmPt" # default material of the sample volume +# DEFAULT_FILE_NAME = "ana-vox" #default name of the output file. No "_" in the name !!!! +DEFAULT_FILE_NAME = "vox" +DEFAULT_BEAM_ENERGY = 130 +# DEFAULT_ACTOR = "VoxelizedPromptGammaAnalogActor" +DEFAULT_ACTOR = "VoxelizedPromptGammaTLEActor" +# DEFAULT_RANGE = 10 +DEFAULT_RANGE = 130 + +path = utility.get_default_test_paths(__file__, output_folder=DEFAULT_OUTPUT) + + +def opengate_run( + output=DEFAULT_OUTPUT, + job_id=0, + number_of_particles=DEFAULT_NUMBER_OF_PARTICLES, + visu=False, + verbose=False, + polarization=False, + energy_type=False, + world=DEFAULT_WORLD, + sample_material=DEFAULT_SAMPLE, + File_name=DEFAULT_FILE_NAME, + collimation=False, + energy=DEFAULT_BEAM_ENERGY, + position=DEFAULT_POSITION, + actor=DEFAULT_ACTOR, + Erange=DEFAULT_RANGE, +): + + if verbose: + print( + f"Running MC… " + str(locals()) + ) # locals = variable locales soit les paramètres de la fonction + + # Call the simulation function and retrieve sim and simulation_parameters + sim = simulation( + output, File_name, job_id, number_of_particles, visu, verbose, actor, Erange + ) + + sim.run() + + +def itk_merge( + output=DEFAULT_OUTPUT, + verbose=False, + File_name=DEFAULT_FILE_NAME, + number_of_jobs=DEFAULT_NUMBER_OF_JOBS, +): + def print_verbose(*args, **kwargs): + if verbose: + print(*args, **kwargs) + + if not os.path.exists(output): + os.makedirs(output) + if number_of_jobs == 1: + print_verbose("Skipping merge, only one job was run.") + return + try: + # Find all .nii.gz files matching the pattern + file_pattern = os.path.join( + f"/home/vguittet/Documents/G10/stage1.5/output/{output}/{File_name}_trace_*.nii.gz" + ) + file_list = sorted(glob.glob(file_pattern)) # Get all matching files + count = number_of_jobs + if not file_list: + raise FileNotFoundError( + f"No .nii.gz files found matching pattern: {file_pattern}" + ) + actor_list = [] + type_list = [] + for file_path in file_list: + base_name = os.path.basename(file_path) + actor_name = base_name.split("_")[1] + type_name = ( + base_name.split("_")[3] + "_" + base_name.split("_")[4] + ) # e.g., neutr_e + if actor_name not in actor_list: + actor_list.append(actor_name) + if type_name not in type_list: + type_list.append(type_name) + for type_name in type_list: + for actor_name in actor_list: + init = itk.imread( + path.output / f"{File_name}_{actor_name}_1_{type_name}" + ) + init_arr = itk.array_from_image(init) + merged_array = np.zeros_like(init_arr, dtype=np.float32) + merged_array.fill(0) # Initialize the merged array + for i in range(count): + file_path = ( + path.output / f"{File_name}_{actor_name}_{i + 1}_{type_name}" + ) + print(file_path) + # Check if the file exists + if not os.path.exists(file_path): + print(f"File not found: {file_path}") + continue + image = itk.imread(file_path) + array = itk.array_from_image(image) + merged_array = merged_array + array # Accumulate the pixel values + merged_array = merged_array / count + merged_image = itk.image_from_array(merged_array) + merged_array = np.array([]) + merged_image.CopyInformation( + init + ) # Copy metadata from the initial image + output_file = ( + path.output / f"{File_name}_{actor_name}_merged_{type_name}" + ) + print_verbose(f"Saving merged image to: {output_file}") + itk.imwrite(merged_image, output_file) + + except FileNotFoundError as e: + print(e) + + +def opengate_pool_run( + output, + number_of_jobs, # mettre autant que de positions sinon on ne simule pas toutes les particules + number_of_particles, + visu, + verbose, + polarization, + energy_type, + world, + sample_material, + File_name, + collimation, + energy, + # step, + # size, +): + + with Pool(maxtasksperchild=1) as pool: + + results = [] + + number_of_particles_per_job = int(number_of_particles / number_of_jobs) + mm = gate.g4_units.mm + um = gate.g4_units.um + cm = gate.g4_units.cm + position = [ + 0, + 0, + 20 * cm, + ] # position initiale à 30um -> trop proche de la source + # delta_x = 0.1*mm #pas du raster scan pour passer d'un pixel à l'autre + # delta_x = step*mm + # delta_y = -step*mm + job_id = 0 + + for i in range(number_of_jobs): + + job_id = i + 1 + # copied_position = position.copy() + print(f"launching job #{job_id}/{number_of_jobs}") + # print(f"launching job #{job_id}/{number_of_jobs} with position x={copied_position[0]/mm:.2f} mm") + result = pool.apply_async( + opengate_run, + kwds={ + "output": output, + "job_id": job_id, + "number_of_particles": number_of_particles_per_job, + "visu": visu, + "verbose": verbose, + "position": position, + "polarization": polarization, + "energy_type": energy_type, + "world": world, + "sample_material": sample_material, + "File_name": File_name, + "collimation": collimation, + "energy": energy, + #'position' : copied_position, + }, + ) + results.append(result) + # position[0] += delta_x + + pool.close() + pool.join() + + for result in results: + result.wait() + if not result.successful(): + print("Failure in MC simulation") + exit(1) + + itk_merge( + output=output, + verbose=verbose, + File_name=File_name, + number_of_jobs=number_of_jobs, + ) + + +def main(): + + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("--output", help="Path of outputs", default=DEFAULT_OUTPUT) + parser.add_argument( + "-j", + "--number-of-jobs", + help="Number of jobs", + default=DEFAULT_NUMBER_OF_JOBS, + type=int, + ) + parser.add_argument( + "-n", + "--number-of-particles", + help="Number of generated particles (total)", + default=DEFAULT_NUMBER_OF_PARTICLES, + type=float, + ) + parser.add_argument( + "--visu", + help="Visualize Monte Carlo simulation", + default=False, + action="store_true", + ) + parser.add_argument( + "--verbose", "-v", help="Verbose execution", default=False, action="store_true" + ) + parser.add_argument( + "-p", + "--polarization", + help="Polarization of the beam", + default=False, + action="store_true", + ) + parser.add_argument( + "--energy_type", + help="Type of energy distribution", + default=False, + action="store_true", + ) # True active le mode polychromatique, rendre la commande plus clair après + parser.add_argument( + "-w", "--world", help="World's material", default=DEFAULT_WORLD, type=str + ) # pas besoin de mettre les guillemets au moment de la commande + parser.add_argument( + "-s", + "--sample_material", + help="sample's material", + default=DEFAULT_SAMPLE, + type=str, + ) + parser.add_argument( + "-f", + "--File_name", + help="name of the output file", + default=DEFAULT_FILE_NAME, + type=str, + ) + parser.add_argument( + "-c", + "--collimation", + help="collimation of the beam", + default=False, + action="store_true", + ) + parser.add_argument( + "-e", + "--energy", + help="Energy of the beam in keV", + default=DEFAULT_BEAM_ENERGY, + type=int, + ) + # parser.add_argument('-step', '--step', help="size of a step between two succesive positions en mm", default=DEFAULT_STEP_SIZE, type=float) + # parser.add_argument('-size', '--size', help="number of pixels in lines/columns", default=DEFAULT_STEP_SIZE, type=int) + # parser.add_argument('-pos', '--number-of-positions', help="Number of positions to simulate", default=DEFAULT_NUMBER_OF_POSITIONS, type=int) + # argument position initiale ? + # argument pour le nombre de position à couvrir (taille de l'image à balayer en gros) + args_info = parser.parse_args() + + opengate_pool_run(**vars(args_info)) + # opengate_run(output, 1, number_of_particles, visu, verbose) + + +if __name__ == "__main__": + main() diff --git a/opengate/contrib/vpgTLE/stageX/stage1/stage_1a.py b/opengate/contrib/vpgTLE/stageX/stage1/stage_1a.py new file mode 100644 index 000000000..bc4a0964d --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/stage1/stage_1a.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import opengate as gate +from opengate.tests import utility +import uproot + + +def simulation( + output, File_name, job_id, number_of_particles, visu, verbose, actor, Erange +): + + paths = utility.get_default_test_paths(__file__, output_folder=output) + + # create the simulation + sim = gate.Simulation() + # main options + sim.visu = visu + sim.g4_verbose = verbose + sim.random_seed = "auto" # FIXME to be replaced by a fixed number at the end + sim.random_engine = "MersenneTwister" + sim.output_dir = paths.output + sim.number_of_threads = 1 + sim.progress_bar = True + + # units + mm = gate.g4_units.mm + cm = gate.g4_units.cm + m = gate.g4_units.m + MeV = gate.g4_units.MeV + ns = gate.g4_units.ns + gcm3 = gate.g4_units.g_cm3 + + # change world size + world = sim.world + world.size = [3 * m, 3 * m, 3 * m] + world.material = "G4_Galactic" + + # patient + """ + ct = sim.add_volume("Image", "ct") + ct.image = paths.data / f"1mm-carbo-volume.mhd" + if visu : + ct.image = paths.data / f"visu-carbo-volume.mhd" + ct.mother = "world" + ct.material = "G4_C" + ct.voxel_materials = [ + # range format [) + [-2000, -700, "G4_C"], + ] + ct.dump_label_image = "labels.mhd" + """ + + ct = sim.add_volume("Image", vol_name) + ct.image = paths.data / f"ct_4mm.mhd" + if sim.visu: + ct.image = paths.data / f"ct_40mm.mhd" + ct.material = "G4_AIR" + f1 = str(paths.data / "Schneider2000MaterialsTable.txt") + f2 = str(paths.data / "Schneider2000DensitiesTable.txt") + tol = 0.05 * gcm3 + ( + ct.voxel_materials, + materials, + ) = gate.geometry.materials.HounsfieldUnit_to_material(sim, tol, f1, f2) + ct.dump_label_image = paths.output / "labels.mhd" + ct.mother = "world" + ct.load_input_image() + + # physics + sim.physics_manager.physics_list_name = "QGSP_BIC_HP_EMY" + + sim.physics_manager.apply_cuts = False # default + sim.physics_manager.global_production_cuts.gamma = 0.1 * mm + sim.physics_manager.global_production_cuts.electron = 0.1 * mm + sim.physics_manager.global_production_cuts.positron = 0.1 * mm + sim.physics_manager.global_production_cuts.proton = 0.1 * mm + + sim.physics_manager.set_max_step_size("ct", 0.1 * mm) + sim.physics_manager.set_user_limits_particles(["proton"]) + + # source of proton + # FIXME to replace by a more realistic proton beam, see tests 044 + source = sim.add_source("GenericSource", "DEFAULT") + source.energy.mono = Erange * MeV + source.particle = "proton" + source.position.type = "point" + source.position.radius = 1 * mm + source.position.translation = [0 * mm, -300 * mm, 0 * mm] + source.n = number_of_particles + source.direction.type = "momentum" + source.direction.momentum = [0, 1, 0] + + # LOOKHERE :: if database not well implanted, has to be modified + with uproot.open(paths.data / "data_merge_proton.root") as root_file: + histo = root_file["standard_Weight"]["Weight"].to_hist() + vect_p = histo.to_numpy()[0] + with uproot.open(paths.data / "data_merge_neutron.root") as root_file: + histo = root_file["standard_Weight"]["Weight"].to_hist() + vect_n = histo.to_numpy()[0] + + vpg_tle = sim.add_actor(actor, actor_name) + vpg_tle.attached_to = vol_name + vpg_tle.output_filename = paths.output / f"{File_name}.nii.gz" + vpg_tle.size = [125, 125, 189] # the same size than ct image is stronly adviced + vpg_tle.spacing = [ + 4, + 4, + 4, + ] # the same spacing is stronly adviced + vpg_tle.timebins = 250 + vpg_tle.timerange = 5 * ns + vpg_tle.energybins = 250 + vpg_tle.energyrange = Erange * MeV + vpg_tle.prot_E.active = True + vpg_tle.neutr_E.active = True + vpg_tle.prot_tof.active = True + vpg_tle.neutr_tof.active = True + vpg_tle.weight = True # True to obtain weighted time spectra + vpg_tle.vect_p = vect_p + vpg_tle.vect_n = vect_n + + # add stat actor + stats = sim.add_actor("SimulationStatisticsActor", "stats") + stats.track_types_flag = True + stats.output_filename = paths.output / f"stat_{job_id}_{File_name}.txt" + + return sim + + +# features of simulation that can be modify +output = "stage1a" +File_name = "vpg" +actor_name = "vpg_tle" +vol_name = "ct" +MJ = False +number_of_particles = 1e2 +# if analog is used, should be replace (Erange = 10 MeV and differenciate from the source energy) +actor = "VoxelizedPromptGammaTLEActor" +# source Energy andrange of the actor +Erange = 130 +if __name__ == "__main__": + sim = simulation( + output, File_name, 0, number_of_particles, False, False, actor, Erange + ) + # start simulation + sim.run() diff --git a/opengate/contrib/vpgTLE/stageX/stage1/stage_1b.py b/opengate/contrib/vpgTLE/stageX/stage1/stage_1b.py new file mode 100644 index 000000000..1836538a9 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/stage1/stage_1b.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import itk +import uproot +import hist +import opengate as gate + +# import xraylib +# import periodictable +from opengate.tests import utility +import os +import glob +import numpy as np + +# import matplotlib.pyplot as plt +# import matplotlib.ticker as mticker +from variables import tle_sim +from stage_1a import * + +# from multijobs_stage1 import output, File_name, number_of_particles, actor, Erange + +sim = simulation( + output=output, + File_name=File_name, + job_id=0, + number_of_particles=number_of_particles, + visu=False, + verbose=False, + actor=actor, + Erange=Erange, +) + + +tle_simu = tle_sim(sim) + +# FIRST STEP : retrieve the arrays and all the needed features +# CT volume : + +ct_object = tle_simu.ct +UH_array = itk.array_from_image(ct_object.load_input_image()) + +# ACTORS volume as a list of object and the output place: +path = tle_simu.path +actor = tle_simu.act +actor_list = tle_simu.actor_list + +# database + +data_protons = tle_simu.root_file +data_neutrons = tle_simu.root_file_neutron +mat = tle_simu.voxel_mat + +# SECOND STEP : compute the material Gamma for each materials present in the ct and store it in a list + +neutr = False # to skip step if neutron are not taling into account +if "neutr_e.nii.gz" in actor_list: + neutr = True + +gamma_neutron = {} +gamma_proton = {} + +# building a dico that links for each mterial name, the Gamma histograms +for data in mat: + name = data[2] + if neutr: + gamma_neutron[name] = tle_simu.gamma_mat(name, data_neutrons) + gamma_proton[name] = tle_simu.gamma_mat(name, data_protons) + +# THIRD STEP : convert the energy histogram stored the 4D output into emission spectra for each voxel of the ct volume +Ep = np.linspace(0, actor.energyrange, actor.energybins + 1) +E_db = np.linspace(0, 200, 500) +ind_db = np.abs(E_db[None, :] - Ep[:, None]).argmin( + axis=1 +) # LOOKHERE :: should find a better way to build the index list + +if MJ: # multijobs or not ?? + img_p = itk.imread(os.path.join(path.output, f"{File_name}_merged_prot_e.nii.gz")) +else: + img_p = itk.imread(os.path.join(path.output, f"{File_name}_0_prot_e.nii.gz")) + +array_p = itk.array_from_image(img_p) +treated_array_p = np.zeros( + (250, array_p.shape[1], array_p.shape[2], array_p.shape[3]) +) # Initialize the treated array for proton + +treated_array_n = np.zeros( + (250, array_p.shape[1], array_p.shape[2], array_p.shape[3]) +) # Initialize the treated array for neutron +gamma_array = np.zeros( + (250, array_p.shape[1], array_p.shape[2], array_p.shape[3]) +) # Initialize the treated array for neutron AND proton + +if neutr: + if MJ: + img_n = itk.imread( + os.path.join(path.output, f"{File_name}_merged_neutr_e.nii.gz") + ) + else: + img_n = itk.imread(os.path.join(path.output, f"{File_name}_0_neutr_e.nii.gz")) + array_n = itk.array_from_image(img_n) + +# treatment of the image voxel by voxel => to store gamma emission spectrum in each voxel +for x in range(array_p.shape[3]): + for y in range(array_p.shape[2]): + for z in range(array_p.shape[1]): + # Get the Hounsfield Unit (HU) value at the current voxel + if ct_object.size == actor.size: + # If the CT size matches the actor size, use the voxel coordinates directly + X, Y, Z = x, y, z + else: + X, Y, Z = tle_simu.redim((x, y, z), ct_object, actor, array_p) + if ( + UH_array[Z, Y, X] == -3024 + ): # LOOKHERE : conditions of the CT where there is no matter, to be removed if CT different from the exemple case + # Skip the voxel if HU value is -3024 + continue + name = tle_simu.voxel_to_mat_name(UH_array[Z, Y, X], tle_simu.voxel_mat) + histo_E_p = array_p[ + :, z, y, x + ] # Get the energy histogram for the current voxel + spectrum_p = np.zeros(250) # Initialize the spectrum for the current voxel + if histo_E_p.sum() != 0: # if nothing is detected, the voxel is not treated + Gamma_m_p = gamma_proton[name] + Gamma_p = Gamma_m_p[ind_db] + spectrum_p = np.dot( + histo_E_p, Gamma_p + ) # np.dot : method used for the spectrum computation BUT, can be optimised ??? + treated_array_p[:, z, y, x] = spectrum_p + if neutr: + histo_E_n = array_n[:, z, y, x] + spectrum_n = np.zeros(250) + if histo_E_n.sum() != 0: + Gamma_m_n = gamma_neutron[name] + Gamma_n = Gamma_m_n[ind_db] + spectrum_n = np.dot( + histo_E_n, Gamma_n + ) # np.dot : method used for the spectrum computation BUT, can be optimised ??? + treated_array_n[:, z, y, x] = spectrum_n + +# Create a new ITK image from the treated array +gamma_array += treated_array_p + treated_array_n +itk_output = itk.image_from_array(treated_array_p) +itk_output.CopyInformation(img_p) +if MJ: + itk.imwrite(itk_output, path.output / f"VPG_{File_name}_merged_prot_e.nii.gz") +else: + itk.imwrite(itk_output, path.output / f"VPG_{File_name}_0_prot_e.nii.gz") +if neutr: + itk_output = itk.image_from_array(treated_array_n) + itk_output.CopyInformation(img_n) + if MJ: + itk.imwrite(itk_output, path.output / f"VPG_{File_name}_merged_prot_e.nii.gz") + else: + itk.imwrite(itk_output, path.output / f"VPG_{File_name}_0_neutr_e.nii.gz") + +itk_output = itk.image_from_array(gamma_array) +if MJ: + itk.imwrite(itk_output, path.output / f"VPG_{File_name}_merged_gamma_e.nii.gz") +else: + itk.imwrite(itk_output, path.output / f"VPG_{File_name}_0_gamma_e.nii.gz") diff --git a/opengate/contrib/vpgTLE/stageX/stage1/variables.py b/opengate/contrib/vpgTLE/stageX/stage1/variables.py new file mode 100644 index 000000000..bb38277a2 --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/stage1/variables.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import uproot +import opengate as gate +from opengate.tests import utility +from opengate.definitions import elements_name_symbol +import os +import numpy as np +from stage_1a import * + + +gcm3 = gate.g4_units.g_cm3 + + +class tle_sim: + def __init__(self, sim): + paths = utility.get_default_test_paths(__file__, output_folder=output) + self.path = paths + self.sim = sim + self.ct = sim.volume_manager.volumes[vol_name] + self.act = sim.actor_manager.actors[actor_name] + actors_list = [] + if self.act.prot_E.active: + actors_list.append("prot_e.nii.gz") + if self.act.neutr_E.active: + actors_list.append("neutr_e.nii.gz") + self.actor_list = actors_list + + f1 = str(paths.data / "Schneider2000MaterialsTable.txt") + f2 = str(paths.data / "Schneider2000DensitiesTable.txt") + tol = 0.05 * gcm3 + ( + voxel_materials, + materials, + ) = gate.geometry.materials.HounsfieldUnit_to_material(sim, tol, f1, f2) + (mat_fraction, el) = gate.geometry.materials.HU_read_materials_table(f1) + mat_fraction.pop() + database_mat = gate.geometry.materials.write_material_database( + sim, materials, str(paths.data / "database.db") + ) + self.mat_fraction = mat_fraction + self.voxel_mat = voxel_materials + self.el = el + self.database_mat = database_mat + + p = os.path.abspath(str(paths.data / "database.db")) + self.data_way = p + + f3 = str(paths.data / "data_merge_proton.root") + f4 = str(paths.data / "data_merge_neutron.root") + self.root_file = uproot.open(f3) + self.root_file_neutron = uproot.open(f4) + + def redim( + self, ind, ct, actor_vol, array + ): # to convert the index from the actor volume to the ct volume + + # centres ct et actor + ct_trans = list(ct.translation) + vol_trans = list(actor_vol.translation) + + # distance entre le centre du ct et le centre du vol + decal = [ + vol_trans[2] - ct_trans[2], + vol_trans[1] - ct_trans[1], + vol_trans[0] - ct_trans[0], + ] + + # calculer la position du voxel dans le volume de l'actor + X = ind[0] * actor_vol.spacing[2] + Y = ind[1] * actor_vol.spacing[1] + Z = ind[2] * actor_vol.spacing[0] + + # calculer la position du voxel dans le ct + x = (X + decal[0]) / ct.spacing[0] + y = (Y + decal[1]) / ct.spacing[1] + z = (Z + decal[2]) / ct.spacing[2] + + return int(x), int(y), int(z) + + def find_emission_vector( + self, el, root_data + ): # to find the TH2D in the root_data corresponding to the element el + if el == "Phosphor": + el = "Phosphorus" + el = elements_name_symbol[el] + histo = root_data[el]["GammaZ"].to_hist() + w = histo.to_numpy()[0] + return w + + def density_mat( + self, mat, data_way + ): # read the datas to find the density of the material + with open(data_way, "r") as f: + mat = mat + ":" + for line in f: + words = line.split() + if len(words) < 1: + continue + if words[0] == mat: + rho = words[1] + if words[2] == "mg/cm3;": + return float(rho[2:]) / 1000 # to convert the mg/cm3 to g/cm3 + return float(rho[2:]) + return "DEFAULT" + + def voxel_to_mat_name(self, UH, mat_data): # from UH to material name + ind = 0 + for l in mat_data: + ind = ind + 1 + if ind == len(mat_data): + mat = l[2] + if UH > l[1]: + continue + else: + mat = l[2] + break + return mat # str + + def mat_to_UH(self, mat, mat_data): # from material to UH + mat = mat.lower() + for l in mat_data: + print(l[2]) + if l[2] == mat.capitalize(): + return l[0] + return "DEFAULT" + + def liste_el_frac( + self, mat, frac_data + ): # from material, extract a list of element and a list of fraction. Element and its corresponding fraction are stored with the same index in both lists + if mat[-3] == "_": + mat = mat[:-3] + else: + mat = mat[:-2] + liste = [] + frac = [] + for l in frac_data: + if l["name"] == mat: + for el in l: + if (el != "HU") and (el != "name"): + if l[el] != 0.0: + liste.append(el) + frac.append(l[el]) + return liste, frac + + def gamma_mat( + self, name, root_data + ): # construct the GammaZ corresponding to the material + Gamma = np.zeros((500, 250)) # Initialize a 2D array for gamma emission + + """Find the material and its composition corresponding to the Hounsfield Unit (UH)""" + rho_mat = self.density_mat(name, self.data_way) + (elements, fractions) = self.liste_el_frac(name, self.mat_fraction) + + """ elements: list of elements in the material""" + for el in elements: + w = fractions[elements.index(el)] / 100 # Convert percentage to fraction + vect = self.find_emission_vector(el, root_data) + Gamma += vect * w * rho_mat + return Gamma diff --git a/opengate/contrib/vpgTLE/stageX/time-patient.py b/opengate/contrib/vpgTLE/stageX/time-patient.py new file mode 100644 index 000000000..ceed5aafa --- /dev/null +++ b/opengate/contrib/vpgTLE/stageX/time-patient.py @@ -0,0 +1,78 @@ +import itk +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.ticker as mtick + +dossier = "patient" +part = "prot" +X = 62 +Y = 66 +Z = 94 +case = "try" +# Read input image +itk_image = itk.imread("output/" + dossier + "/tle_patient_0_" + part + "_tof.nii.gz") +# Copy of itk.Image, pixel data is copied +array = itk.array_from_image(itk_image) +# Read input image +itk_image = itk.imread("output/" + dossier + "/ana_patient_0_" + part + "_tof.nii.gz") +# Copy of itk.Image, pixel data is copied +ana_array = itk.array_from_image(itk_image) +N = 1e7 + +ana_histogram = np.zeros(ana_array.shape[0]) +ana_histogram = ana_array[:, Z, Y, X] +# ana_histogram = ana_histogram[:-1] + +histogram = np.zeros(array.shape[0]) +histogram = array[:, Z, Y, X] + +ana_norma = np.sum(ana_histogram) +norma = np.sum(histogram) + +ana_histogram = ana_histogram / ana_norma +histogram = histogram / norma + +inc = np.sqrt(ana_histogram / N) +inc = inc / ana_norma +print(max(inc)) + +# Define the energy axis (assuming the histogram bins correspond to energy levels) +energy_axis = np.linspace( + 0, 5, len(ana_histogram) +) # Adjust the range and number of bins as needed + +# Plot the histogram +plt.figure(figsize=(10, 6)) +plt.plot(energy_axis, ana_histogram, label=f"analog MC", color="red", linewidth=1) +plt.plot(energy_axis, histogram, label=f"vpgtle MC", color="green", linewidth=1) + +# Add labels, title, and grid +plt.xlabel("Time of flight (ns)", fontsize=14) +plt.ylabel("PG yield / 20 ps / protons", fontsize=14) +if part == "prot": + plt.xlim(0, 5) +else: + plt.xlim(0, 5) +# Enable minor ticks for a more precise grid +plt.minorticks_on() +plt.title( + f"PG emission probability depending on " + part + "ons ToF [" + str(Y * 4) + " mm]", + fontsize=16, +) + +# Customize the grid +plt.grid(which="major", linestyle="-", linewidth=0.8, alpha=0.8) # Major grid lines +plt.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) # Minor grid lines +plt.ticklabel_format(axis="y", style="sci", scilimits=(0, 0)) + +# Add a legend +plt.legend(fontsize=12) +plt.savefig( + "output/" + dossier + "/ToF_" + part + "on_" + case + ".pdf", + dpi=300, + format="pdf", + bbox_inches="tight", +) +# Show the plot + +plt.show() diff --git a/opengate/tests/src/test087_multijobs.py b/opengate/tests/src/test087_multijobs.py new file mode 100644 index 000000000..7b3ef31cf --- /dev/null +++ b/opengate/tests/src/test087_multijobs.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python +import argparse +import os +import itk +from multiprocessing import Pool + +from opengate.tests import utility +import matplotlib.pyplot as plt +import opengate as gate +import glob +import numpy as np +import pyvista as pv +from scipy.optimize import curve_fit + +from scipy.spatial.transform import Rotation as R +import spekpy as sp + +#import critical_angle +#import psutil +from test_088_vpg_tle_script import * +DEFAULT_OUTPUT = 'debug_new' #file name where single root file are stocked + +DEFAULT_NUMBER_OF_JOBS = 1 +DEFAULT_NUMBER_OF_PARTICLES = 1e6 +DEFAULT_POSITION = [0, 0, 0] +DEFAULT_STEP_SIZE = float(1) + +DEFAULT_WORLD = "G4_Galactic" #default material of the world volume +DEFAULT_SAMPLE = "Water3ppmPt" #default material of the sample volume + +DEFAULT_FILE_NAME = "prestep" +DEFAULT_BEAM_ENERGY = 130 +DEFAULT_ACTOR = "VoxelizedPromptGammaAnalogActor" + +path = utility.get_default_test_paths(__file__, output_folder=DEFAULT_OUTPUT) + +def opengate_run( + output=DEFAULT_OUTPUT, + job_id=0, + number_of_particles=DEFAULT_NUMBER_OF_PARTICLES, + visu=True, + verbose=True, + polarization=False, + energy_type=False, + world=DEFAULT_WORLD, + sample_material=DEFAULT_SAMPLE, + File_name=DEFAULT_FILE_NAME, + collimation=False, + energy = DEFAULT_BEAM_ENERGY, + position=DEFAULT_POSITION, + actor = DEFAULT_ACTOR, + ): + + if verbose: + print(f"Running MC… " + str(locals())) #locals = variable locales soit les paramètres de la fonction + + # Call the simulation function and retrieve sim and simulation_parameters + sim, ct, vpg_actor, source = simulation(output, File_name, job_id, number_of_particles, visu, verbose, actor) + + sim.run() + + #visualization + + # === Charger le fichier VRML avec PyVista === + """print("📂 Chargement du fichier VRML pour affichage...") + pl = pv.Plotter() + pl.import_vrml("simulation.wrl") # Charger la scène OpenGATE + + # === Ajouter des axes colorés pour l'orientation === + axes = pv.Axes() + axes.axes_actor.total_length = [1, 1, 1] # Taille des axes + axes.axes_actor.x_axis_shaft_properties.color = (1, 0, 0) # Rouge pour X + axes.axes_actor.y_axis_shaft_properties.color = (0, 1, 0) # Vert pour Y + axes.axes_actor.z_axis_shaft_properties.color = (0, 0, 1) # Bleu pour Z + pl.add_actor(axes.axes_actor) + + # === Paramètres d'affichage === + pl.background_color = "black" # Fond noir pour meilleur contraste + pl.show_grid() # Afficher une grille + pl.show() # Afficher la scène 3D""" + +def itk_merge( + output=DEFAULT_OUTPUT, + verbose=False, + File_name=DEFAULT_FILE_NAME, + number_of_jobs=DEFAULT_NUMBER_OF_JOBS, +): + def print_verbose(*args, **kwargs): + if verbose: + print(*args, **kwargs) + if not os.path.exists(output): + os.makedirs(output) + if number_of_jobs == 1 : + print_verbose("Skipping merge, only one job was run.") + return + try: + # Find all .nii.gz files matching the pattern + file_pattern = os.path.join(f"/home/vguittet/Documents/G10/stage1.5/output/{output}/{File_name}_*.nii.gz") + file_list = sorted(glob.glob(file_pattern)) # Get all matching files + count = number_of_jobs + if not file_list: + raise FileNotFoundError(f"No .nii.gz files found matching pattern: {file_pattern}") + actor_list = [] + type_list = [] + for file_path in file_list: + base_name = os.path.basename(file_path) + actor_name = base_name.split("_")[1] + type_name = base_name.split("_")[3] + "_" + base_name.split("_")[4] # e.g., neutr_e + + if actor_name not in actor_list: + actor_list.append(actor_name) + if type_name not in type_list: + type_list.append(type_name) + init = itk.imread(path.output/f"{File_name}_{actor_list[0]}_1_{type_list[0]}") + init_arr = itk.array_from_image(init) + merged_array = np.zeros_like(init_arr, dtype=np.float32) + for type_name in type_list: + for actor_name in actor_list: + merged_array.fill(0) # Initialize the merged array + for i in range(count): + file_path = path.output/f"{File_name}_{actor_name}_{i + 1}_{type_name}" + # Check if the file exists + if not os.path.exists(file_path): + print(f"File not found: {file_path}") + continue + image = itk.imread(file_path) + array = itk.array_from_image(image) + merged_array = merged_array + array / count # Accumulate the pixel values + merged_image = itk.image_from_array(merged_array) + merged_image.CopyInformation(init) # Copy metadata from the initial image + output_file = path.output/f"{File_name}_{actor_name}_merged_{type_name}" + print_verbose(f"Saving merged image to: {output_file}") + itk.imwrite(merged_image, output_file) + + + + except FileNotFoundError as e: + print(e) + + +def opengate_pool_run( + output, + number_of_jobs, #mettre autant que de positions sinon on ne simule pas toutes les particules + number_of_particles, + visu, + verbose, + polarization, + energy_type, + world, + sample_material, + File_name, + collimation, + energy, + #step, + #size, + +): + + with Pool(maxtasksperchild=1) as pool: + + results = [] + number_of_particles_per_job = int(number_of_particles / number_of_jobs) + mm = gate.g4_units.mm + um = gate.g4_units.um + cm = gate.g4_units.cm + + position = [0, 0, 20*cm] #position initiale à 30um -> trop proche de la source + #delta_x = 0.1*mm #pas du raster scan pour passer d'un pixel à l'autre + #delta_x = step*mm + #delta_y = -step*mm + job_id = 0 + #image_size = size + + + """ for y in range(image_size) : #lignes + for x in range(image_size) : #colonnes + job_id += 1 + # Calcule la nouvelle position à partir de la base + position = [ + x * delta_x, + y * delta_y, + position[2] + ] + #le carré décrit est balayé de gauche à droite et de haut en bas + + + copied_position = position.copy() + print(f"launching job #{job_id}/{number_of_jobs} with position x={copied_position[0]/mm:.2f} mm, y={copied_position[1]/mm:.2f} mm") + + result = pool.apply_async(opengate_run, kwds={ + 'output': output, + 'job_id': job_id, + 'number_of_particles': number_of_particles_per_job, + 'visu': visu, + 'verbose': verbose, + 'position': copied_position + + #'position' : position + }) + results.append(result)""" + + + + + for i in range(number_of_jobs): + + job_id = i+1 + #copied_position = position.copy() + print(f"launching job #{job_id}/{number_of_jobs}") + #print(f"launching job #{job_id}/{number_of_jobs} with position x={copied_position[0]/mm:.2f} mm") + result = pool.apply_async(opengate_run, kwds={ + 'output': output, + 'job_id': job_id, + 'number_of_particles': number_of_particles_per_job, + 'visu': visu, + 'verbose': verbose, + 'position' : position, + 'polarization': polarization, + 'energy_type': energy_type, + 'world' : world, + 'sample_material' : sample_material, + 'File_name': File_name, + 'collimation' : collimation, + 'energy' : energy, + #'position' : copied_position, + }) + results.append(result) + #position[0] += delta_x + + + + + + pool.close() + pool.join() + + for result in results: + result.wait() + if not result.successful(): + print("Failure in MC simulation") + exit(1) + + itk_merge( + output=output, + verbose=verbose, + File_name=File_name, + number_of_jobs=number_of_jobs, + ) + +def main(): + + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('--output', help="Path of outputs", default=DEFAULT_OUTPUT) + parser.add_argument('-j', '--number-of-jobs', help="Number of jobs", default=DEFAULT_NUMBER_OF_JOBS, type=int) + parser.add_argument('-n', '--number-of-particles', help="Number of generated particles (total)", default=DEFAULT_NUMBER_OF_PARTICLES, type=float) + parser.add_argument('--visu', help="Visualize Monte Carlo simulation", default=False, action='store_true') + parser.add_argument('--verbose', '-v', help="Verbose execution", default=False, action='store_true') + parser.add_argument('-p', '--polarization', help="Polarization of the beam", default=False, action='store_true') + parser.add_argument('--energy_type', help="Type of energy distribution", default=False, action='store_true')#True active le mode polychromatique, rendre la commande plus clair après + parser.add_argument('-w', '--world', help="World's material", default=DEFAULT_WORLD, type=str)#pas besoin de mettre les guillemets au moment de la commande + parser.add_argument('-s', '--sample_material', help="sample's material", default=DEFAULT_SAMPLE, type=str) + parser.add_argument('-f', '--File_name', help="name of the output file", default=DEFAULT_FILE_NAME, type=str) + parser.add_argument('-c', '--collimation', help="collimation of the beam", default=False, action='store_true') + parser.add_argument('-e', '--energy', help="Energy of the beam in keV", default=DEFAULT_BEAM_ENERGY, type= int) + #parser.add_argument('-step', '--step', help="size of a step between two succesive positions en mm", default=DEFAULT_STEP_SIZE, type=float) + #parser.add_argument('-size', '--size', help="number of pixels in lines/columns", default=DEFAULT_STEP_SIZE, type=int) + #parser.add_argument('-pos', '--number-of-positions', help="Number of positions to simulate", default=DEFAULT_NUMBER_OF_POSITIONS, type=int) + #argument position initiale ? + #argument pour le nombre de position à couvrir (taille de l'image à balayer en gros) + args_info = parser.parse_args() + + opengate_pool_run(**vars(args_info)) + #opengate_run(output, 1, number_of_particles, visu, verbose) +if __name__ == '__main__': diff --git a/opengate/tests/src/test087_spectrum.py b/opengate/tests/src/test087_spectrum.py new file mode 100755 index 000000000..4bfaf65ba --- /dev/null +++ b/opengate/tests/src/test087_spectrum.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import itk +import uproot +import hist +import opengate as gate + +from opengate.tests import utility +import os +import glob +import numpy as np + +# import matplotlib.pyplot as plt +# import matplotlib.ticker as mticker +from variables import tle_sim +from simu_lecture import simulation +from multijobs2 import ( + DEFAULT_OUTPUT, + DEFAULT_FILE_NAME, + DEFAULT_NUMBER_OF_PARTICLES, + DEFAULT_ACTOR, +) + +simu, ct, vpg_actor, source = simulation( + output=DEFAULT_OUTPUT, + File_name=DEFAULT_FILE_NAME, + job_id=0, + number_of_particles=DEFAULT_NUMBER_OF_PARTICLES, + visu=False, + verbose=False, + actor=DEFAULT_ACTOR, +) + +tle_simu = tle_sim(simu, ct, vpg_actor, source) + +# FIRST STEP : retrieve the arrays and all the needed features +# CT volume : + + +ct_object = tle_simu.ct +UH_array = itk.array_from_image(ct_object.load_input_image()) + + +path = tle_simu.path +actor_object = tle_simu.vpg_actor +feature = ["prot_e", "neutr_e"] +File_name = DEFAULT_FILE_NAME + +# database + +data_protons = tle_simu.root_file +data_neutrons = tle_simu.root_file_neutron + +gamma_neutron = {} +gamma_proton = {} + +for x in range(UH_array.shape[2]): + for y in range(UH_array.shape[1]): + for z in range(UH_array.shape[0]): + UH = UH_array[z, y, x] + if UH not in gamma_neutron: + gamma_neutron[UH] = tle_simu.gamma_mat(UH, data_neutrons, False) + if UH not in gamma_proton: + gamma_proton[UH] = tle_simu.gamma_mat(UH, data_protons, True) + + +Ep = np.linspace(0, actor_object.energyrange, actor_object.energybins + 1) + +output_name = actor_object.name +for type_name in feature: + file_path = os.path.join( + path.output, f"{File_name}_{output_name}_1_{type_name}.nii.gz" + ) + + print(file_path) + # Check if the file exists + if not os.path.exists(file_path): + print(f"File {file_path} not found in output directory.") + continue # Skip to the next iteration if the file does not exist + # Read the image + img = itk.imread(file_path) + + if img is None: + print(f"Image {output_name}_merged_{type_name} not found in output directory.") + # Convert the image to a numpy array + array = itk.array_from_image(img) + treated_array = np.zeros( + (250, array.shape[1], array.shape[2], array.shape[3]) + ) # Initialize the treated array + for x in range(array.shape[3]): + for y in range(array.shape[2]): + for z in range(array.shape[1]): + # Get the Hounsfield Unit (HU) value at the current voxel + X, Y, Z = tle_simu.redim((x, y, z), ct_object, actor_object, array) + UH = UH_array[X, Y, Z] + histo_E = array[ + :, z, y, x + ] # Get the energy histogram for the current voxel + Gamma_m = ( + gamma_proton[UH] + if type_name == "prot_e.nii.gz" + else gamma_neutron[UH] + ) + spectrum = np.zeros( + 250 + ) # Initialize the spectrum for the current voxel + for i in range(len(histo_E) - 1): + print(histo_E[i]) + En = ( + i * actor_object.energyrange / actor_object.energybins + ) # Calculate the energy for the current bin + bin_index = int( + En / (200 / 500) + ) # Determine the bin index for the current energy value + spectrum = spectrum + Gamma_m[bin_index, :] * histo_E[i] + + treated_array[:, z, y, x] = treated_array[:, z, y, x] + spectrum +# Create a new ITK image from the treated array +itk_output = itk.image_from_array(treated_array) +itk_output.CopyInformation(img) + +itk.imwrite(itk_output, path.output / f"VPG_{output_name}_Energy.nii.gz") diff --git a/opengate/tests/src/test087_variables.py b/opengate/tests/src/test087_variables.py new file mode 100755 index 000000000..17938f065 --- /dev/null +++ b/opengate/tests/src/test087_variables.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import itk +import uproot +import hist +import opengate as gate +import xraylib +import periodictable +from opengate.tests import utility +from opengate.definitions import elements_name_symbol +import os +import numpy as np +from test_088_vpg_tle_script import * + +from test_088_multijobs import ( + DEFAULT_OUTPUT, + DEFAULT_FILE_NAME, + DEFAULT_NUMBER_OF_PARTICLES, + DEFAULT_ACTOR, +) + +gcm3 = gate.g4_units.g_cm3 + + +class tle_sim: + def __init__(self, sim, ct, actor, source): + paths = utility.get_default_test_paths(__file__, output_folder=DEFAULT_OUTPUT) + self.path = paths + self.sim = sim + self.ct = ct + self.vpg_actor = actor + f1 = str(paths.data / "Schneider2000MaterialsTable.txt") + f2 = str(paths.data / "Schneider2000DensitiesTable.txt") + tol = 0.05 * gcm3 + ( + voxel_materials, + materials, + ) = gate.geometry.materials.HounsfieldUnit_to_material(sim, tol, f1, f2) + (mat_fraction, el) = gate.geometry.materials.HU_read_materials_table(f1) + mat_fraction.pop() + database_mat = gate.geometry.materials.write_material_database( + sim, materials, str(paths.data / "database.db") + ) + + self.mat_fraction = mat_fraction + self.voxel_mat = voxel_materials + self.el = el + self.database_mat = database_mat + p = os.path.abspath(str(paths.data / "database.db")) + + self.data_way = p + f3 = str(paths.data / "data_merge.root") + f4 = str(paths.data / "data_merge_neutron.root") + self.root_file = uproot.open(f3) + self.root_file_neutron = uproot.open(f4) + + def redim( + self, ind, ct, actor_vol, array + ): # to convert the index from the actor volume to the ct volume + ct_trans = list(ct.translation) # centre du ct + vol_trans = list(actor_vol.translation) + # passer du centre au côtés droit voxel + ct_size = list(array.shape) # size du ct + + ct_size = array.shape + ct_space = list(ct.spacing) + ct_len = ct_size[0] * ct_space[0] + ct_wid = ct_size[1] * ct_space[1] + ct_hig = ct_size[2] * ct_space[2] + + # côté droit du vol + x_center = ct_trans[2] - ct_len / 2 + y_center = ct_trans[1] - ct_wid / 2 + z_center = ct_trans[0] - ct_hig / 2 + # distance entre le centre du ct et le centre du vol + decal = [ + vol_trans[0] - x_center, + vol_trans[1] - y_center, + vol_trans[2] - z_center, + ] + # calculer la position du voxel dans le vol + X = ind[0] * actor_vol.spacing[0] + Y = ind[1] * actor_vol.spacing[1] + Z = ind[2] * actor_vol.spacing[2] + # calculer la position du voxel dans le ct + + x = (X + decal[0]) / ct_space[0] + y = (Y + decal[1]) / ct_space[1] + z = (Z + decal[2]) / ct_space[2] + return int(x), int(y), int(z) + + def find_emission_vector(self, el, root_data, prot): + if el == "Phosphor": + el = "Phosphorus" + el = elements_name_symbol[el] + if prot: + histo = root_data[el][ + "PGs energy as a function of protons and GAMMAZ" + ].to_hist() + else: + histo = root_data[el][ + "PGs energy as a function of neutrons and GAMMAZ" + ].to_hist() + w = histo.to_numpy()[0] + return w + + def density_mat(self, mat, data_way): + with open(data_way, "r") as f: + mat = mat + ":" + for line in f: + words = line.split() + if len(words) < 1: + continue + if words[0] == mat: + rho = words[1] + if words[2] == "mg/cm3;": + return float(rho[2:]) / 1000 # to convert the mg/cm3 to g/cm3 + return float(rho[2:]) + return "DEFAULT" + + def voxel_to_mat_name(self, UH, mat_data): + ind = 0 + for l in mat_data: + ind = ind + 1 + if ind == len(mat_data): + mat = l[2] + if UH > l[1]: + continue + else: + mat = l[2] + break + return mat # str + + def mat_to_UH(self, mat, mat_data): + mat = mat.lower() + for l in mat_data: + print(l[2]) + if l[2] == mat.capitalize(): + return l[0] + return "DEFAULT" + + def liste_el_frac(self, mat, frac_data): + if mat[-3] == "_": + mat = mat[:-3] + else: + mat = mat[:-2] + liste = [] + frac = [] + for l in frac_data: + + if l["name"] == mat: + for el in l: + if (el != "HU") and (el != "name"): + if l[el] != 0.0: + + liste.append(el) + frac.append(l[el]) + return liste, frac + + def Volume_to_array(self, vol, actor): + size = vol.size + spacing = actor.spacing + array_el = np.empty( + ( + int(size[0] / spacing[0]), + int(size[1] / spacing[0]), + int(size[2] / spacing[0]), + ), + dtype=object, + ) + array_el.fill(vol.material) # fill the array with the material of the volume + return array_el # 3D array + + def gamma_mat(self, UH, root_data, prot): + Gamma = np.zeros((500, 250)) # Initialize a 2D array for gamma emission + """Find the material and its composition corresponding to the Hounsfield Unit (UH)""" + name = self.voxel_to_mat_name(UH, self.voxel_mat) + rho_mat = self.density_mat(name, self.data_way) + (elements, fractions) = self.liste_el_frac(name, self.mat_fraction) + """ elements: list of elements in the material""" + for el in elements: + w = fractions[elements.index(el)] / 100 # Convert percentage to fraction + vect = self.find_emission_vector(el, root_data, prot) + Gamma += vect * w * rho_mat + return Gamma diff --git a/opengate/tests/src/test087_vpg_tle_script.py b/opengate/tests/src/test087_vpg_tle_script.py new file mode 100755 index 000000000..88f9226fe --- /dev/null +++ b/opengate/tests/src/test087_vpg_tle_script.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# import itk +import uproot +import hist +import opengate as gate + +# import xraylib +# import periodictable +from opengate.tests import utility +import os +import numpy as np + +# import matplotlib.pyplot as plt +# import matplotlib.ticker as mticker +import test_088_variables + + +def simulation( + output, + File_name, + job_id, + number_of_particles, + visu=False, + verbose=False, + actor="VoxelizedPromptGammaTLEActor", +): + + paths = utility.get_default_test_paths(__file__, output_folder=output) + + # create the simulation + sim = gate.Simulation() + # main options + sim.visu = visu + sim.g4_verbose = verbose + sim.random_seed = "auto" # FIXME to be replaced by a fixed number at the end + sim.random_engine = "MersenneTwister" + sim.output_dir = paths.output + sim.number_of_threads = 1 + sim.progress_bar = True + + # units + mm = gate.g4_units.mm + cm = gate.g4_units.cm + m = gate.g4_units.m + gcm3 = gate.g4_units.g_cm3 + keV = gate.g4_units.keV + MeV = gate.g4_units.MeV + Bq = gate.g4_units.Bq + ns = gate.g4_units.ns + mg = gate.g4_units.mg + cm3 = gate.g4_units.cm3 + g = gate.g4_units.g + km = gate.g4_units.km + + # change world size + world = sim.world + world.size = [3 * m, 3 * m, 3 * m] + world.material = "G4_Galactic" + + # cas test = boite à un élément + ct = sim.add_volume("Box", "ct") + ct.mother = "world" + ct.translation = [0 * cm, 15 * cm, 0 * cm] + ct.size = [4 * cm, 20 * cm, 4 * cm] + ct.material = "G4_C" + ct.color = [1, 0, 0, 1] + + """# insert voxelized CT + ct = sim.add_volume("Image", "ct") + ct.image = paths.data / f"ct_4mm.mhd" + if sim.visu: + ct.image = paths.data / f"ct_40mm.mhd" + ct.material = "G4_AIR" + f1 = str(paths.data / "Schneider2000MaterialsTable.txt") + f2 = str(paths.data / "Schneider2000DensitiesTable.txt") + tol = 0.05 * gcm3 + ( + ct.voxel_materials, + materials, + ) = gate.geometry.materials.HounsfieldUnit_to_material(sim, tol, f1, f2) + print(f"tol = {tol/gcm3} g/cm3") + print(f"mat : {len(ct.voxel_materials)} materials") + + ct.dump_label_image = paths.output / "labels.mhd" + ct.mother = "world" + """ + + # physics + sim.physics_manager.physics_list_name = "QGSP_BIC_HP_EMY" + + # sim.physics_manager.set_production_cut("world", "gamma", 1 * km) + # sim.physics_manager.set_production_cut("world", "electron", 1 * km) + # sim.physics_manager.set_production_cut("world", "proton", 1 * km) + # sim.physics_manager.set_production_cut("world", "positron", 1 * km) + + sim.physics_manager.set_production_cut("ct", "gamma", 1 * mm) + # sim.physics_manager.set_production_cut("ct", "electron", 1 * mm) + sim.physics_manager.set_production_cut("ct", "proton", 1 * mm) + # sim.physics_manager.set_production_cut("ct", "positron", 1 * mm) + + sim.physics_manager.set_max_step_size("ct", 1 * mm) + sim.physics_manager.set_user_limits_particles(["proton"]) + + # source of proton + # FIXME to replace by a more realistic proton beam, see tests 044 + source = sim.add_source("GenericSource", "proton_beam") + source.energy.mono = 150 * MeV + source.particle = "proton" + source.position.type = "disc" + source.position.radius = 0 + source.position.translation = [0, 0, 0] + source.n = number_of_particles + source.direction.type = "momentum" + source.direction.momentum = [0, 1, 0] + + # add vpgtle actor + vpg_tle = sim.add_actor(actor, "vpg_tle") + vpg_tle.attached_to = "ct" + vpg_tle.output_filename = paths.output / f"{File_name}_appic_{job_id}.nii.gz" + vpg_tle.size = [40, 400, 40] + vpg_tle.spacing = [1 * mm, 1 * mm, 1 * mm] + vpg_tle.translation = [0, 0, 0] + vpg_tle.timebins = 200 + vpg_tle.timerange = 1 * ns + vpg_tle.energybins = 250 + vpg_tle.energyrange = 150 * MeV + vpg_tle.prot_E.active = False + + vpg_tle.neutr_E.active = True + vpg_tle.prot_tof.active = False + vpg_tle.neutr_tof.active = True + + # add stat actor + stats = sim.add_actor("SimulationStatisticsActor", "stats") + stats.track_types_flag = True + stats.output_filename = paths.output / f"stat_{job_id}.txt" + + return sim, ct, vpg_tle, source + + +"""output = "TEST" +File_name = "test" +number_of_particles = 1000 +actor = "VoxelizedPromptGammaTLEActor" + +if __name__ == "__main__": + sim = simulation(output, File_name, 0, number_of_particles,False, False, actor) + # start simulation + sim.run()""" diff --git a/opengate/tests/src/test087_vpg_tle_stage1_actor.py b/opengate/tests/src/test087_vpg_tle_stage1_actor.py index 609294b78..a21d9ca19 100755 --- a/opengate/tests/src/test087_vpg_tle_stage1_actor.py +++ b/opengate/tests/src/test087_vpg_tle_stage1_actor.py @@ -60,7 +60,7 @@ source.particle = "proton" source.position.type = "sphere" source.position.radius = 10 * mm - source.position.translation = [0, -44 * cm, 0] + source.position.translation = [0, 0 * cm, 0] source.activity = 10000 * Bq source.direction.type = "momentum" source.direction.momentum = [0, 1, 0] @@ -72,8 +72,9 @@ vpg_tle.size = [100, 100, 100] vpg_tle.bins = 50 vpg_tle.spacing = [2 * mm, 2 * mm, 2 * mm] + vpg_tle.activity = 1000 * Bq # FIXME to put elsewhere ? change the name ? - vpg_tle.stage_0_database = paths.data / "vpgtle_stage0.db" + # vpg_tle.stage_0_database = paths.data / "vpgtle_stage0.db" # add stat actor stats = sim.add_actor("SimulationStatisticsActor", "stats") From 9e7f3a5a99d57792702bc5e790bbe315194fb502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean=20M=20L=C3=A9tang?= Date: Thu, 4 Dec 2025 17:34:49 +0100 Subject: [PATCH 11/13] code Violette added --- core/external/pybind11 | 2 +- core/opengate_core/opengate_core.cpp | 3 +++ opengate/image.py | 9 ++++++--- opengate/tests/data | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/external/pybind11 b/core/external/pybind11 index 8e7307f06..a2e59f0e7 160000 --- a/core/external/pybind11 +++ b/core/external/pybind11 @@ -1 +1 @@ -Subproject commit 8e7307f0699726c29f5bdc2c177dd55b6b330061 +Subproject commit a2e59f0e7065404b44dfe92a28aca47ba1378dc4 diff --git a/core/opengate_core/opengate_core.cpp b/core/opengate_core/opengate_core.cpp index bc6314765..66d3b4bc4 100644 --- a/core/opengate_core/opengate_core.cpp +++ b/core/opengate_core/opengate_core.cpp @@ -308,6 +308,8 @@ void init_GateTLEDoseActor(py::module &m); void init_GateVoxelizedPromptGammaTLEActor(py::module &m); +void init_GateVoxelizedPromptGammaAnalogActor(py::module &m); + void init_GateFluenceActor(py::module &m); void init_GateLETActor(py::module &m); @@ -591,6 +593,7 @@ PYBIND11_MODULE(opengate_core, m) { init_GateDoseActor(m); init_GateTLEDoseActor(m); init_GateVoxelizedPromptGammaTLEActor(m); + init_GateVoxelizedPromptGammaAnalogActor(m); init_GateFluenceActor(m); init_GateLETActor(m); init_GateProductionAndStoppingActor(m); diff --git a/opengate/image.py b/opengate/image.py index 36040922d..723ff03b1 100644 --- a/opengate/image.py +++ b/opengate/image.py @@ -62,9 +62,12 @@ def create_3d_image( def create_3d_image_of_histogram( size, spacing, bins, origin=None, pixel_type="float", allocate=True ): - size.append(bins) - spacing.append(1.0) - if origin is not None: + if len(size) != 4: + size.append(bins) + spacing.append(1.0) + else: + size[3] = bins + if (origin is not None) and (len(origin) != 4): origin.append(0.0) return create_4d_image(size, spacing, origin, pixel_type, allocate) diff --git a/opengate/tests/data b/opengate/tests/data index 5c1b4b1e4..fdab18992 160000 --- a/opengate/tests/data +++ b/opengate/tests/data @@ -1 +1 @@ -Subproject commit 5c1b4b1e407bb88866df70b9aaa82a9ba803b8ea +Subproject commit fdab189926aff471f829a411af3de77ae1c05d98 From 6259a45e18fd413957689a0daa0cc682986897b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean=20M=20L=C3=A9tang?= Date: Mon, 8 Dec 2025 11:57:29 +0100 Subject: [PATCH 12/13] fetch+conflicts master --- core/opengate_core/opengate_core.cpp | 50 +- .../opengate_lib/GateDoseActor.cpp | 141 ++- .../opengate_lib/GateTLEDoseActor.cpp | 259 ++++-- opengate/actors/doseactors.py | 843 +++++++++++++++++- opengate/managers.py | 301 +++++-- 5 files changed, 1344 insertions(+), 250 deletions(-) diff --git a/core/opengate_core/opengate_core.cpp b/core/opengate_core/opengate_core.cpp index 66d3b4bc4..4420fb4ed 100644 --- a/core/opengate_core/opengate_core.cpp +++ b/core/opengate_core/opengate_core.cpp @@ -38,6 +38,8 @@ void init_G4MaterialCutsCouple(py::module &); void init_G4Element(py::module &); +void init_G4Isotope(py::module &); + void init_G4IonisParamMat(py::module &); void init_G4MaterialPropertiesTable(py::module &); @@ -174,6 +176,8 @@ void init_G4RegionStore(py::module &); // geometry/solids void init_G4Box(py::module &); +void init_G4Ellipsoid(py::module &); + void init_G4Polyhedra(py::module &); void init_G4Sphere(py::module &); @@ -286,6 +290,8 @@ void init_GateInfo(py::module &); void init_GateVActor(py::module &); +void init_GateWeightedEdepActor(py::module &); + void init_GateActorManager(py::module &); // Gate filters @@ -316,6 +322,10 @@ void init_GateLETActor(py::module &m); void init_GateProductionAndStoppingActor(py::module &m); +void init_GateBeamQualityActor(py::module &m); + +void init_GateEmCalculatorActor(py::module &m); + void init_GateARFActor(py::module &m); void init_GateARFTrainingDatasetActor(py::module &m); @@ -342,14 +352,17 @@ void init_GateSimulationStatisticsActor(py::module &); void init_GatePhaseSpaceActor(py::module &); -void init_GateOptrComptSplittingActor(py::module &m); - +// biasing void init_GateBOptrBremSplittingActor(py::module &m); -void init_GateOptrFreeFlightActor(py::module &m); +void init_GateGammaFreeFlightOptrActor(py::module &m); + +void init_GateScatterSplittingFreeFlightOptrActor(py::module &m); void init_G4VBiasingOperator(py::module &m); +void init_GateTLEDoseActor(py::module &m); + // Gate digit void init_GateVDigitizerWithOutputActor(py::module &); @@ -367,6 +380,8 @@ void init_GateDigitizerSpatialBlurringActor(py::module &m); void init_GateDigitizerEnergyWindowsActor(py::module &m); +void init_GateDigiAttributeProcessDefinedStepInVolumeActor(py::module &m); + void init_GateDigitizerProjectionActor(py::module &m); void init_GateDigiAttributeManager(py::module &m); @@ -377,7 +392,7 @@ void init_GateUniqueVolumeIDManager(py::module &); void init_GateUniqueVolumeID(py::module &); -void init_GateVolumeDepthID(py::module &m); +void init_GateGeometryUtils(py::module &); // Gate source void init_GateVSource(py::module &); @@ -412,6 +427,8 @@ void init_GateHelpers(py::module &); void init_GateVolumeVoxelizer(py::module &); +void init_GateImageBox(py::module &m); + PYBIND11_MODULE(opengate_core, m) { init_G4ThreeVector(m); @@ -428,6 +445,7 @@ PYBIND11_MODULE(opengate_core, m) { init_G4Material(m); init_G4MaterialCutsCouple(m); init_G4Element(m); + init_G4Isotope(m); init_G4IonisParamMat(m); init_G4MaterialPropertiesTable(m); init_GateMaterialMuHandler(m); @@ -485,6 +503,7 @@ PYBIND11_MODULE(opengate_core, m) { init_G4RegionStore(m); init_G4Box(m); + init_G4Ellipsoid(m); init_G4Polyhedra(m); init_G4Sphere(m); init_G4Trap(m); @@ -554,7 +573,7 @@ PYBIND11_MODULE(opengate_core, m) { init_G4VisAttributes(m); // interfaces -#if DUSE_USE_VISU > 0 +#if USE_VISU > 0 init_QMainWindow(m); init_G4UIExecutive(m); init_G4UIQt(m); @@ -564,6 +583,7 @@ PYBIND11_MODULE(opengate_core, m) { init_GateCheckDeex(m); init_GateInfo(m); init_GateVActor(m); + init_GateWeightedEdepActor(m); init_GateActorManager(m); init_GateVFilter(m); init_GateParticleFilter(m); @@ -590,6 +610,7 @@ PYBIND11_MODULE(opengate_core, m) { init_GateRunAction(m); init_GateEventAction(m); init_GateTrackingAction(m); + init_GateDoseActor(m); init_GateTLEDoseActor(m); init_GateVoxelizedPromptGammaTLEActor(m); @@ -597,13 +618,19 @@ PYBIND11_MODULE(opengate_core, m) { init_GateFluenceActor(m); init_GateLETActor(m); init_GateProductionAndStoppingActor(m); + init_GateBeamQualityActor(m); + init_GateEmCalculatorActor(m); init_GateSimulationStatisticsActor(m); - init_GatePhaseSpaceActor(m); + init_GateBOptrBremSplittingActor(m); - init_GateOptrComptSplittingActor(m); - init_GateOptrFreeFlightActor(m); + init_GateGammaFreeFlightOptrActor(m); + init_GateScatterSplittingFreeFlightOptrActor(m); + + init_GatePhaseSpaceActor(m); init_GateHitsCollectionActor(m); init_GateVDigitizerWithOutputActor(m); + init_GateDigiAttributeManager(m); + init_GateVDigiAttribute(m); init_GateHitsAdderActor(m); init_GateDigitizerReadoutActor(m); init_GateDigitizerBlurringActor(m); @@ -611,18 +638,19 @@ PYBIND11_MODULE(opengate_core, m) { init_GateDigitizerSpatialBlurringActor(m); init_GateDigitizerEnergyWindowsActor(m); init_GateDigitizerProjectionActor(m); + init_GateDigiAttributeProcessDefinedStepInVolumeActor(m); + init_GateARFActor(m); init_GateARFTrainingDatasetActor(m); init_GateKillActor(m); init_GateKillAccordingProcessesActor(m); init_GateAttenuationImageActor(m); - init_GateDigiAttributeManager(m); - init_GateVDigiAttribute(m); init_GateExceptionHandler(m); init_GateNTuple(m); init_GateHelpers(m); init_GateVolumeVoxelizer(m); init_GateUniqueVolumeIDManager(m); init_GateUniqueVolumeID(m); - init_GateVolumeDepthID(m); + init_GateGeometryUtils(m); + init_GateImageBox(m); } diff --git a/core/opengate_core/opengate_lib/GateDoseActor.cpp b/core/opengate_core/opengate_lib/GateDoseActor.cpp index c769b5c96..c56020abc 100644 --- a/core/opengate_core/opengate_lib/GateDoseActor.cpp +++ b/core/opengate_core/opengate_lib/GateDoseActor.cpp @@ -32,7 +32,16 @@ G4Mutex ComputeUncertaintyMutex = G4MUTEX_INITIALIZER; G4Mutex SetNbEventMutex = G4MUTEX_INITIALIZER; GateDoseActor::GateDoseActor(py::dict &user_info) - : GateVActor(user_info, true) {} + : GateVActor(user_info, true) { + + fUncertaintyGoal = 0.0; + fThreshEdepPerc = 0.0; + fOvershoot = 0.0; + fNbEventsFirstCheck = 0; + fNbEventsNextCheck = 0; + fGoalUncertainty = 0.0; + fNbOfEvent = 0; +} void GateDoseActor::InitializeUserInfo(py::dict &user_info) { // IMPORTANT: call the base class method @@ -40,13 +49,12 @@ void GateDoseActor::InitializeUserInfo(py::dict &user_info) { // translation fTranslation = DictGetG4ThreeVector(user_info, "translation"); - // Hit type (random, pre, post etc) + // Hit type (random, pre, post, etc) fHitType = DictGetStr(user_info, "hit_type"); } void GateDoseActor::InitializeCpp() { GateVActor::InitializeCpp(); - NbOfThreads = G4Threading::GetNumberOfRunningWorkerThreads(); // Create the image pointers // (the size and allocation will be performed on the py side) @@ -68,11 +76,11 @@ void GateDoseActor::InitializeCpp() { void GateDoseActor::BeginOfRunActionMasterThread(int run_id) { // Reset the number of events (per run) - NbOfEvent = 0; + fNbOfEvent = 0; // for stop on target uncertainty. As we reset the nb of events, we reset also // this variable - NbEventsNextCheck = NbEventsFirstCheck; + fNbEventsNextCheck = fNbEventsFirstCheck; // Important ! The volume may have moved, so we re-attach each run AttachImageToVolume(cpp_edep_image, fPhysicalVolumeName, @@ -103,7 +111,7 @@ void GateDoseActor::BeginOfRunActionMasterThread(int run_id) { } void GateDoseActor::PrepareLocalDataForRun(threadLocalT &data, - int numberOfVoxels) { + const unsigned int numberOfVoxels) { data.squared_worker_flatimg.resize(numberOfVoxels); std::fill(data.squared_worker_flatimg.begin(), data.squared_worker_flatimg.end(), 0.0); @@ -113,7 +121,7 @@ void GateDoseActor::PrepareLocalDataForRun(threadLocalT &data, } void GateDoseActor::BeginOfRunAction(const G4Run *run) { - int N_voxels = size_edep[0] * size_edep[1] * size_edep[2]; + const auto N_voxels = size_edep[0] * size_edep[1] * size_edep[2]; if (fEdepSquaredFlag) { PrepareLocalDataForRun(fThreadLocalDataEdep.Get(), N_voxels); } @@ -124,15 +132,50 @@ void GateDoseActor::BeginOfRunAction(const G4Run *run) { void GateDoseActor::BeginOfEventAction(const G4Event *event) { G4AutoLock mutex(&SetNbEventMutex); - NbOfEvent++; + fNbOfEvent++; +} + +void GateDoseActor::GetVoxelPosition(G4Step *step, G4ThreeVector &position, + bool &isInside, + Image3DType::IndexType &index) const { + auto preGlobal = step->GetPreStepPoint()->GetPosition(); + auto postGlobal = step->GetPostStepPoint()->GetPosition(); + auto touchable = step->GetPreStepPoint()->GetTouchable(); + + // consider random position between pre and post + if (fHitType == "pre") { + position = preGlobal; + } + if (fHitType == "post") { + position = postGlobal; + } + if (fHitType == "random") { + auto x = G4UniformRand(); + auto direction = postGlobal - preGlobal; + position = preGlobal + x * direction; + } + if (fHitType == "middle") { + auto direction = postGlobal - preGlobal; + position = preGlobal + 0.5 * direction; + } + + auto localPosition = + touchable->GetHistory()->GetTransform(0).TransformPoint(position); + + // convert G4ThreeVector to itk PointType + Image3DType::PointType point; + point[0] = localPosition[0]; + point[1] = localPosition[1]; + point[2] = localPosition[2]; + + isInside = cpp_edep_image->TransformPhysicalPointToIndex(point, index); } void GateDoseActor::SteppingAction(G4Step *step) { - auto event_id = + const auto event_id = G4RunManager::GetRunManager()->GetCurrentEvent()->GetEventID(); auto preGlobal = step->GetPreStepPoint()->GetPosition(); auto postGlobal = step->GetPostStepPoint()->GetPosition(); - auto touchable = step->GetPreStepPoint()->GetTouchable(); // FIXME If the volume has multiple copy, touchable->GetCopyNumber(0) ? @@ -140,13 +183,12 @@ void GateDoseActor::SteppingAction(G4Step *step) { G4ThreeVector position; bool isInside; Image3DType::IndexType index; - GetStepVoxelPosition(step, fHitType, cpp_edep_image, position, - isInside, index); + GetVoxelPosition(step, position, isInside, index); if (isInside) { // get edep in MeV (take weight into account) - auto w = step->GetTrack()->GetWeight(); + const auto w = step->GetTrack()->GetWeight(); auto edep = step->GetTotalEnergyDeposit() / CLHEP::MeV * w; double dose; @@ -176,17 +218,17 @@ void GateDoseActor::SteppingAction(G4Step *step) { if (fDoseFlag || fDoseSquaredFlag) { double density; if (fToWaterFlag) { - auto *water = + const auto *water = G4NistManager::Instance()->FindOrBuildMaterial("G4_WATER"); density = water->GetDensity(); } else { - auto *current_material = step->GetPreStepPoint()->GetMaterial(); + const auto *current_material = step->GetPreStepPoint()->GetMaterial(); density = current_material->GetDensity(); } dose = edep / density; } - // all ImageAddValue calls in a mutexed {}-scope + // all ImageAddValue calls in a mutex-scope { G4AutoLock mutex(&SetPixelMutex); ImageAddValue(cpp_edep_image, index, edep); @@ -213,31 +255,35 @@ void GateDoseActor::SteppingAction(G4Step *step) { } void GateDoseActor::EndOfEventAction(const G4Event *event) { - - // flush thread local data into global image (postponed for now) - - // if the user didn't set uncertainty goal, do nothing + // if the user didn't set an uncertainty goal, do nothing if (fUncertaintyGoal == 0) { return; } - // check if we reached the Nb of events for next evaluation - if (NbOfEvent >= NbEventsNextCheck) { - // get thread idx. Ideally, only one thread should do the uncertainty + // check if we reached the Nb of events for the next evaluation + if (fNbOfEvent >= fNbEventsNextCheck) { + // flush thread local data into the global image + // reset local data to zero is done in FlushSquaredValue + if (fEdepSquaredFlag) { + FlushSquaredValues(fThreadLocalDataEdep.Get(), cpp_edep_squared_image); + } + if (fDoseSquaredFlag) { + FlushSquaredValues(fThreadLocalDataDose.Get(), cpp_dose_squared_image); + } + + // Get thread idx. Ideally, only one thread should do the uncertainty // calculation don't ask for thread idx if no MT if (!G4Threading::IsMultithreadedApplication() || G4Threading::G4GetThreadId() == 0) { // check stop criteria - std::cout << "NbEventsNextCheck: " << NbEventsNextCheck << std::endl; double UncCurrent = ComputeMeanUncertainty(); if (UncCurrent <= fUncertaintyGoal) { - // fStopRunFlag = true; - fSourceManager->SetRunTerminationFlag(true); + GateSourceManager::SetRunTerminationFlag(true); } else { - // estimate Nevents at which next check should occour - NbEventsNextCheck = (UncCurrent / fUncertaintyGoal) * - (UncCurrent / fUncertaintyGoal) * NbOfEvent * - Overshoot; + // estimate nb of events at which the next check should occur + fNbEventsNextCheck = static_cast((UncCurrent / fUncertaintyGoal) * + (UncCurrent / fUncertaintyGoal) * + fNbOfEvent * fOvershoot); } } } @@ -250,7 +296,7 @@ double GateDoseActor::ComputeMeanUncertainty() { double mean_unc = 0.0; int n_voxel_unc = 0; double n = 2.0; - n = NbOfEvent; + n = fNbOfEvent; if (n < 2.0) { n = 2.0; @@ -265,7 +311,8 @@ double GateDoseActor::ComputeMeanUncertainty() { if (val > max_edep * fThreshEdepPerc) { val /= n; n_voxel_unc++; - double val_squared_mean = cpp_edep_squared_image->GetPixel(index_f) / n; + const double val_squared_mean = + cpp_edep_squared_image->GetPixel(index_f) / n; double unc_i = (1.0 / (n - 1.0)) * (val_squared_mean - pow(val, 2)); if (unc_i < 0) { @@ -290,7 +337,6 @@ double GateDoseActor::ComputeMeanUncertainty() { } else { mean_unc = 1.; } - std::cout << "unc: " << mean_unc << std::endl; return mean_unc; } @@ -312,41 +358,41 @@ void GateDoseActor::ind2sub(int index_flat, Image3DType::IndexType &index3D) { void GateDoseActor::EndOfRunAction(const G4Run *run) { // FlushSquaredValue() is thread-safe because it contains a mutex if (fEdepSquaredFlag) { - GateDoseActor::FlushSquaredValue(fThreadLocalDataEdep.Get(), - cpp_edep_squared_image); + GateDoseActor::FlushSquaredValues(fThreadLocalDataEdep.Get(), + cpp_edep_squared_image); } if (fDoseSquaredFlag) { - GateDoseActor::FlushSquaredValue(fThreadLocalDataDose.Get(), - cpp_dose_squared_image); + GateDoseActor::FlushSquaredValues(fThreadLocalDataDose.Get(), + cpp_dose_squared_image); } } void GateDoseActor::ScoreSquaredValue(threadLocalT &data, - Image3DType::Pointer cpp_image, - double value, int event_id, - Image3DType::IndexType index) { - int index_flat = sub2ind(index); - auto previous_id = data.lastid_worker_flatimg[index_flat]; + const Image3DType::Pointer &cpp_image, + const double value, const int event_id, + const Image3DType::IndexType &index) { + const int index_flat = sub2ind(index); + const auto previous_id = data.lastid_worker_flatimg[index_flat]; data.lastid_worker_flatimg[index_flat] = event_id; if (event_id == previous_id) { // Same event: sum the deposited value associated with this event ID // and square once a new event ID is found (case below) data.squared_worker_flatimg[index_flat] += value; } else { - // Different event : square deposited quantity from the last event ID + // Different event: square deposited quantity from the last event ID // and start accumulating deposited quantity for this new event ID auto v = data.squared_worker_flatimg[index_flat]; { G4AutoLock mutex(&SetPixelMutex); - ImageAddValue(cpp_image, index, v * v); + ImageAddValue(cpp_image, index, v * v); // implicit flush } // new temp value data.squared_worker_flatimg[index_flat] = value; } } -void GateDoseActor::FlushSquaredValue(threadLocalT &data, - Image3DType::Pointer cpp_image) { +void GateDoseActor::FlushSquaredValues(threadLocalT &data, + const Image3DType::Pointer &cpp_image) { G4AutoLock mutex(&SetPixelMutex); itk::ImageRegionIterator iterator3D( cpp_image, cpp_image->GetLargestPossibleRegion()); @@ -356,6 +402,9 @@ void GateDoseActor::FlushSquaredValue(threadLocalT &data, data.squared_worker_flatimg[sub2ind(index_f)]; ImageAddValue(cpp_image, index_f, pixelValue3D * pixelValue3D); } + // reset thread local data to zero + const auto N_voxels = size_edep[0] * size_edep[1] * size_edep[2]; + PrepareLocalDataForRun(data, N_voxels); } int GateDoseActor::EndOfRunActionMasterThread(int run_id) { return 0; } diff --git a/core/opengate_core/opengate_lib/GateTLEDoseActor.cpp b/core/opengate_core/opengate_lib/GateTLEDoseActor.cpp index f2b06c111..09b66dc9a 100644 --- a/core/opengate_core/opengate_lib/GateTLEDoseActor.cpp +++ b/core/opengate_core/opengate_lib/GateTLEDoseActor.cpp @@ -5,118 +5,235 @@ See LICENSE.md for further details ------------------------------------ -------------- */ +#include "GateTLEDoseActor.h" +#include "G4Electron.hh" #include "G4EmCalculator.hh" +#include "G4Exception.hh" #include "G4ParticleDefinition.hh" #include "G4RandomTools.hh" #include "G4RunManager.hh" #include "G4Threading.hh" - #include "GateHelpers.h" #include "GateHelpersDict.h" #include "GateHelpersImage.h" #include "GateMaterialMuHandler.h" -#include "GateTLEDoseActor.h" +#include "GateUserTrackInformation.h" #include #include -#include #include G4Mutex SetPixelTLEMutex = G4MUTEX_INITIALIZER; +G4Mutex SetEkinMaxMutex = G4MUTEX_INITIALIZER; GateTLEDoseActor::GateTLEDoseActor(py::dict &user_info) : GateDoseActor(user_info) { fMultiThreadReady = true; + fEnergyMin = 0; + fTLEThreshold = 0; } void GateTLEDoseActor::InitializeUserInfo(py::dict &user_info) { GateDoseActor::InitializeUserInfo(user_info); + fStrTLEThresholdType = py::cast(user_info["tle_threshold_type"]); + fTLEThreshold = py::cast(user_info["tle_threshold"]); fEnergyMin = py::cast(user_info["energy_min"]); - fEnergyMax = py::cast(user_info["energy_max"]); - auto database = py::cast(user_info["database"]); - fMaterialMuHandler = GateMaterialMuHandler::GetInstance(database, fEnergyMax); + fDatabase = py::cast(user_info["database"]); + if (fStrTLEThresholdType == "max range") { + fTLEThresholdType = 0; + } else if (fStrTLEThresholdType == "average range") { + fTLEThresholdType = 1; + } else if (fStrTLEThresholdType == "energy") { + fTLEThresholdType = 2; + } else if (fStrTLEThresholdType == "None") { + fTLEThresholdType = 3; + } else { + G4ExceptionDescription ed; + ed << "You did not define a correct threshold type" << G4endl; + G4Exception("GateTLEDoseActor::InitializeUserInfo", "TLE.LV1", + FatalException, ed); + } +} + +void GateTLEDoseActor::SetTLETrackInformationOnSecondaries(G4Step *step, + G4bool info, + G4int nbSec) { + if (nbSec > 0) { + for (auto i = 0; i < nbSec; i++) { + GateUserTrackInformation *trackInfo = nullptr; + auto *secs = step->GetfSecondary(); + auto *sec = (*secs)[secs->size() - i - 1]; + if (sec->GetUserInformation() == 0) { + trackInfo = new GateUserTrackInformation(); + } else { + trackInfo = + dynamic_cast(sec->GetUserInformation()); + } + trackInfo->SetGateTrackInformation(this, info); + sec->SetUserInformation(trackInfo); + } + } +} + +G4double GateTLEDoseActor::FindEkinMaxForTLE() { + G4MaterialTable *matTable = G4Material::GetMaterialTable(); + G4int nbOfMaterials = G4Material::GetNumberOfMaterials(); + G4double ekinMax = 0; + for (G4int i = 0; i < nbOfMaterials; i++) { + G4Material *currentMat = (*matTable)[i]; + G4double ekin = fEmCalc->GetKinEnergy(fTLEThreshold, + G4Electron::Definition(), currentMat); + if (i == 0) { + ekinMax = ekin; + } + if (ekin > ekinMax) { + ekinMax = ekin; + } + } + return ekinMax; +} + +void GateTLEDoseActor::InitializeCSDAForNewGamma(G4bool isFirstStep, + G4Step *step) { + if ((isFirstStep) && + (step->GetTrack()->GetDefinition()->GetParticleName() == "gamma")) { + auto &l = fThreadLocalData.Get(); + G4StepPoint *pre_step = step->GetPreStepPoint(); + G4double energy = pre_step->GetKineticEnergy(); + const G4Material *currentMat = pre_step->GetMaterial(); + l.fCsda = + fEmCalc->GetCSDARange(energy, G4Electron::Definition(), currentMat); + l.fPreviousMatName = currentMat->GetName(); + } } void GateTLEDoseActor::BeginOfEventAction(const G4Event *event) { + // EM calc does not work at the beginning of the simulation + { + G4AutoLock mutex(&SetEkinMaxMutex); + if (fEmCalc == 0) { + G4double ekinMax = 0; + fEmCalc = new G4EmCalculator; + if ((fTLEThresholdType == 0) || (fTLEThresholdType == 1)) { + if (fTLEThreshold != std::numeric_limits::infinity()) { + ekinMax = FindEkinMaxForTLE(); + } else { + ekinMax = 50 * CLHEP::MeV; + } + } else if (fTLEThresholdType == 2) { + ekinMax = fTLEThreshold; + } + if (fTLEThresholdType <= 2) + fMaterialMuHandler = + GateMaterialMuHandler::GetInstance(fDatabase, ekinMax); + else { + fMaterialMuHandler = + GateMaterialMuHandler::GetInstance(fDatabase, 5 * CLHEP::MeV); + } + } + } + auto &l = fThreadLocalData.Get(); l.fIsTLESecondary = false; - l.fSecNbWhichDeposit.clear(); + l.fSecWhichDeposit.clear(); GateDoseActor::BeginOfEventAction(event); } void GateTLEDoseActor::PreUserTrackingAction(const G4Track *track) { + G4Event *event = G4EventManager::GetEventManager()->GetNonconstCurrentEvent(); auto &l = fThreadLocalData.Get(); - - // if the particle is a gamma, we associate a map which will associate the - // gamma TID with the number of secondaries created when the particle is in - // TLE mode - - if (track->GetDefinition()->GetParticleName() == "gamma") { + l.fIsFirstStep = true; + // If the particle is a gamma, the TLE is initiated as false. The TLE + // application will be decided in the stepping action depending on the + // secondary csda range with the gamma energy within the current volume. + if ((track->GetDefinition()->GetParticleName() == "gamma") || + (track->GetParentID() == 0)) { l.fIsTLEGamma = false; l.fIsTLESecondary = false; - l.fSecNbWhichDeposit[track->GetTrackID()] = 0; } - - // if the particle is not a gamma, we want to associate a secondary boolean to - // allow or not the energy deposition. - //- If it's a direct secondary of the gamma with the PID inside, which have to - // not deposit its energy, the boolean is set to false - //- If it's a direct secondary, but the number of remaining secondaries to - // track is 0, it means that this secondary was not created by a TLE gamma, - // and - // the boolean is set to false - //- If it's an indirect secondary, created by a secondary generated by a - // gamma, we do nothing, since the boolean was already fixed. - // If it's a primary or a secondary generated from a primary which is not a - // gamma, the boolean is set to False at the beginning of the event + // if the particle is not a gamma, we want to associate a secondary boolean + // to allow or not the energy deposition. + //- If it's a direct secondary, the boolean will be applied according to the + // tracking information + // defined in the stepping action according to the csda conditions. + //- If it's a secondary of a secondary (but not a gamma or not from a gamma + // secondary), we do nothing, + // since we already fixed the wyto deposited dose for the mother secondary + // (electrons or positron). else { - auto parent_id = track->GetParentID(); - // std::cout<<"begin"<first<<" "<second<first) { - if (it->second > 0) { - l.fIsTLESecondary = true; - } - if (it->second == 0) { - l.fSecNbWhichDeposit.erase(it); - l.fIsTLESecondary = false; - break; - } - it->second--; - } + if (track->GetUserInformation() != 0) { + auto *info = track->GetUserInformation(); + GateUserTrackInformation *trackInfo = + dynamic_cast(info); + l.fIsTLESecondary = trackInfo->GetGateTrackInformation(this); } - // std::cout<<"end"<GetPreStepPoint(); - auto pre_step = step->GetPreStepPoint(); double energy = 0; - if (pre_step != 0) - energy = pre_step->GetKineticEnergy(); + energy = pre_step->GetKineticEnergy(); + const G4Material *currentMat = pre_step->GetMaterial(); + auto nbSec = step->GetSecondaryInCurrentStep()->size(); + + if (fTLEThresholdType <= 1) + InitializeCSDAForNewGamma(l.fIsFirstStep, step); + + G4double mu_en_over_rho; + G4double mu_over_rho; + + // At the secondary creation, the TLE status is by default defined as the TLE + // status of the mother particle Then, if the TLE status has to be changed, it + // will be modified afterward according to the CSDA range condition. + + SetTLETrackInformationOnSecondaries(step, l.fIsTLESecondary, nbSec); + if (step->GetTrack()->GetDefinition()->GetParticleName() == "gamma") { + G4bool tleCondition = false; + if ((fTLEThresholdType <= 2)) { + if ((fTLEThresholdType == 0) || (fTLEThresholdType == 1)) { + auto sec_ekin = energy; + if (fTLEThresholdType == 1) { + mu_en_over_rho = fMaterialMuHandler->GetMuEnOverRho( + pre_step->GetMaterialCutsCouple(), energy); + } + + if ((energy != l.fPreviousEnergy) || + ((pre_step->GetStepStatus() == 1) && + (currentMat->GetName() != l.fPreviousMatName))) { + if (fTLEThresholdType == 1) { + mu_over_rho = fMaterialMuHandler->GetMuOverRho( + pre_step->GetMaterialCutsCouple(), energy); + sec_ekin = energy * mu_en_over_rho / mu_over_rho; + } + l.fCsda = fEmCalc->GetCSDARange(sec_ekin, G4Electron::Definition(), + currentMat); + l.fPreviousMatName = currentMat->GetName(); + l.fPreviousEnergy = energy; + } + tleCondition = l.fCsda / CLHEP::mm <= fTLEThreshold; + } - // For too high energy, no TLE - if (energy > fEnergyMax) { - l.fIsTLEGamma = false; - l.fIsTLESecondary = false; - return GateDoseActor::SteppingAction(step); + else if (fTLEThresholdType == 2) { + tleCondition = energy / CLHEP::MeV <= fTLEThreshold / CLHEP::MeV; + } + // std::cout<GetSecondaryInCurrentStep()->size(); - if (nbSec > 0) { - l.fIsTLESecondary = true; - l.fSecNbWhichDeposit[step->GetTrack()->GetTrackID()] += nbSec; - } - // l.fLastTrackId += step->GetSecondaryInCurrentStep()->size(); + l.fIsTLESecondary = true; } } @@ -127,30 +244,32 @@ void GateTLEDoseActor::SteppingAction(G4Step *step) { } return GateDoseActor::SteppingAction(step); } - + // std::cout<<"TLE"<GetTrack()->GetWeight(); auto step_length = step->GetStepLength(); auto density = pre_step->GetMaterial()->GetDensity(); - auto mu_en_over_rho = fMaterialMuHandler->GetMuEnOverRho( - pre_step->GetMaterialCutsCouple(), energy); + if (fTLEThresholdType != 1) { + mu_en_over_rho = fMaterialMuHandler->GetMuEnOverRho( + pre_step->GetMaterialCutsCouple(), energy); + } // (0.1 because length is in mm -> cm) + auto edep = weight * 0.1 * energy * mu_en_over_rho * step_length * density / (CLHEP::g / CLHEP::cm3); // Kill photon below a given energy if (energy <= fEnergyMin) { - edep = energy; + edep = weight * energy; step->GetTrack()->SetTrackStatus(fStopAndKill); } - double dose = edep / density; + const double dose = edep / density; // Get the voxel index and check if the step was within the 3D image G4ThreeVector position; bool isInside; Image3DType::IndexType index; - GetStepVoxelPosition(step, fHitType, cpp_edep_image, position, - isInside, index); - auto event_id = + GetVoxelPosition(step, position, isInside, index); + const auto event_id = G4RunManager::GetRunManager()->GetCurrentEvent()->GetEventID(); if (isInside) { G4AutoLock mutex(&SetPixelTLEMutex); // mutex is bound to the if-scope diff --git a/opengate/actors/doseactors.py b/opengate/actors/doseactors.py index 3173c11f7..6d8c78c35 100644 --- a/opengate/actors/doseactors.py +++ b/opengate/actors/doseactors.py @@ -1,18 +1,21 @@ import numpy as np +import pandas as pd +import os from scipy.spatial.transform import Rotation +from pathlib import Path import opengate_core as g4 from .base import ActorBase -from ..exception import fatal, warning -from ..utility import ( - g4_units, - standard_error_c4_correction, -) +from ..exception import fatal +from ..utility import g4_units from ..image import ( update_image_py_to_cpp, get_py_image_from_cpp_image, images_have_same_domain, resample_itk_image_like, + itk_image_from_array, + divide_itk_images, + scale_itk_image, ) from ..geometry.utility import get_transform_world_to_local from ..base import process_cls @@ -23,6 +26,9 @@ ActorOutputSingleImageWithVariance, UserInterfaceToActorOutputImage, ) +from .dataitems import ( + ItkImageDataItem, +) class VoxelDepositActor(ActorBase): @@ -280,44 +286,6 @@ def EndSimulationAction(self): u.end_of_simulation() -def compute_std_from_sample( - number_of_samples, value_array, squared_value_array, correct_bias=False -): - unc = np.ones_like(value_array) - if number_of_samples > 1: - # unc = np.sqrt(1 / (N - 1) * (square / N - np.power(edep / N, 2))) - unc = np.sqrt( - np.clip( - ( - squared_value_array / number_of_samples - - np.power(value_array / number_of_samples, 2) - ) - / (number_of_samples - 1), - 0, - None, - ) - ) - if correct_bias: - # Standard error is biased (to underestimate the error); - # this option allows to correct for the bias - assuming normal distribution. - # For few N this in is huge, but for N>8 the difference is minimal - unc /= standard_error_c4_correction(number_of_samples) - unc = np.divide( - unc, - value_array / number_of_samples, - out=np.ones_like(unc), - where=value_array != 0, - ) - - else: - # unc += 1 # we init with 1. - warning( - "You try to compute statistical errors with only one or zero event! " - "The uncertainty value for all voxels has been fixed at 1" - ) - return unc - - def _setter_hook_ste_of_mean_unbiased(self, value): if value is True: self.ste_of_mean = True @@ -711,6 +679,60 @@ def EndSimulationAction(self): VoxelDepositActor.EndSimulationAction(self) +class TLEDoseActor(DoseActor, g4.GateTLEDoseActor): + """TLE = Track Length Estimator""" + + energy_min: float + range_type: str + max_range: float + database: str + + user_info_defaults = { + "energy_min": ( + 0.0, + {"doc": "Kill the gamma if below this energy"}, + ), + "tle_threshold": ( + np.inf, + { + "doc": "Define a criterium to enable TLE or not. It can be in terms of gamma energy or in secondary particle range depending on the provided tle_threshold_type" + }, + ), + "tle_threshold_type": ( + "None", + { + "doc": "Define the type of range lim provided to hTLE. It could be applied without threshold (None), by energy (energy), or by the range of an electron with the full gamma energy (max range) or the average transfered energy (average range)." + }, + ), + "database": ( + "EPDL", + { + "doc": "which database to use", + "allowed_values": ("EPDL", "NIST"), # "simulated" does not work + }, + ), + } + + def __initcpp__(self): + g4.GateTLEDoseActor.__init__(self, self.user_info) + self.AddActions( + { + "BeginOfRunAction", + "BeginOfEventAction", + "SteppingAction", + "PreUserTrackingAction", + "EndOfRunAction", + } + ) + + def initialize(self, *args): + if self.score_in != "material": + fatal( + f"TLEDoseActor cannot score in {self.score_in}, only 'material' is allowed." + ) + super().initialize(args) + + def _setter_hook_score_in_let_actor(self, value): if value.lower() in ("g4_water", "g4water"): """Assuming a misspelling of G4_WATER and correcting it to correct spelling; Note that this is rather dangerous operation.""" @@ -841,6 +863,672 @@ def EndSimulationAction(self): VoxelDepositActor.EndSimulationAction(self) +def _setter_hook_lookup_table_path(self, path): + if not os.path.exists(path): + raise ValueError(f"Lookup table path {path} does not exist.") + return path + + +class BeamQualityActor(VoxelDepositActor, g4.GateBeamQualityActor): + """ + BeamQualityActor: + Handles the logic for RE, RBE and potentially other actors + + """ + + user_info_defaults = { + "energy_per_nucleon": ( + False, + { + "doc": "The kinetic energy in the table is the energy per nucleon MeV/n, else False.", + }, + ), + "model": ( + "RE", + { + "doc": "which model is used to calculate beam quality.", + "allowed_values": ("mMKM", "LEM1lda", "RE"), + }, + ), + "score_in": ( + "material", + { + "doc": """The score_in command allows to convert the scored quantity from the material, which is defined in the geometry, to any user defined material. Note, that this does not change the material definition in the geometry. The default value is 'material', which means that no conversion is performed and the quantity to the local material is scored. You can use any material defined in the simulation or pre-defined by Geant4 such as 'G4_WATER', which may be one of the most use cases of this functionality. + """, + }, + ), + "lookup_table_path": ( + "", + { + "doc": "path of the z*_1d or alpha_z table.", + "setter_hook": _setter_hook_lookup_table_path, + }, + ), + "lookup_table": ( + None, + { + "doc": "z*_1d or alpha_z table. Read by the actor.", + }, + ), + } + scored_quantity = "alpha" + user_output_config = { + f"{scored_quantity}_mix": { + "actor_output_class": ActorOutputQuotientMeanImage, + "interfaces": { + f"{scored_quantity}_numerator": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 0, + "active": True, + "write_to_disk": False, + }, + f"{scored_quantity}_denominator": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 1, + "active": True, + "write_to_disk": False, + }, + f"{scored_quantity}_mix": { + "interface_class": UserInterfaceToActorOutputImage, + "item": "quotient", + "write_to_disk": True, + "active": True, + # "suffix": None, # default suffix would be 'alpha_mix', but we prefer no suffix + }, + }, + }, + "beta_mix": { + "actor_output_class": ActorOutputQuotientMeanImage, + "interfaces": { + "beta_numerator": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 0, + "active": False, + "write_to_disk": False, + }, + "beta_denominator": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 1, + "active": False, + "write_to_disk": False, + }, + "beta_mix": { + "interface_class": UserInterfaceToActorOutputImage, + "item": "quotient", + "write_to_disk": True, + "active": False, + # "suffix": None, # default suffix would be 'beta_mix', but we prefer no suffix + }, + }, + }, + } + + def __init__(self, *args, **kwargs): + # Init parent + VoxelDepositActor.__init__(self, *args, **kwargs) + # calculate some internal variables + self.element_to_atomic_number_dict = { + "H": 1, + "He": 2, + "Li": 3, + "Be": 4, + "B": 5, + "C": 6, + "N": 7, + "O": 8, + "F": 9, + "Ne": 10, + } + self.atomic_number_to_mass_dict = { + 1: 1, + 2: 4, + 3: 6.9, + 4: 9, + 5: 10.8, + 6: 12, + 7: 14, + 8: 16, + 9: 19, + 10: 20.2, + } + self.lookup_table = None + self._extend_table_to_zero_and_inft = True + self.multiple_scoring = False + self.__initcpp__() + + def __initcpp__(self): + g4.GateBeamQualityActor.__init__(self, self.user_info) + self.AddActions( + { + "BeginOfRunActionMasterThread", + "EndOfRunActionMasterThread", + "BeginOfEventAction", + } + ) + + def initialize(self): + """ + At the start of the run, the image is centered according to the coordinate system of + the attached volume. This function computes the correct origin = center + translation. + Note that there is a half-pixel shift to align according to the center of the pixel, + like in ITK. + """ + + VoxelDepositActor.initialize(self) + + self.check_user_input() + + if self.lookup_table_path: + if not Path(self.lookup_table_path).is_file(): + raise ValueError( + f"Lookuptable path does not exist or is no file: {self.lookup_table_path}" + ) + + self.read_lookup_table(self.lookup_table_path) + + if self.lookup_table is None: + raise ValueError( + "Missing lookup table. Set it manually with the lookup_table attribute or provide a table path to the lookup_table_path attribute." + ) + + if self.multiple_scoring: + self.user_output.beta_mix.set_active(True, item="all") + self.user_output.beta_mix.set_write_to_disk(True, item="quotient") + + self.InitializeUserInfo(self.user_info) + # Set the physical volume name on the C++ side + self.SetPhysicalVolumeName(self.get_physical_volume_name()) + self.InitializeCpp() + + def read_lookup_table_txt(self, table_path): + # Element-Z mapping + + with open(table_path, "r") as f: + lines = f.readlines() + # add extra line to sign end of file + lines.append("\n") + start_table = False + end_table = True + fragments = [] + v_table = [] + for line in lines: + if "Fragment" in line: + element = line.split()[1] + if element in self.element_to_atomic_number_dict: + Z = self.element_to_atomic_number_dict[element] + else: + raise ValueError( + f"Lookup table inconsistency: cannot convert element name to atomic number: {element}" + ) + values = [] + energy = [] + v_table.append([Z]) + fragments.append(Z) + start_table = True + end_table = False + elif line.startswith("\n") == False and start_table: + # if count == 1: # energy vector is the same for all Z + energy.append(float(line.split()[0])) + values.append(float(line.split()[1])) + elif not end_table: + start_table = False + # if count == 1: + # e_table.append(energy) + v_table.append(energy) + v_table.append(values) # we want to do this only once per table + end_table = True + return v_table, fragments + + def read_lookup_table_csv(self, table_path): + dataset = pd.read_csv(table_path) + particles = dataset["particle"].unique() + fragments = [] + v_table = [] + for p in particles: + if p in self.element_to_atomic_number_dict: + Z = self.element_to_atomic_number_dict[p] + else: + raise ValueError( + f"Lookup table inconsistency: cannot convert element name to atomic number: {p}" + ) + fragments.append(Z) + v_table.append([Z]) + energies_p = dataset.loc[dataset["particle"] == p, "meanEnergy"] + v_table.append(list(energies_p)) + values_p = dataset.loc[dataset["particle"] == p, "z1D*"] + v_table.append(list(values_p)) + return v_table, fragments + + def read_lookup_table(self, table_path): + p = Path(table_path) + if p.suffix == ".txt": + v_table, fragments = self.read_lookup_table_txt(table_path) + elif p.suffix == ".csv": + v_table, fragments = self.read_lookup_table_csv(table_path) + else: + raise NotImplementedError( + "Cannot read lookup table. File type reading not implemented yet." + ) + + self.set_lookup_table(v_table, fragments) + + def set_lookup_table(self, v_table: list, fragments: list): + element_map = self.element_to_atomic_number_dict + # convert fragments strings to atomic number if they are + fragments = [ + element_map[element] if element in element_map else element + for element in fragments + ] + # Check if all values in `a` are positive + if all(isinstance(x, (int, float)) and x > 0 for x in fragments): + # Convert all elements in `a` to integers + fragments = [ + int(x) if isinstance(x, (int, float)) else x for x in fragments + ] + else: + raise ValueError("Non numeric entries in table") + self.check_table(v_table, fragments) + # get max and min atomic numbers available + self.ZMinTable = min(fragments) + self.ZMaxTable = max(fragments) + if len(v_table) != 3 * len(fragments): + raise ValueError( + f"Error: {len(v_table) = } and {len(fragments) = } are not equal." + ) + if self.energy_per_nucleon: + for i in range(1, len(v_table), 3): + n_nuclei = self.atomic_number_to_mass_dict[ + v_table[i - 1][0] + ] # v_table[i - 1][0] * (1 + float(v_table[i - 1][0] != 1)) + v_table[i] = [x * n_nuclei for x in v_table[i]] + if self._extend_table_to_zero_and_inft: + for i in range(1, len(v_table), 3): + energy_0 = v_table[i][0] + energy_max = v_table[i][-1] + if energy_0 > 0: + v_table[i].insert(0, 0.0) + v_table[i + 1].insert(0, v_table[i + 1][0]) + if energy_max < np.finfo(np.float32).max: + # we use a single precision float here to enable on the cpp side the table to be a float instead of a double for more efficient cache use --> future improvmement + v_table[i].append(np.finfo(np.float32).max) + v_table[i + 1].append(v_table[i + 1][-1]) + + self.lookup_table = v_table + + def check_table(self, v_table, fragments): + if not fragments: + raise ValueError("Fragment table is empty") + sorted_fragments = sorted(fragments) + if not sorted_fragments == list( + range(sorted_fragments[0], sorted_fragments[0] + len(sorted_fragments)) + ): + raise ValueError("Fragment list is missing entries") + + def BeginOfRunActionMasterThread(self, run_index): + + self.prepare_output_for_run(f"{self.scored_quantity}_mix", run_index) + self.push_to_cpp_image( + f"{self.scored_quantity}_mix", + run_index, + self.cpp_numerator_alpha_image, + self.cpp_denominator_image, + ) + + if self.multiple_scoring: + self.prepare_output_for_run("beta_mix", run_index) + self.push_to_cpp_image( + "beta_mix", + run_index, + self.cpp_numerator_beta_image, + self.cpp_denominator_image, + ) + + g4.GateBeamQualityActor.BeginOfRunActionMasterThread(self, run_index) + + def EndOfRunActionMasterThread(self, run_index): + self.fetch_from_cpp_image( + f"{self.scored_quantity}_mix", + run_index, + self.cpp_numerator_image, + self.cpp_denominator_image, + ) + self._update_output_coordinate_system(f"{self.scored_quantity}_mix", run_index) + self.user_output.__getattr__(f"{self.scored_quantity}_mix").store_meta_data( + run_index, number_of_samples=self.NbOfEvent + ) + if self.multiple_scoring: + self.fetch_from_cpp_image( + "beta_mix", + run_index, + self.cpp_numerator_beta_image, + self.cpp_denominator_image, + ) + self._update_output_coordinate_system("beta_mix", run_index) + self.user_output.beta_mix.store_meta_data( + run_index, number_of_samples=self.NbOfEvent + ) + + VoxelDepositActor.EndOfRunActionMasterThread(self, run_index) + return 0 + + def EndSimulationAction(self): + g4.GateBeamQualityActor.EndSimulationAction(self) + VoxelDepositActor.EndSimulationAction(self) + + def compute_dose_from_edep_img(self, overrides=dict()): + """ + * cretae mass image: + - from ct HU units, if dose actor attached to ImageVolume. + - from material density, if standard volume + * compute dose as edep_image / mass_image + """ + vol_name = self.user_info.attached_to + vol = self.simulation.volume_manager.get_volume(vol_name) + vol_type = vol.volume_type + + spacing = np.array(self.user_info.spacing) + voxel_volume = spacing[0] * spacing[1] * spacing[2] + Gy = g4_units.Gy + + edep_img = ( + self.user_output.__getattr__(f"{self.scored_quantity}_mix") + .merged_data.data[1] + .image + ) + + score_in_material = self.user_info.score_in + + material_database = ( + self.simulation.volume_manager.material_database.g4_materials + ) + + if score_in_material != "material": + # score in other material + if score_in_material not in material_database: + self.simulation.volume_manager.material_database.FindOrBuildMaterial( + score_in_material + ) + density = material_database[score_in_material].GetDensity() + dose_img = scale_itk_image(edep_img, 1 / (voxel_volume * density * Gy)) + else: + # divide edep image with the original mass image + if vol_type == "ImageVolume": + density_img = vol.create_density_image() + edep_density = divide_itk_images( + img1_numerator=edep_img, + img2_denominator=density_img, + filterVal=0, + replaceFilteredVal=0, + ) + # divide by voxel volume and convert unit. + dose_img = scale_itk_image( + edep_density, g4_units.cm3 / (Gy * voxel_volume * g4_units.g) + ) + else: + if vol.material not in material_database: + self.simulation.volume_manager.material_database.FindOrBuildMaterial( + vol.material + ) + density = material_database[vol.material].GetDensity() + dose_img = scale_itk_image(edep_img, 1 / (voxel_volume * density * Gy)) + + dose_image = ItkImageDataItem(data=dose_img) + dose_image.copy_image_properties(edep_img) + + return dose_image + + +class REActor(BeamQualityActor, g4.GateBeamQualityActor): + scored_quantity = "RE" + user_output_config = { + f"{scored_quantity}_mix": { + "actor_output_class": ActorOutputQuotientMeanImage, + "interfaces": { + f"{scored_quantity}_numerator": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 0, + "active": True, + "write_to_disk": False, + }, + f"{scored_quantity}_denominator": { + "interface_class": UserInterfaceToActorOutputImage, + "item": 1, + "active": True, + "write_to_disk": False, + }, + f"{scored_quantity}_mix": { + "interface_class": UserInterfaceToActorOutputImage, + "item": "quotient", + "write_to_disk": True, + "active": True, + # "suffix": None, # default suffix would be 'alpha_mix', but we prefer no suffix + }, + }, + }, + } + + +class RBEActor(BeamQualityActor, g4.GateBeamQualityActor): + """ + RBEActor: + - mMKM: + - alpha mix (score separately numerator and denominator) + - LEM1lda: + - alpha mix (score separately numerator and denominator) + - beta mix (score separately numerator and denominator) + + Note: all will also score edep as denominator image -> retrieve dose + """ + + user_info_defaults = { + "alpha_0": ( + 0.172, + { + "doc": "mMKM specific fitted parameter, for the calculation of cellline alpha_z per radiation. Unit: Gy-1", + }, + ), + "beta_ref": ( + None, + { + "doc": "Cellline specific parameter. Initialized according to cell_type.", + }, + ), + "F_clin": ( + 2.41, + { + "doc": "mMKM specific arbituary parameter, the clinical sclaing factor.", + }, + ), + "D_cut": ( + 30.0, + { + "doc": "LEM specific arbituary parameter, the physical dose threshold between linear quadratic and linear cell survival. Unit: Gy.", + }, + ), + "r_nucleus": ( + 5, + { + "doc": "nucleus's radius, in um. Used when rbe_model is LEM", + }, + ), + "cell_type": ( + "HSG", + { + "doc": "To add new cell types, update the self.cells_radiosensitivity" + "variable in the init of the actor", + "allowed_values": ("HSG", "Chordoma"), + }, + ), + "write_RBE_dose_image": ( + True, + { + "doc": "Do you want to calcu;ate and write to disk RBE and RBE dose images?", + }, + ), + } + + user_output_config = BeamQualityActor.user_output_config.copy() + user_output_config.update( + { + "rbe": { + "actor_output_class": ActorOutputSingleMeanImage, + "active": False, + }, + "rbe_dose": { + "actor_output_class": ActorOutputSingleMeanImage, + "active": False, + }, + } + ) + + def __init__(self, *args, **kwargs): + # Init parent + BeamQualityActor.__init__(self, *args, **kwargs) + + # internal variables + self.cells_radiosensitivity = { + "HSG": {"alpha_ref": 0.764, "beta_ref": 0.0615}, + "Chordoma": {"alpha_ref": 0.1, "beta_ref": 0.05}, + } + + self.s_max = None + self.lnS_cut = None + + self.__initcpp__() + + def initialize(self): + """ + At the start of the run, the image is centered according to the coordinate system of + the attached volume. This function computes the correct origin = center + translation. + Note that there is a half-pixel shift to align according to the center of the pixel, + like in ITK. + """ + VoxelDepositActor.initialize(self) + + self.check_user_input() + + if self.lookup_table_path: + self.read_lookup_table(self.lookup_table_path) + + if self.lookup_table is None: + raise ValueError( + "Missing lookup table. Set it manually with the lookup_table attribute or provide a table path to the lookup_table_path attribute." + ) + + # calculate some internal variables + self.fAreaNucl = (np.pi * self.r_nucleus**2) * g4_units.um * g4_units.um + alpha_ref = self.cells_radiosensitivity[self.cell_type]["alpha_ref"] + beta_ref = self.cells_radiosensitivity[self.cell_type]["beta_ref"] + self.beta_ref = beta_ref + self.s_max = alpha_ref + 2 * beta_ref * self.D_cut + self.fSmax = self.s_max # set also to cpp + self.lnS_cut = -beta_ref * self.D_cut**2 - alpha_ref * self.D_cut + + if self.model == "LEM1lda": + self.multiple_scoring = True + self.user_output.beta_mix.set_active(True, item="all") + self.user_output.beta_mix.set_write_to_disk(True, item="quotient") + + self.InitializeUserInfo(self.user_info) + # Set the physical volume name on the C++ side + self.SetPhysicalVolumeName(self.get_physical_volume_name()) + self.InitializeCpp() + + def EndSimulationAction(self): + g4.GateBeamQualityActor.EndSimulationAction(self) + if self.model == "mMKM": + self._postprocess_alpha_numerator_mkm() + if self.write_RBE_dose_image: + self.compute_rbe_weighted_dose() + VoxelDepositActor.EndSimulationAction(self) + + def _postprocess_alpha_numerator_mkm(self): + beta_ref = self.cells_radiosensitivity[self.cell_type]["beta_ref"] + alpha_mix_numerator_img = self.user_output.__getattr__( + f"{self.scored_quantity}_mix" + ).merged_data.data[0] + alpha_mix_denominator_img = ( + self.user_output.__getattr__( + f"{self.scored_quantity}_mix" + ).merged_data.data[1] + * self.alpha_0 + ) + self.user_output.__getattr__(f"{self.scored_quantity}_mix").merged_data.data[ + 0 + ] = (alpha_mix_numerator_img * beta_ref + alpha_mix_denominator_img) + + def compute_rbe_weighted_dose(self): + alpha_ref = self.cells_radiosensitivity[self.cell_type]["alpha_ref"] + beta_ref = self.cells_radiosensitivity[self.cell_type]["beta_ref"] + dose_img = self.compute_dose_from_edep_img() + + if self.model == "mMKM": + alpha_mix_img = self.user_output.__getattr__( + f"{self.scored_quantity}_mix" + ).merged_data.quotient + log_survival = alpha_mix_img * dose_img * ( + -1 + ) + dose_img * dose_img * beta_ref * (-1) + log_survival_arr = log_survival.image_array + elif self.model == "LEM1lda": + dose_arr = dose_img.image_array + arr_mask_linear = dose_arr > self.D_cut + alpha_mix_img = self.user_output.__getattr__( + f"{self.scored_quantity}_mix" + ).merged_data.quotient + sqrt_beta_mix_img = self.user_output.beta_mix.merged_data.quotient + + log_survival_lq = alpha_mix_img * dose_img * ( + -1 + ) + dose_img * dose_img * sqrt_beta_mix_img * sqrt_beta_mix_img * (-1) + log_survival_lq_arr = log_survival_lq.image_array + log_survival_linear = ( + alpha_mix_img * self.D_cut * (-1) + + sqrt_beta_mix_img * sqrt_beta_mix_img * self.D_cut * self.D_cut * (-1) + + (dose_img + self.D_cut * (-1)) * self.s_max * (-1) + ) + log_survival_linear_arr = log_survival_linear.image_array + + log_survival_arr = np.zeros(log_survival_lq.image_array.shape) + log_survival_arr[arr_mask_linear] = log_survival_linear_arr[arr_mask_linear] + log_survival_arr[~arr_mask_linear] = log_survival_lq_arr[~arr_mask_linear] + + # solve linear quadratic equation to get Dx + if self.model == "mMKM": + rbe_dose_arr = ( + (-alpha_ref + np.sqrt(alpha_ref**2 - 4 * beta_ref * log_survival_arr)) + / (2 * beta_ref) + * self.F_clin + ) + else: + arr_mask_linear = log_survival_arr < self.lnS_cut + rbe_dose_lq_arr = ( + -alpha_ref + np.sqrt(alpha_ref**2 - 4 * beta_ref * log_survival_arr) + ) / (2 * beta_ref) + rbe_dose_linear_arr = ( + -log_survival_arr + self.lnS_cut + ) / self.s_max + self.D_cut + rbe_dose_arr = np.zeros(log_survival_arr.shape) + rbe_dose_arr[arr_mask_linear] = rbe_dose_linear_arr[arr_mask_linear] + rbe_dose_arr[~arr_mask_linear] = rbe_dose_lq_arr[~arr_mask_linear] + + # create new data item for the survival image. Same metadata as the other images, but new image array + rbedose_img = itk_image_from_array(rbe_dose_arr) + rbe_weighted_dose_image = ItkImageDataItem(data=rbedose_img) + rbe_weighted_dose_image.copy_image_properties(dose_img.image) + + rbe_image = rbe_weighted_dose_image / dose_img + + self.user_output.rbe.merged_data = rbe_image + rbe_path = self.user_output.rbe.get_output_path() + + self.user_output.rbe_dose.merged_data = rbe_weighted_dose_image + rbe_dose_path = self.user_output.rbe_dose.get_output_path() + + rbe_weighted_dose_image.write(rbe_dose_path) + rbe_image.write(rbe_path) + + class ProductionAndStoppingActor(VoxelDepositActor, g4.GateProductionAndStoppingActor): """This actor scores the number of particles stopping or starting in a voxel grid in the volume to which the actor is attached to.""" @@ -993,8 +1681,77 @@ def EndSimulationAction(self): VoxelDepositActor.EndSimulationAction(self) +class EmCalculatorActor(ActorBase, g4.GateEmCalculatorActor): + user_info_defaults = { + "is_ion": ( + True, + { + "doc": "Wheather the particle we calculate dedx for is an ion", + }, + ), + "particle_name": ( + "", + { + "doc": "GenericIon if the particle is ion, else name of the particle", + }, + ), + "ion_params": ( + "", + { + "doc": "string indicating atomic number and mass, separated by one space. Example '6 12' for C12", + }, + ), + "material": ( + "", + { + "doc": "Material in which to score dedx", + }, + ), + "nominal_energies": ( + [], + { + "doc": "List of nominal energies to use to generate the dedx table", + }, + ), + "savefile_path": ( + "", + { + "doc": "file path to save the table", + }, + ), + } + + def __init__(self, *args, **kwargs): + ActorBase.__init__(self, *args, **kwargs) + self.__initcpp__() + + def __initcpp__(self): + g4.GateEmCalculatorActor.__init__(self, self.user_info) + self.AddActions( + { + "BeginOfRunActionMasterThread", + "EndOfRunActionMasterThread", + "BeginOfRunAction", + "EndOfRunAction", + "BeginOfEventAction", + "SteppingAction", + } + ) + + def initialize(self, *args): + if self.is_ion: + self.particle_name = "GenericIon" + self.InitializeUserInfo(self.user_info) # C++ side + self.InitializeCpp() + + process_cls(VoxelDepositActor) process_cls(DoseActor) +process_cls(TLEDoseActor) process_cls(LETActor) +process_cls(RBEActor) +process_cls(REActor) +process_cls(BeamQualityActor) process_cls(FluenceActor) process_cls(ProductionAndStoppingActor) +process_cls(EmCalculatorActor) diff --git a/opengate/managers.py b/opengate/managers.py index b56a5785a..4e3ef63a8 100644 --- a/opengate/managers.py +++ b/opengate/managers.py @@ -1,12 +1,11 @@ -import sys -import logging +import copy from typing import Optional, List, Union from box import Box from anytree import RenderTree, LoopError import shutil -import os import weakref from pathlib import Path +import io import opengate_core as g4 from .base import ( @@ -27,8 +26,8 @@ ensure_directory_exists, insert_suffix_before_extension, ) -from . import logger -from .logger import global_log +from .logger import * + from .physics import ( Region, OpticalSurface, @@ -62,6 +61,7 @@ VolumeBase, BoxVolume, SphereVolume, + EllipsoidVolume, TrapVolume, ImageVolume, TubsVolume, @@ -77,11 +77,17 @@ ) from .actors.filters import get_filter_class, FilterBase, filter_classes from .actors.base import ActorBase + from .actors.doseactors import ( DoseActor, + TLEDoseActor, LETActor, FluenceActor, ProductionAndStoppingActor, + RBEActor, + REActor, + BeamQualityActor, + EmCalculatorActor, ) from .actors.tleactors import TLEDoseActor, VoxelizedPromptGammaTLEActor from .actors.dynamicactors import DynamicGeometryActor @@ -94,9 +100,9 @@ ) from .actors.biasingactors import ( GenericBiasingActorBase, - ComptSplittingActor, - BremSplittingActor, - FreeFlightActor, + BremsstrahlungSplittingActor, + GammaFreeFlightActor, + ScatterSplittingFreeFlightActor, ) from .actors.digitizers import ( DigitizerAdderActor, @@ -108,6 +114,7 @@ DigitizerEnergyWindowsActor, DigitizerHitsCollectionActor, PhaseSpaceActor, + DigiAttributeProcessDefinedStepInVolumeActor, ) particle_names_Gate_to_G4 = { @@ -125,6 +132,10 @@ "VoxelizedPromptGammaTLEActor": VoxelizedPromptGammaTLEActor, "LETActor": LETActor, "ProductionAndStoppingActor": ProductionAndStoppingActor, + "RBEActor": RBEActor, + "REActor": REActor, + "BeamQualityActor": BeamQualityActor, + "EmCalculatorActor": EmCalculatorActor, "FluenceActor": FluenceActor, # misc "AttenuationImageActor": AttenuationImageActor, @@ -144,10 +155,11 @@ "DigitizerProjectionActor": DigitizerProjectionActor, "DigitizerEnergyWindowsActor": DigitizerEnergyWindowsActor, "DigitizerHitsCollectionActor": DigitizerHitsCollectionActor, + "DigiAttributeProcessDefinedStepInVolumeActor": DigiAttributeProcessDefinedStepInVolumeActor, # biasing - "BremSplittingActor": BremSplittingActor, - "ComptSplittingActor": ComptSplittingActor, - "FreeFlightActor": FreeFlightActor, + "BremsstrahlungSplittingActor": BremsstrahlungSplittingActor, + "GammaFreeFlightActor": GammaFreeFlightActor, + "ScatterSplittingFreeFlightActor": ScatterSplittingFreeFlightActor, } @@ -280,7 +292,7 @@ def __init__(self, simulation, *args, **kwargs): def __str__(self): """ - str only dump the user info on a single line + str only dumps the user info on a single line """ v = [v.name for v in self.sources.values()] s = f'{" ".join(v)} ({len(self.sources)})' @@ -305,6 +317,7 @@ def get_source(self, source_name): f"Cannot find the source {source_name}. " f"Sources included in this simulation are: {list(self.sources.keys())}" ) + return None # to avoid warning def add_source(self, source, name): new_source = None @@ -329,6 +342,26 @@ def add_source(self, source, name): # return the volume if it has not been passed as input, i.e. it was created here if new_source is not source: return new_source + return source + + def add_source_copy(self, origin_source_name, copied_source_name): + # get the source to copy + orig = self.get_source(origin_source_name) + # get its type name + cls_name = type(orig).__name__ + # create the new source + new_source = self.add_source(cls_name, copied_source_name) + # copy all elements except its name + new_source.user_info = copy.deepcopy(orig.user_info) + new_source.user_info.name = copied_source_name + return new_source + + def remove_source(self, source_name): + if not source_name in self.sources: + fatal( + f"In 'remove_source', the source {source_name} is not in the list of sources: {self.sources}" + ) + self.sources.pop(source_name) def _create_source(self, source_type, name): cls = None @@ -343,6 +376,10 @@ def _create_source(self, source_type, name): return cls(name=name, simulation=self.simulation) def initialize_before_g4_engine(self): + if len(self.sources) == 0: + self.simulation.warn_user( + "No configured source, no particle will be generated." + ) for source in self.sources.values(): if source.initialize_source_before_g4_engine: source.initialize_source_before_g4_engine(source) @@ -451,6 +488,44 @@ def add_actor(self, actor, name): if new_actor is not actor: return new_actor + def find_actors(self, sub_str, case_sensitive=False): + # find all actors that contain this substring + actors = [] + if not case_sensitive: + sub_str = sub_str.lower() + for actor in self.actors.values(): + name = actor.name + if not case_sensitive: + name = name.lower() + if sub_str in name: + actors.append(actor) + return actors + + def find_actors_by_type(self, type_name, sub_str=None, case_sensitive=False): + # find all actors of a given type + actors = [] + if not case_sensitive and sub_str is not None: + sub_str = sub_str.lower() + for actor in self.actors.values(): + if actor.type_name == type_name: + if sub_str is None: + actors.append(actor) + else: + name = actor.name + if not case_sensitive: + name = name.lower() + if sub_str in name: + actors.append(actor) + return actors + + def find_actor_by_type(self, type_name, sub_str=None, case_sensitive=False): + actors = self.find_actors_by_type(type_name, sub_str, case_sensitive) + if len(actors) == 1: + return actors[0] + else: + fatal(f'Found {len(actors)} actors of type "{type_name}". Expected 1.') + return None + def remove_actor(self, name): self.actors.pop(name) @@ -484,13 +559,20 @@ class PhysicsListManager(GateObject): "G4OpticalPhysics", ] - special_physics_constructor_classes = {} - special_physics_constructor_classes["G4DecayPhysics"] = g4.G4DecayPhysics - special_physics_constructor_classes["G4RadioactiveDecayPhysics"] = ( - g4.G4RadioactiveDecayPhysics - ) - special_physics_constructor_classes["G4OpticalPhysics"] = g4.G4OpticalPhysics - special_physics_constructor_classes["G4EmDNAPhysics"] = g4.G4EmDNAPhysics + special_physics_constructor_classes = { + "G4DecayPhysics": g4.G4DecayPhysics, + "G4RadioactiveDecayPhysics": g4.G4RadioactiveDecayPhysics, + "G4OpticalPhysics": g4.G4OpticalPhysics, + "G4EmDNAPhysics": g4.G4EmDNAPhysics, + "G4EmDNAPhysics_option1": g4.G4EmDNAPhysics_option1, + "G4EmDNAPhysics_option2": g4.G4EmDNAPhysics_option2, + "G4EmDNAPhysics_option3": g4.G4EmDNAPhysics_option3, + "G4EmDNAPhysics_option4": g4.G4EmDNAPhysics_option4, + "G4EmDNAPhysics_option5": g4.G4EmDNAPhysics_option5, + "G4EmDNAPhysics_option6": g4.G4EmDNAPhysics_option6, + "G4EmDNAPhysics_option7": g4.G4EmDNAPhysics_option7, + "G4EmDNAPhysics_option8": g4.G4EmDNAPhysics_option8, + } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -678,6 +760,12 @@ class PhysicsManager(GateObject): "Mostly used for using acolinearity during annihilation in some materials" }, ), + "material_ionisation_potential": ( + Box(), + { + "doc": "Dict of material_name:energy_value, such that: sim.physics_manager.material_ionisation_potential['IEC_PLASTIC'] = 5.0 * eV. " + }, + ), # "processes_to_bias": ( # Box( # [ @@ -1003,6 +1091,7 @@ class VolumeManager(GateObject): volume_types = { "BoxVolume": BoxVolume, "SphereVolume": SphereVolume, + "EllipsoidVolume": EllipsoidVolume, "TrapVolume": TrapVolume, "ImageVolume": ImageVolume, "TubsVolume": TubsVolume, @@ -1048,6 +1137,11 @@ def __init__(self, simulation, *args, **kwargs) -> None: # database of materials self.material_database = MaterialDatabase() + # List of ImageBox solids + # They need to be init after the creation of OpenGL + # Store them to init them later + self.solid_with_texture_init = [] + def reset(self): self.__init__(self.simulation) @@ -1119,6 +1213,19 @@ def get_volume(self, volume_name): f"Volumes included in this simulation are: {self.volumes.keys()}" ) + def find_volumes(self, sub_str, case_sensitive=False): + # find all volumes that contains this substring + volumes = [] + if not case_sensitive: + sub_str = sub_str.lower() + for actor in self.volumes.values(): + name = actor.name + if not case_sensitive: + name = name.lower() + if sub_str in name: + volumes.append(actor) + return volumes + def update_volume_tree_if_needed(self): if self._need_tree_update is True: self.update_volume_tree() @@ -1236,14 +1343,25 @@ def print_material_database_names(self): print(self.dump_material_database_names()) -def setter_hook_verbose_level(self, verbose_level): - try: - level = int(verbose_level) - except ValueError: - level = getattr(logging, verbose_level) - global_log.setLevel(level) - # return verbose_level - return level +def _setter_hook_verbose_level(self, verbose_level): + # print(f"_setter_hook_verbose_level to ", sim.log_handler_id, verbose_level) + if self.log_handler_id == -1: + # Remove the default logger configuration + logger.remove() + else: + # Remove the current logger configuration + try: + logger.remove(self.log_handler_id) + except: + pass + + self.log_handler_id = logger.add( + sys.stdout, + colorize=True, + level=verbose_level, + format=("{level.icon}{message}"), # Message formatting + ) + return verbose_level class Simulation(GateObject): @@ -1252,13 +1370,14 @@ class Simulation(GateObject): It contains: - a set of global user parameters (SimulationUserInfo) - user parameters for Volume, Source, Actors and Filters, Physics - - a list of g4 commands that will be set to G4 engine after the initialization + - a list of g4 commands that will be set to the G4 engine after the initialisation There is NO Geant4 engine here, it is only a set of parameters and options. """ # hints for IDE verbose_level: int + log_sink: str verbose_close: bool verbose_getstate: bool running_verbose_level: int @@ -1280,7 +1399,7 @@ class Simulation(GateObject): run_timing_intervals: List[List[float]] output_dir: Path store_json_archive: bool - json_archive_filename: Path + json_archive_filename: str store_input_files: bool g4_commands_before_init: List[str] g4_commands_after_init: List[str] @@ -1295,15 +1414,27 @@ class Simulation(GateObject): { "doc": "Gate pre-run verbosity. " "Will display more or fewer messages during initialization. ", - "allowed_values": ( + "setter_hook": _setter_hook_verbose_level, + "allowed_values": [ "NONE", - "INFO", "DEBUG", - logger.NONE, - logger.INFO, - logger.DEBUG, - ), - "setter_hook": setter_hook_verbose_level, + "INFO", + "WARNING", + "CRITICAL", + NONE, + DEBUG, + INFO, + WARNING, + CRITICAL, + ], + }, + ), + "log_sink": ( + None, + { + "doc": "Log sink. " + "If None, the default logger sys.stdout will be used. " + "If 'str', the logger output will be available after the simulation in sim.log_output" }, ), "verbose_close": ( @@ -1318,7 +1449,7 @@ class Simulation(GateObject): 0, { "doc": "Gate verbosity while the simulation is running.", - # "allowed_values": (0, logger.RUN, logger.EVENT), # FIXME + "allowed_values": (0, RUN, EVENT), }, ), "g4_verbose_level": ( @@ -1505,16 +1636,17 @@ class Simulation(GateObject): def __init__(self, name="simulation", **kwargs): """ - Main members are: + The main members are: - managers of volumes, physics, sources, actors and filters - the Geant4 objects will be only built during initialisation in SimulationEngine """ - # default (INFO level) - global_log.setLevel(12) + # init logger + self.log_handler_id = -1 + self.log_output = "" + _setter_hook_verbose_level(self, INFO) # The Simulation instance should not hold a reference to itself (cycle) kwargs.pop("simulation", None) - setter_hook_verbose_level(self, "INFO") super().__init__(name=name, **kwargs) # list to store warning messages issued somewhere in the simulation @@ -1595,19 +1727,11 @@ def from_dictionary(self, d): self.actor_manager.from_dictionary(d["actor_manager"]) def to_json_string(self): - warning( - "******************************************************************************\n" - "* WARNING: Only parts of the simulation can currently be dumped as JSON. *\n" - "******************************************************************************\n" - ) + warning("Only parts of the simulation can currently be dumped as JSON") return dumps_json(self.to_dictionary()) def to_json_file(self, directory=None, filename=None): - warning( - "******************************************************************************\n" - "* WARNING: Only parts of the simulation can currently be dumped as JSON. *\n" - "******************************************************************************\n" - ) + warning("Only parts of the simulation can currently be dumped as JSON.") d = self.to_dictionary() if filename is None: filename = self.json_archive_filename @@ -1619,19 +1743,11 @@ def to_json_file(self, directory=None, filename=None): self.copy_input_files(directory, dct=d) def from_json_string(self, json_string): - warning( - "**********************************************************************************\n" - "* WARNING: Only parts of the simulation can currently be reloaded from JSON. *\n" - "**********************************************************************************\n" - ) + warning("Only parts of the simulation can currently be reloaded from JSON.") self.from_dictionary(loads_json(json_string)) def from_json_file(self, path): - warning( - "**********************************************************************************\n" - "* WARNING: Only parts of the simulation can currently be reloaded from JSON. *\n" - "**********************************************************************************\n" - ) + warning("Only parts of the simulation can currently be reloaded from JSON.") with open(path, "r") as f: self.from_dictionary(load_json(f)) @@ -1723,6 +1839,9 @@ def add_actor(self, actor_type, name): def get_actor(self, name): return self.actor_manager.get_actor(name) + def find_actors(self, sub_str, case_sensitive=False): + return self.actor_manager.find_actors(sub_str, case_sensitive) + def add_filter(self, filter_type, name): return self.filter_manager.add_filter(filter_type, name) @@ -1730,17 +1849,22 @@ def add_filter(self, filter_type, name): def multithreaded(self): return self.number_of_threads > 1 or self.force_multithread_mode + def initialize_logger(self): + original_stdout = sys.stdout + self.log_output = io.StringIO() + if self.log_sink == "str": + sys.stdout = self.log_output + # reinstall the logger (for subprocesses) + self.verbose_level = self.verbose_level + return original_stdout + def _run_simulation_engine(self, start_new_process): """Method that creates a simulation engine in a context (with ...) and runs a simulation. - Args: - q (:obj: queue, optional) : A queue object to which simulation output can be added if run in a subprocess. - The dispatching function needs to extract the output from the queue. - start_new_process (bool, optional) : A flag passed to the engine + start_new_process (bool, optional): A flag passed to the engine so it knows if it is running in a subprocess. - Returns: - :obj:SimulationOutput : The output of the simulation run. + obj:SimulationOutput : The output of the simulation run. """ with SimulationEngine(self) as se: @@ -1757,7 +1881,7 @@ def run(self, start_new_process=False): "Run the simulation with one thread." ) - # prepare sub process + # prepare the subprocess if start_new_process is True: """Important: put: if __name__ == '__main__': @@ -1765,10 +1889,10 @@ def run(self, start_new_process=False): https://britishgeologicalsurvey.github.io/science/python-forking-vs-spawn/ """ - global_log.info("Dispatching simulation to subprocess ...") + logger.info("Dispatching simulation to subprocess ...") output = dispatch_to_subprocess(self._run_simulation_engine, True) - # Recover output from unpickled actors coming from sub-process queue + # Recover output from unpickled actors coming from the subprocess queue for actor in self.actor_manager.actors.values(): actor.recover_user_output(output.get_actor(actor.name)) @@ -1787,13 +1911,20 @@ def run(self, start_new_process=False): if "total_zero_events" in s.__dict__: source.total_zero_events = s.__dict__["total_zero_events"] source.total_skipped_events = s.__dict__["total_skipped_events"] + if "particle_generators" in s.__dict__: + source.particle_generators = s.__dict__["particle_generators"] + source.num_entries = s.__dict__["num_entries"] else: # Nothing special to do if the simulation engine ran in the native python process # because everything is already in place. output = self._run_simulation_engine(False) - self._user_warnings.extend(output.warnings) + # replace warnings by the one of the subprocess + self._user_warnings = output.warnings + + # save the log output + self.log_output = output.log_output # FIXME workaround self.expected_number_of_events = output.expected_number_of_events @@ -1809,14 +1940,24 @@ def run(self, start_new_process=False): if self.volume_manager.material_database is None: self.volume_manager.material_database = MaterialDatabase() - if len(self.warnings) > 0: - print("*" * 20) - print(f"{len(self.warnings)} warnings occurred in this simulation: \n") - for i, w in enumerate(self.warnings): - print(f"{i+1}) " + "-" * 10) - print(w) - print() - print("*" * 20) + # print the warnings (if the logger level is ok) + if log_level(self.log_handler_id) < WARNING: + if len(self.warnings) > 0: + if len(self.warnings) == 1: + warning(f"One warning occurred in this simulation:") + else: + warning( + f"{len(self.warnings)} warnings occurred in this simulation:" + ) + for i, w in enumerate(self.warnings): + warning(f"({i+1}) {w}") + + # For all biasing operators inheriting from G4VBiasingOperator, + # we need to clean the global static variable "fOperators" once everything is done + # to be able to start another simulation. This should be only once, + # and for any one of the operators, we choose GateGammaFreeFlightOptrActor + # but this cleans for all. Trust me, bro. + g4.GateGammaFreeFlightOptrActor.ClearOperators() def voxelize_geometry( self, @@ -1830,7 +1971,7 @@ def voxelize_geometry( def initialize_source_before_g4_engine(self): """ - Some sources need to perform computation once everything is defined in user_info but *before* the + Some sources need to perform computation once everything is defined in user_info, but *before* the initialization of the G4 engine starts. This can be done via this function. """ self.source_manager.initialize_before_g4_engine() From d404c748b09f6c3c0cde817e44fc2da4f950f86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean=20M=20L=C3=A9tang?= Date: Mon, 8 Dec 2025 13:26:05 +0100 Subject: [PATCH 13/13] conflict mods --- core/opengate_core/opengate_lib/GateDoseActor.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/opengate_core/opengate_lib/GateDoseActor.h b/core/opengate_core/opengate_lib/GateDoseActor.h index 18a9657ac..b3f1f24e1 100644 --- a/core/opengate_core/opengate_lib/GateDoseActor.h +++ b/core/opengate_core/opengate_lib/GateDoseActor.h @@ -110,6 +110,9 @@ class GateDoseActor : public GateVActor { static void PrepareLocalDataForRun(threadLocalT &data, unsigned int numberOfVoxels); + void GetVoxelPosition(G4Step *step, G4ThreeVector &position, bool &isInside, + Image3DType::IndexType &index) const; + // Option: indicate we must convert to dose to water bool fToWaterFlag{};