Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,26 @@ jobs:
uses: actions/checkout@v6
- uses: ./.github/workflows/actions_tests

test_wheel_castor:
runs-on: ubuntu-24.04
needs: [build_opengate_wheel, build_opengate_core_wheel_pr]
strategy:
fail-fast: false
steps:
- name: Checkout github repo
uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
pattern: dist-*
merge-multiple: true
path: dist/
- name: Run tests
shell: bash {0}
run: |
mv dist/opengate_core-*-cp312-cp312-manylinux*_x86_64.whl $GITHUB_WORKSPACE/.github/workflows/
mv dist/opengate-*.whl $GITHUB_WORKSPACE/.github/workflows/
docker run --rm -v $GITHUB_WORKSPACE:/home tbaudier/opengate_castor_ci /home/opengate/tests/src/external/castor/run_test_opengate_castor.sh

publish_test:
runs-on: ubuntu-24.04
if: ${{ (always()) && (github.ref == 'refs/heads/master') }}
Expand Down
2 changes: 2 additions & 0 deletions opengate/bin/opengate_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ def get_files_to_run():
"test094a_spect_simu_prim", # to compute the reference data
"test094b_spect_simu_scatter", # to compute the reference data
"test094c_build_sinogram", # to compute the reference data
"test096_pet_castor_ref.py", # test for castor compatibility
"test096_pet_castor_coinc.py", # test for castor compatibility
]
path_tests_src = Path(path_tests_src)
all_file_paths = []
Expand Down
233 changes: 233 additions & 0 deletions opengate/contrib/pet/ge_signa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import pathlib
from opengate.utility import g4_units
from opengate.geometry.utility import get_grid_repetition, get_circular_repetition
from opengate.actors.coincidences import *

# colors
red = [1, 0, 0, 1]
blue = [0, 0, 1, 1]
green = [0, 1, 0, 1]
yellow = [0.9, 0.9, 0.3, 1]
gray = [0.5, 0.5, 0.5, 1]
white = [1, 1, 1, 0.8]
transparent = [1, 1, 1, 0]


def create_LYSO_material(sim):
g_cm3 = g4_units.g_cm3
sim.volume_manager.material_database.add_material_weights(
"LYSO", ["Lu", "Y", "Si", "O"], [0.7299, 0.0279, 0.0628, 0.1794], 7.21 * g_cm3
)


def add_pet(sim, name="pet"):
"""
Geometry of a PET/MR SIGNA GE
"""

# unit
mm = g4_units.mm

create_LYSO_material(sim)

# ring volume
pet = sim.add_volume("Tubs", "pet")
pet.rmax = 350 * mm
pet.rmin = 290 * mm
pet.dz = 135 * mm
pet.color = gray
pet.material = "G4_AIR"

# module
module = sim.add_volume("Box", "module")
module.mother = pet.name
module.size = [64.5 * mm, 25 * mm, 250.4 * mm]
translations_ring, rotations_ring = get_circular_repetition(
28, [0 * mm, 324.3 * mm, 0 ], start_angle_deg=190, axis=[0, 0, 1]
)
module.translation = translations_ring
module.rotation = rotations_ring
module.material = "G4_AIR"
module.color = white


# unit
unit = sim.add_volume("Box", "unit")
unit.mother = module.name
unit.size = [ module.size[0] , module.size[1] , 47.84 * mm ]
unit.material = "G4_AIR"
unit.translation = get_grid_repetition([1, 1, 5], [0, 0 * mm, 50.64 * mm])
unit.color = blue


# block
block = sim.add_volume("Box", "block")
block.mother = unit.name
block.size = [ 15.9 * mm , module.size[1] , 47.84 * mm ]
block.material = "G4_AIR"
block.translation = get_grid_repetition([4, 1, 1], [16.2 * mm, 0 * mm, 0 * mm])
block.color = red

# optical
optical = sim.add_volume("Box", "optical")
optical.mother = block.name
optical.size = [ 15.9 * mm , module.size[1] , 15.9 * mm ]
optical.material = "G4_AIR"
optical.translation = get_grid_repetition([1, 1, 3], [0, 0 , 15.97 * mm])
optical.color = yellow

# Crystal
crystal = sim.add_volume("Box", "crystal")
crystal.mother = optical.name
crystal.size = [3.95 * mm, module.size[1], 5.3 * mm]
crystal.material = "LYSO"
#crystal.translation = get_grid_repetition([1, 1, 1], [0 * mm ,0 * mm, 0 * mm])
crystal.color = green

n_crystal = len(crystal.translation)
n_optical= len(optical.translation)
n_block = len(block.translation)
n_unit = len(unit.translation)
n_module = len(module.translation)

print(f"Number of crystals : {n_crystal}")
print(f"Number of opticals : {n_optical}")
print(f"Number of blocks : {n_block}")
print(f"Number of units : {n_unit}")
print(f"Number of modules : {n_module}")
return pet


def add_digitizer(
sim, pet_name, output_filename, hits_name="Hits", singles_name="Singles"
):

print( output_filename)
# unit
mm = g4_units.mm
keV = g4_units.keV
ps = g4_units.ps


# get crystal volume
crystal = sim.volume_manager.volumes["crystal"]
unit = sim.volume_manager.volumes["unit"]

# hits collection
hc = sim.add_actor("DigitizerHitsCollectionActor", hits_name)
hc.attached_to = crystal.name
hc.authorize_repeated_volumes = True
#hc.output_filename = output_filename
hc.attributes = [
"EventID",
"PreStepUniqueVolumeIDAsInt",
"PostPositionLocal",
"PostPosition",
"TotalEnergyDeposit",
"PreStepUniqueVolumeID",
"GlobalTime",
]

# ADDER
sc_adder = sim.add_actor("DigitizerAdderActor", f"Singles_{crystal.name}_adder")
sc_adder.attached_to = hc.attached_to
sc_adder.authorize_repeated_volumes = True
sc_adder.input_digi_collection = hc.name
sc_adder.discretize_volume = crystal.name
sc_adder.policy = "EnergyWeightedCentroidPosition"
#sc_adder.output = output_filename

# READOUT
sc_ro = sim.add_actor("DigitizerReadoutActor", f"Singles_{crystal.name}_readout")
sc_ro.authorize_repeated_volumes = True
sc_ro.attached_to = sc_adder.attached_to
sc_ro.input_digi_collection = sc_adder.name
sc_ro.group_volume = unit.name
sc_ro.discretize_volume = crystal.name
sc_ro.policy = "EnergyWinnerPosition"
#sc_ro.output_filename = output_filename

# SPATIAL BLURRING
sc_sp_blur = sim.add_actor("DigitizerSpatialBlurringActor", f"Singles_{crystal.name}_sp_blur")
sc_sp_blur.attached_to = sc_ro.attached_to
sc_sp_blur.authorize_repeated_volumes = True
sc_sp_blur.input_digi_collection = sc_ro.name
sc_sp_blur.keep_in_solid_limits = False
sc_sp_blur.use_truncated_Gaussian = True
sc_sp_blur.blur_attribute = "PostPosition"
sc_sp_blur.blur_fwhm = [1*mm, 1*mm, 1*mm]
#sc_sp_blur.output_filename = output_filename

# EFFICIENCY
sc_efficiency = sim.add_actor("DigitizerEfficiencyActor",f"Singles_{crystal.name}_efficiency")
sc_efficiency.attached_to = sc_sp_blur.attached_to
sc_efficiency.authorize_repeated_volumes = True
sc_efficiency.input_digi_collection = sc_sp_blur.name
sc_efficiency.efficiency = 0.93
#sc_eff.output_filename = output_filename

# ENERGY BLURRING
sc_energy_blur = sim.add_actor("DigitizerBlurringActor", f"Singles_{crystal.name}_energy_blur")
sc_energy_blur.attached_to = sc_efficiency.attached_to
sc_energy_blur.authorize_repeated_volumes = True
sc_energy_blur.input_digi_collection = sc_efficiency.name
sc_energy_blur.blur_attribute = "TotalEnergyDeposit"
sc_energy_blur.blur_method = "InverseSquare"
sc_energy_blur.blur_resolution = 0.12
sc_energy_blur.blur_reference_value = 511 * keV
#sc_ebergy_blur.output_filename = output_filename

# TIME BLURRING
sc_time_blur = sim.add_actor("DigitizerBlurringActor", f"Singles_{crystal.name}_time_blur")
sc_time_blur.input_digi_collection = sc_energy_blur.name
sc_time_blur.attached_to = sc_energy_blur.attached_to
sc_time_blur.authorize_repeated_volumes = True
sc_time_blur.blur_attribute = "GlobalTime"
sc_time_blur.blur_method = "Gaussian"
sc_time_blur.blur_fwhm = 270 * ps
#sc_time_blur.output_filename = output_filename


# ENERGY WINDOW
sc_energy_window = sim.add_actor("DigitizerEnergyWindowsActor",f"{singles_name}" )
sc_energy_window.attached_to = sc_time_blur.attached_to
sc_energy_window.authorize_repeated_volumes = True
sc_energy_window.input_digi_collection = sc_time_blur.name
sc_energy_window.channels = [
{"name": f"{singles_name}", "min": 425 * keV, "max": 650 * keV}]
sc_energy_window.output_filename = output_filename

return sc_energy_window

## Sorter: 4.57 takeAllGoods minsecDiff 3

def add_coincidences_sorter(
singles_tree
):

mm = g4_units.mm
ns = g4_units.ns

time_window = 4.57 * ns
policy = "takeAllGoods"
min_trans_dist = 26 * mm
transaxial_plane = "xy"
max_axial_dist = 300 * mm


# apply coincidences sorter
# (chunk size can be much larger, keep a low value to check it is ok)
coincidences = coincidences_sorter(
singles_tree,
time_window,
policy,
min_trans_dist,
transaxial_plane,
max_axial_dist,
chunk_size=1000000,
)




return coincidences
Empty file.
44 changes: 44 additions & 0 deletions opengate/tests/src/external/castor/Dockerfile_opengate_castor
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#Docker for opengate_core
#systemctl start docker
#login: docker login
#build: docker build -t tbaudier/opengate_castor_ci -f Dockerfile_opengate_castor.
#push: docker push tbaudier/opengate_castor_ci
#run: docker run --rm -v $(Pipeline.Workspace):/home tbaudier/opengate_castor_ci /home/.github/workflows/run_test_opengate_castor.sh
#interactive: docker run -ti --rm -v $PWD:/home quay.io/pypa/manylinux_2_34_x86_64 /bin/bash

FROM quay.io/pypa/manylinux_2_34_x86_64
MAINTAINER Thomas Baudier <thomas.baudier@creatis.insa-lyon.fr>
#Install packages
RUN yum install -y gcc wget git expat-devel fftw-devel qt6-qtbase-devel freeglut-devel libXmu-devel xerces-c-devel libXpm-devel libXft-devel xrootd-devel openssl-devel libuuid-devel \

#Create folder
&& mkdir -p /software/cmake /software/root/src /software/root/bin /software/castor/src /software/castor/bin /software/wheelhouse \

#Install cmake
&& cd /software/cmake \
&& wget https://github.com/Kitware/CMake/releases/download/v3.31.8/cmake-3.31.8-linux-x86_64.tar.gz \
&& tar xzvf cmake-3.31.8-linux-x86_64.tar.gz \
&& rm -rf cmake-3.31.8-linux-x86_64.tar.gz \
&& mv cmake-3.31.8-linux-x86_64 cmake \
&& export PATH=/software/cmake/cmake/bin/:${PATH} \

#Compile root
&& cd /software/root \
&& git clone --branch v6-36-06 https://github.com/root-project/root.git --depth 1 src \
&& cd bin \
&& . /opt/rh/gcc-toolset-14/enable \
&& cmake -Dxrootd=OFF ../src \
&& make -j10 \
&& source /software/root/bin/bin/thisroot.sh \

#Compile castor
&& cd /software/castor \
&& git clone -b feature/gatev10_converter https://gitlab.com/castor-collaboration/castor.git --depth 1 src \
&& cd bin \
&& cmake -DCMAKE_CXX_FLAGS=-std=c++17 -DCASToR_BUILD_GATE_UTILITIES=ON ../src \
&& make -j10 \
&& export PATH=/software/castor/bin/:$PATH

# docker commit <container_name>
# docker tag <image_id> tbaudier/opengate_castor_ci:1.0.0

Empty file.
19 changes: 19 additions & 0 deletions opengate/tests/src/external/castor/run_test_opengate_castor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#! /bin/bash

#Instal opengate wheels
cd /home/.github/workflows
/opt/python/cp312-cp312/bin/pip install opengate_core-*.whl
/opt/python/cp312-cp312/bin/pip install opengate-*.whl
cd /opt/_internal/cpython-3.12.12/lib/python3.12/site-packages/opengate/tests/src/external/castor
/opt/python/cp312-cp312/bin/python test096_pet_castor.py
/opt/python/cp312-cp312/bin/python test096_pet_castor_ref.py
/opt/python/cp312-cp312/bin/python test096_pet_castor_coinc.py
cd /opt/_internal/cpython-3.12.12/lib/python3.12/site-packages/opengate/tests/output/test096_pet_castor_interface

#Convert Gate root output to Castor
export PATH=/software/castor/bin/:$PATH
castor-GATERootToCastor -vb 2 -i coincidences.root -o test_gatev10 -js castor_config.json -s "PET_GATEv10_TEST" -t -geo

#Reonstruction with castor
castor-recon -vb 2 -df test_gatev10_df.Cdh -fout reco_test_gatev10 -oit -1 -it 10:1 -dim 128,128,40 -vox 3,3,3 -th 0 -fov-out 98. -slice-out 2

Empty file.