Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6f79295
add .copy() to filtered DataFrames in coincidences.py to avoid Settin…
dsarrut Nov 28, 2025
679724a
simplify imports in test096_ideal_compton_sorter.py by using wildcard…
dsarrut Nov 28, 2025
556a4a0
add MACACO1 materials, camera definitions, and CORESI utility/helper …
dsarrut Nov 28, 2025
2d08230
fix comments in castor_helpers.py: correct grammar, clarify unused pa…
dsarrut Nov 28, 2025
fefbd15
remove unnecessary camera translation in macaco.py
dsarrut Nov 28, 2025
79c195f
add CORESI configuration management utilities, refactor coresi_helper…
dsarrut Nov 28, 2025
58720cd
Merge branch 'master' into coresi_ccmod
dsarrut Nov 30, 2025
0fe1387
Refactor add_macaco1_camera function
ZainaHurani Dec 14, 2025
85cb61e
Add PCB material with density and elements
ZainaHurani Dec 14, 2025
9cbde69
Merge pull request #864 from ZainaHurani/patch-2
dsarrut Dec 15, 2025
e59006e
Update materials and translations in macaco.py
ZainaHurani Dec 17, 2025
ca79a0e
Merge pull request #863 from ZainaHurani/patch-1
dsarrut Dec 18, 2025
e2fd45a
switch from fork to spawn for GUI safety
dsarrut Dec 3, 2025
857c69b
was crashing with new process
dsarrut Dec 3, 2025
3561632
Switch to Manager.Queue for all multiprocessing methods.
dsarrut Dec 5, 2025
8ce0375
Refactor GateSourceManager to use static argv/argc for visualization …
dsarrut Dec 5, 2025
ac5561a
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Dec 8, 2025
4780bb9
Merge master into coresi_ccmod and resolve naming/folder conflicts
dsarrut Jan 6, 2026
2c2539b
Remove unused test096_ideal_compton_sorter and helper scripts
dsarrut Jan 6, 2026
82b6b16
Add `add_macaco1_camera_digitizer` function for setting up digitizers…
dsarrut Jan 6, 2026
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
4 changes: 2 additions & 2 deletions opengate/actors/coincidences.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ def ccmod_ideal_singles(data):
)
df = filter_pandas_tree(
df, branch_name="ProcessDefinedStep", value="Rayl", accepted=False
) # Check but I think that this process did not generate a pulse
).copy()

# Create a new branch with ideal energy info
df["IdealTotalEnergyDeposit"] = df["PreKineticEnergy"] - df["PostKineticEnergy"]
Expand All @@ -668,7 +668,7 @@ def ccmod_ideal_coincidences(df):
# create a new attribute CoincID that groups hits from the same coincidence. We can have more that two hits/pulses in a coincidence oe
nSingles = df["EventID"].value_counts()
# keep only events with more than one nSingles
df = df[df["EventID"].isin(nSingles[nSingles > 1].index)]
df = df[df["EventID"].isin(nSingles[nSingles > 1].index)].copy()
# Assign CoincIDs starting from 0, in order of first appearance).
df["CoincID"] = pd.factorize(df["EventID"])[0]

Expand Down
208 changes: 208 additions & 0 deletions opengate/contrib/compton_camera/coresi_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import opengate_core as g4
from opengate.geometry.utility import vec_g4_as_np, rot_g4_as_np
from opengate.exception import fatal
from opengate.utility import g4_units
import yaml
import uproot


# --- Custom List for Inline YAML Formatting ---
class FlowList(list):
"""A custom list that will be dumped as [x, y, z] in YAML."""

pass


def flow_list_representer(dumper, data):
return dumper.represent_sequence("tag:yaml.org,2002:seq", data, flow_style=True)


yaml.add_representer(FlowList, flow_list_representer)


def convert_to_flowlist(data):
"""
Recursively traverse the dictionary.
Convert any list containing only simple scalars (int, float, str) to FlowList
so they appear as [a, b, c] in the YAML output.
"""
if isinstance(data, dict):
return {k: convert_to_flowlist(v) for k, v in data.items()}
elif isinstance(data, list):
# Check if list is "simple" (contains only primitives, no dicts or nested lists)
is_simple = all(
isinstance(i, (int, float, str, bool)) or i is None for i in data
)

if is_simple:
return FlowList(data)
else:
# If list contains complex objects, process them recursively but keep the list as is
return [convert_to_flowlist(i) for i in data]
else:
return data


def coresi_new_config():
config = {
"data_file": "coinc.dat",
"data_type": "GATE",
"n_events": 0,
"starts_at": 0,
"E0": [],
"remove_out_of_range_energies": False,
"energy_range": [120, 150],
"energy_threshold": 5,
"log_dir": None,
"cameras": {
"n_cameras": 0,
"common_attributes": {
"n_sca_layers": 0,
"sca_material": "Si",
"abs_material": "Si",
"n_absorbers": 0,
},
"position_0": {
"frame_origin": [0, 0, 0],
"Ox": [1, 0, 0], # parallel to scatterer edge
"Oy": [0, 1, 0], # parallel to scatterer edge
"Oz": [0, 0, 1], # orthogonal to the camera, tw the source"
},
},
"volume": {
"volume_dimensions": [10, 10, 10], # in cm?
"n_voxels": [50, 50, 1], # in voxels
"volume_centre": [0, 0, 0], # in cm?
},
"lm_mlem": {
"cone_thickness": "angular",
"model": "cos1rho2",
"last_iter": 0,
"first_iter": 0,
"n_sigma": 2,
"width_factor": 1,
"checkpoint_dir": "checkpoints",
"save_every": 76,
"sensitivity": False,
"sensitivity_model": "like_system_matrix",
"sensitivity_point_samples": 1,
},
}

return config


def set_hook_coresi_config(sim, cameras, filename):
"""
Prepare everything to create the coresi config file at the init of the simulation.
The param structure allows retrieving the coresi config at the end of the simulation.
"""
# create the param structure
param = {
"cameras": cameras,
"filename": filename,
"coresi_config": coresi_new_config(),
}
sim.user_hook_after_init = create_coresi_config
sim.user_hook_after_init_arg = param
return param


def create_coresi_config(simulation_engine, param):
# (note: simulation_engine is not used here but must be the first param)
coresi_config = param["coresi_config"]
cameras = param["cameras"]

for camera in cameras.values():
c = coresi_config["cameras"]
c["n_cameras"] += 1
scatter_layer_names = camera["scatter_layer_names"]
absorber_layer_names = camera["absorber_layer_names"]

for layer_name in scatter_layer_names:
coresi_add_scatterer(coresi_config, layer_name)
for layer_name in absorber_layer_names:
coresi_add_absorber(coresi_config, layer_name)


def coresi_add_scatterer(coresi_config, layer_name):
# find all volumes ('touchable' in Geant4 terminology)
touchables = g4.FindAllTouchables(layer_name)
if len(touchables) != 1:
fatal(f"Cannot find unique volume for layer {layer_name}: {touchables}")
touchable = touchables[0]

# current nb of scatterers
id = coresi_config["cameras"]["common_attributes"]["n_sca_layers"]
coresi_config["cameras"]["common_attributes"]["n_sca_layers"] += 1
layer = {
"center": [0, 0, 0],
"size": [0, 0, 0],
}
coresi_config["cameras"]["common_attributes"][f"sca_layer_{id}"] = layer

# Get the information: WARNING in cm!
cm = g4_units.cm
translation = vec_g4_as_np(touchable.GetTranslation(0)) / cm
solid = touchable.GetSolid(0)
pMin_local = g4.G4ThreeVector()
pMax_local = g4.G4ThreeVector()
solid.BoundingLimits(pMin_local, pMax_local)
size = [
(pMax_local.x - pMin_local.x) / cm,
(pMax_local.y - pMin_local.y) / cm,
(pMax_local.z - pMin_local.z) / cm,
]
layer["center"] = translation.tolist()
layer["size"] = size


def coresi_add_absorber(coresi_config, layer_name):
# find all volumes ('touchable' in Geant4 terminology)
touchables = g4.FindAllTouchables(layer_name)
if len(touchables) != 1:
fatal(f"Cannot find unique volume for layer {layer_name}: {touchables}")
touchable = touchables[0]

# current nb of scatterers
id = coresi_config["cameras"]["common_attributes"]["n_absorbers"]
coresi_config["cameras"]["common_attributes"]["n_absorbers"] += 1
layer = {
"center": [0, 0, 0],
"size": [0, 0, 0],
}
coresi_config["cameras"]["common_attributes"][f"abs_layer_{id}"] = layer

# Get the information: WARNING in cm!
cm = g4_units.cm
translation = vec_g4_as_np(touchable.GetTranslation(0)) / cm
solid = touchable.GetSolid(0)
pMin_local = g4.G4ThreeVector()
pMax_local = g4.G4ThreeVector()
solid.BoundingLimits(pMin_local, pMax_local)
size = [
(pMax_local.x - pMin_local.x) / cm,
(pMax_local.y - pMin_local.y) / cm,
(pMax_local.z - pMin_local.z) / cm,
]
layer["center"] = translation.tolist()
layer["size"] = size


def coresi_write_config(coresi_config, filename):
# Convert vectors to FlowList just before writing
formatted_config = convert_to_flowlist(coresi_config)

with open(filename, "w") as f:
yaml.dump(
formatted_config, f, default_flow_style=False, sort_keys=False, indent=2
)


def coresi_convert_root_data(root_filename, branch_name, output_filename):
root_file = uproot.open(root_filename)
tree = root_file[branch_name]
print("todo")
Loading
Loading