|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | # -*- coding: utf-8 -*- |
3 | 3 |
|
4 | | -from box import Box |
| 4 | +import opengate_core as g4 |
| 5 | +from opengate.geometry.utility import vec_g4_as_np, rot_g4_as_np |
| 6 | +from opengate.exception import fatal |
| 7 | +from opengate.utility import g4_units |
| 8 | +import yaml |
| 9 | +import uproot |
| 10 | + |
| 11 | + |
| 12 | +# --- Custom List for Inline YAML Formatting --- |
| 13 | +class FlowList(list): |
| 14 | + """A custom list that will be dumped as [x, y, z] in YAML.""" |
| 15 | + |
| 16 | + pass |
| 17 | + |
| 18 | + |
| 19 | +def flow_list_representer(dumper, data): |
| 20 | + return dumper.represent_sequence("tag:yaml.org,2002:seq", data, flow_style=True) |
| 21 | + |
| 22 | + |
| 23 | +yaml.add_representer(FlowList, flow_list_representer) |
| 24 | + |
| 25 | + |
| 26 | +def convert_to_flowlist(data): |
| 27 | + """ |
| 28 | + Recursively traverse the dictionary. |
| 29 | + Convert any list containing only simple scalars (int, float, str) to FlowList |
| 30 | + so they appear as [a, b, c] in the YAML output. |
| 31 | + """ |
| 32 | + if isinstance(data, dict): |
| 33 | + return {k: convert_to_flowlist(v) for k, v in data.items()} |
| 34 | + elif isinstance(data, list): |
| 35 | + # Check if list is "simple" (contains only primitives, no dicts or nested lists) |
| 36 | + is_simple = all( |
| 37 | + isinstance(i, (int, float, str, bool)) or i is None for i in data |
| 38 | + ) |
| 39 | + |
| 40 | + if is_simple: |
| 41 | + return FlowList(data) |
| 42 | + else: |
| 43 | + # If list contains complex objects, process them recursively but keep the list as is |
| 44 | + return [convert_to_flowlist(i) for i in data] |
| 45 | + else: |
| 46 | + return data |
5 | 47 |
|
6 | 48 |
|
7 | 49 | def coresi_new_config(): |
8 | | - config = Box( |
9 | | - { |
10 | | - "data_file": "coinc.dat", |
11 | | - "data_type": "GATE", |
12 | | - "n_events": 0, |
13 | | - "starts_at": 0, |
14 | | - "E0": [], |
15 | | - "remove_out_of_range_energies": False, |
16 | | - "energy_range": [120, 150], |
17 | | - "energy_threshold": 5, |
18 | | - "log_dir": None, |
19 | | - "cameras": {"n_cameras": 0}, |
20 | | - "volume": { |
21 | | - "volume_dimensions": [10, 10, 10], # in cm |
22 | | - "n_voxels": [50, 50, 1], |
23 | | - "volume_centre": [0, 0, 0], |
| 50 | + config = { |
| 51 | + "data_file": "coinc.dat", |
| 52 | + "data_type": "GATE", |
| 53 | + "n_events": 0, |
| 54 | + "starts_at": 0, |
| 55 | + "E0": [], |
| 56 | + "remove_out_of_range_energies": False, |
| 57 | + "energy_range": [120, 150], |
| 58 | + "energy_threshold": 5, |
| 59 | + "log_dir": None, |
| 60 | + "cameras": { |
| 61 | + "n_cameras": 0, |
| 62 | + "common_attributes": { |
| 63 | + "n_sca_layers": 0, |
| 64 | + "sca_material": "Si", |
| 65 | + "abs_material": "Si", |
| 66 | + "n_absorbers": 0, |
24 | 67 | }, |
25 | | - "lm_mlem": { |
26 | | - "cone_thickness": "angular", |
27 | | - "model": "cos1rho2", |
28 | | - "last_iter": 0, |
29 | | - "first_iter": 0, |
30 | | - "n_sigma": 2, |
31 | | - "width_factor": 1, |
32 | | - "checkpoint_dir": "checkpoints", |
33 | | - "save_every": 76, |
34 | | - "sensitivity": False, |
35 | | - "sensitivity_model": "like_system_matrix", |
36 | | - "sensitivity_point_samples": 1, |
| 68 | + "position_0": { |
| 69 | + "frame_origin": [0, 0, 0], |
| 70 | + "Ox": [1, 0, 0], # parallel to scatterer edge |
| 71 | + "Oy": [0, 1, 0], # parallel to scatterer edge |
| 72 | + "Oz": [0, 0, 1], # orthogonal to the camera, tw the source" |
37 | 73 | }, |
38 | | - } |
39 | | - ) |
| 74 | + }, |
| 75 | + "volume": { |
| 76 | + "volume_dimensions": [10, 10, 10], # in cm? |
| 77 | + "n_voxels": [50, 50, 1], # in voxels |
| 78 | + "volume_centre": [0, 0, 0], # in cm? |
| 79 | + }, |
| 80 | + "lm_mlem": { |
| 81 | + "cone_thickness": "angular", |
| 82 | + "model": "cos1rho2", |
| 83 | + "last_iter": 0, |
| 84 | + "first_iter": 0, |
| 85 | + "n_sigma": 2, |
| 86 | + "width_factor": 1, |
| 87 | + "checkpoint_dir": "checkpoints", |
| 88 | + "save_every": 76, |
| 89 | + "sensitivity": False, |
| 90 | + "sensitivity_model": "like_system_matrix", |
| 91 | + "sensitivity_point_samples": 1, |
| 92 | + }, |
| 93 | + } |
| 94 | + |
40 | 95 | return config |
| 96 | + |
| 97 | + |
| 98 | +def set_hook_coresi_config(sim, cameras, filename): |
| 99 | + """ |
| 100 | + Prepare everything to create the coresi config file at the init of the simulation. |
| 101 | + The param structure allows retrieving the coresi config at the end of the simulation. |
| 102 | + """ |
| 103 | + # create the param structure |
| 104 | + param = { |
| 105 | + "cameras": cameras, |
| 106 | + "filename": filename, |
| 107 | + "coresi_config": coresi_new_config(), |
| 108 | + } |
| 109 | + sim.user_hook_after_init = create_coresi_config |
| 110 | + sim.user_hook_after_init_arg = param |
| 111 | + return param |
| 112 | + |
| 113 | + |
| 114 | +def create_coresi_config(simulation_engine, param): |
| 115 | + # (note: simulation_engine is not used here but must be the first param) |
| 116 | + coresi_config = param["coresi_config"] |
| 117 | + cameras = param["cameras"] |
| 118 | + |
| 119 | + for camera in cameras.values(): |
| 120 | + c = coresi_config["cameras"] |
| 121 | + c["n_cameras"] += 1 |
| 122 | + scatter_layer_names = camera["scatter_layer_names"] |
| 123 | + absorber_layer_names = camera["absorber_layer_names"] |
| 124 | + |
| 125 | + for layer_name in scatter_layer_names: |
| 126 | + coresi_add_scatterer(coresi_config, layer_name) |
| 127 | + for layer_name in absorber_layer_names: |
| 128 | + coresi_add_absorber(coresi_config, layer_name) |
| 129 | + |
| 130 | + |
| 131 | +def coresi_add_scatterer(coresi_config, layer_name): |
| 132 | + # find all volumes ('touchable' in Geant4 terminology) |
| 133 | + touchables = g4.FindAllTouchables(layer_name) |
| 134 | + if len(touchables) != 1: |
| 135 | + fatal(f"Cannot find unique volume for layer {layer_name}: {touchables}") |
| 136 | + touchable = touchables[0] |
| 137 | + |
| 138 | + # current nb of scatterers |
| 139 | + id = coresi_config["cameras"]["common_attributes"]["n_sca_layers"] |
| 140 | + coresi_config["cameras"]["common_attributes"]["n_sca_layers"] += 1 |
| 141 | + layer = { |
| 142 | + "center": [0, 0, 0], |
| 143 | + "size": [0, 0, 0], |
| 144 | + } |
| 145 | + coresi_config["cameras"]["common_attributes"][f"sca_layer_{id}"] = layer |
| 146 | + |
| 147 | + # Get the information: WARNING in cm! |
| 148 | + cm = g4_units.cm |
| 149 | + translation = vec_g4_as_np(touchable.GetTranslation(0)) / cm |
| 150 | + solid = touchable.GetSolid(0) |
| 151 | + pMin_local = g4.G4ThreeVector() |
| 152 | + pMax_local = g4.G4ThreeVector() |
| 153 | + solid.BoundingLimits(pMin_local, pMax_local) |
| 154 | + size = [ |
| 155 | + (pMax_local.x - pMin_local.x) / cm, |
| 156 | + (pMax_local.y - pMin_local.y) / cm, |
| 157 | + (pMax_local.z - pMin_local.z) / cm, |
| 158 | + ] |
| 159 | + layer["center"] = translation.tolist() |
| 160 | + layer["size"] = size |
| 161 | + |
| 162 | + |
| 163 | +def coresi_add_absorber(coresi_config, layer_name): |
| 164 | + # find all volumes ('touchable' in Geant4 terminology) |
| 165 | + touchables = g4.FindAllTouchables(layer_name) |
| 166 | + if len(touchables) != 1: |
| 167 | + fatal(f"Cannot find unique volume for layer {layer_name}: {touchables}") |
| 168 | + touchable = touchables[0] |
| 169 | + |
| 170 | + # current nb of scatterers |
| 171 | + id = coresi_config["cameras"]["common_attributes"]["n_absorbers"] |
| 172 | + coresi_config["cameras"]["common_attributes"]["n_absorbers"] += 1 |
| 173 | + layer = { |
| 174 | + "center": [0, 0, 0], |
| 175 | + "size": [0, 0, 0], |
| 176 | + } |
| 177 | + coresi_config["cameras"]["common_attributes"][f"abs_layer_{id}"] = layer |
| 178 | + |
| 179 | + # Get the information: WARNING in cm! |
| 180 | + cm = g4_units.cm |
| 181 | + translation = vec_g4_as_np(touchable.GetTranslation(0)) / cm |
| 182 | + solid = touchable.GetSolid(0) |
| 183 | + pMin_local = g4.G4ThreeVector() |
| 184 | + pMax_local = g4.G4ThreeVector() |
| 185 | + solid.BoundingLimits(pMin_local, pMax_local) |
| 186 | + size = [ |
| 187 | + (pMax_local.x - pMin_local.x) / cm, |
| 188 | + (pMax_local.y - pMin_local.y) / cm, |
| 189 | + (pMax_local.z - pMin_local.z) / cm, |
| 190 | + ] |
| 191 | + layer["center"] = translation.tolist() |
| 192 | + layer["size"] = size |
| 193 | + |
| 194 | + |
| 195 | +def coresi_write_config(coresi_config, filename): |
| 196 | + # Convert vectors to FlowList just before writing |
| 197 | + formatted_config = convert_to_flowlist(coresi_config) |
| 198 | + |
| 199 | + with open(filename, "w") as f: |
| 200 | + yaml.dump( |
| 201 | + formatted_config, f, default_flow_style=False, sort_keys=False, indent=2 |
| 202 | + ) |
| 203 | + |
| 204 | + |
| 205 | +def coresi_convert_root_data(root_filename, branch_name, output_filename): |
| 206 | + root_file = uproot.open(root_filename) |
| 207 | + tree = root_file[branch_name] |
| 208 | + print("todo") |
0 commit comments