simind-python-connector 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simind_python_connector/__init__.py +72 -0
- simind_python_connector/backends/__init__.py +480 -0
- simind_python_connector/backends/base.py +387 -0
- simind_python_connector/backends/sirf_backend.py +309 -0
- simind_python_connector/backends/stir_backend.py +395 -0
- simind_python_connector/builders/__init__.py +19 -0
- simind_python_connector/builders/acquisition_builder.py +526 -0
- simind_python_connector/builders/image_builder.py +217 -0
- simind_python_connector/configs/AnyScan.yaml +420 -0
- simind_python_connector/configs/Discovery670.yaml +412 -0
- simind_python_connector/configs/Example.yaml +420 -0
- simind_python_connector/configs/MLD001_SCAN0.yaml +426 -0
- simind_python_connector/configs/__init__.py +41 -0
- simind_python_connector/configs/input.smc +51 -0
- simind_python_connector/connectors/__init__.py +24 -0
- simind_python_connector/connectors/_spacing.py +69 -0
- simind_python_connector/connectors/base.py +40 -0
- simind_python_connector/connectors/python_connector.py +355 -0
- simind_python_connector/connectors/pytomography_adaptor.py +263 -0
- simind_python_connector/connectors/sirf_adaptor.py +164 -0
- simind_python_connector/connectors/stir_adaptor.py +164 -0
- simind_python_connector/converters/__init__.py +16 -0
- simind_python_connector/converters/attenuation.py +367 -0
- simind_python_connector/converters/dicom_to_stir.py +3 -0
- simind_python_connector/converters/simind_to_stir.py +769 -0
- simind_python_connector/core/__init__.py +7 -0
- simind_python_connector/core/config.py +939 -0
- simind_python_connector/core/executor.py +96 -0
- simind_python_connector/core/types.py +203 -0
- simind_python_connector/data/Schneider2000.json +222 -0
- simind_python_connector/data/__init__.py +25 -0
- simind_python_connector/data/bone.atn +187 -0
- simind_python_connector/data/h2o.atn +92 -0
- simind_python_connector/utils/__init__.py +120 -0
- simind_python_connector/utils/backend_access.py +121 -0
- simind_python_connector/utils/import_helpers.py +74 -0
- simind_python_connector/utils/interfile_numpy.py +195 -0
- simind_python_connector/utils/interfile_parser.py +175 -0
- simind_python_connector/utils/io_utils.py +14 -0
- simind_python_connector/utils/simind_utils.py +70 -0
- simind_python_connector/utils/sirf_stir_utils.py +194 -0
- simind_python_connector/utils/stir_utils.py +485 -0
- simind_python_connector-1.0.0.dist-info/METADATA +274 -0
- simind_python_connector-1.0.0.dist-info/RECORD +47 -0
- simind_python_connector-1.0.0.dist-info/WHEEL +5 -0
- simind_python_connector-1.0.0.dist-info/licenses/LICENSE +195 -0
- simind_python_connector-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
# This file contains a wrapper to access, edit and save simulation configuration
|
|
2
|
+
# files for the Simind Monte Carlo simulation software.
|
|
3
|
+
# It can work as a standalone to make the .smc files accessible and editable in
|
|
4
|
+
# a more user-friendly way.
|
|
5
|
+
# Or you can use it with the Simulator class to run the simulation with SIRF
|
|
6
|
+
# in python.
|
|
7
|
+
|
|
8
|
+
### Author: Sam Porter
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SimulationConfig:
|
|
17
|
+
"""
|
|
18
|
+
SimulationConfig Class with YAML import/export capabilities
|
|
19
|
+
|
|
20
|
+
This class is designed to parse, manipulate, and save simulation configuration
|
|
21
|
+
files. It provides easy access
|
|
22
|
+
to configuration parameters, including index-based data, simulation flags,
|
|
23
|
+
text variables, and associated data files.
|
|
24
|
+
Now includes YAML export/import for better visualization and editing.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
filepath (str): Path to the simulation configuration file.
|
|
28
|
+
index_dict (dict): Dictionary mapping indices to parameter names for basic
|
|
29
|
+
change data.
|
|
30
|
+
flag_dict (dict): Dictionary mapping indices to simulation flags.
|
|
31
|
+
data_file_dict (dict): Dictionary mapping indices to data file descriptions.
|
|
32
|
+
data (list): List of basic change data values.
|
|
33
|
+
flags (str): String representing simulation flags as 'T' (True) or 'F' (False).
|
|
34
|
+
text_variables (dict): Dictionary of text variables.
|
|
35
|
+
data_files (dict): Dictionary of associated data files.
|
|
36
|
+
comment (str): Comment section from the configuration file.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, filepath):
|
|
40
|
+
"""
|
|
41
|
+
Initialize the SimulationConfig instance.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
filepath (str): Path to the simulation configuration file.
|
|
45
|
+
"""
|
|
46
|
+
self.filepath = filepath
|
|
47
|
+
self.index_dict = {
|
|
48
|
+
1: "photon_energy",
|
|
49
|
+
2: "source_half_length",
|
|
50
|
+
3: "source_half_width",
|
|
51
|
+
4: "source_half_height",
|
|
52
|
+
5: "phantom_half_length",
|
|
53
|
+
6: "phantom_half_width",
|
|
54
|
+
7: "phantom_half_height",
|
|
55
|
+
8: "crystal_half_length_radius",
|
|
56
|
+
9: "crystal_thickness",
|
|
57
|
+
10: "crystal_half_width",
|
|
58
|
+
11: "backscattering_material_thickness",
|
|
59
|
+
12: "height_to_detector_surface",
|
|
60
|
+
13: "cover_thickness",
|
|
61
|
+
14: "phantom_type",
|
|
62
|
+
15: "source_type",
|
|
63
|
+
16: "shift_source_x",
|
|
64
|
+
17: "shift_source_y",
|
|
65
|
+
18: "shift_source_z",
|
|
66
|
+
19: "photon_direction",
|
|
67
|
+
20: "upper_window_threshold",
|
|
68
|
+
21: "lower_window_threshold",
|
|
69
|
+
22: "energy_resolution",
|
|
70
|
+
23: "intrinsic_resolution",
|
|
71
|
+
24: "emitted_photons_per_decay",
|
|
72
|
+
25: "source_activity",
|
|
73
|
+
26: "number_photon_histories",
|
|
74
|
+
27: "kev_per_channel",
|
|
75
|
+
28: "pixel_size_simulated_image",
|
|
76
|
+
29: "spect_no_projections",
|
|
77
|
+
30: "spect_rotation",
|
|
78
|
+
31: "pixel_size_density_images",
|
|
79
|
+
32: "orientation_density_images",
|
|
80
|
+
33: "first_image_density_images",
|
|
81
|
+
34: "number_density_images",
|
|
82
|
+
35: "density_limit_border",
|
|
83
|
+
36: "shift_density_images_y",
|
|
84
|
+
37: "shift_density_images_z",
|
|
85
|
+
38: "step_size_photon_path_simulation",
|
|
86
|
+
39: "shift_density_images_x",
|
|
87
|
+
40: "density_threshold_soft_bone",
|
|
88
|
+
41: "spect_starting_angle",
|
|
89
|
+
42: "spect_orbital_rotation_fraction",
|
|
90
|
+
43: "camera_offset_x",
|
|
91
|
+
44: "camera_offset_y",
|
|
92
|
+
45: "code_definitions_zubal_phantom",
|
|
93
|
+
46: "hole_size_x",
|
|
94
|
+
47: "hole_size_y",
|
|
95
|
+
48: "distance_between_holes_x",
|
|
96
|
+
49: "distance_between_holes_y",
|
|
97
|
+
50: "shift_center_hole_x",
|
|
98
|
+
51: "shift_center_hole_y",
|
|
99
|
+
52: "collimator_thickness",
|
|
100
|
+
53: "collimator_routine",
|
|
101
|
+
54: "hole_shape",
|
|
102
|
+
55: "type",
|
|
103
|
+
56: "distance_collimator_detector",
|
|
104
|
+
57: "unused_parameter_1",
|
|
105
|
+
58: "unused_parameter_2",
|
|
106
|
+
59: "random_collimator_movement",
|
|
107
|
+
60: "unused_parameter_3",
|
|
108
|
+
76: "matrix_size_image_i",
|
|
109
|
+
77: "matrix_size_image_j",
|
|
110
|
+
78: "matrix_size_density_map_i",
|
|
111
|
+
79: "matrix_size_source_map_i",
|
|
112
|
+
80: "energy_spectra_channels",
|
|
113
|
+
81: "matrix_size_density_map_j",
|
|
114
|
+
82: "matrix_size_source_map_j",
|
|
115
|
+
83: "cutoff_energy_terminate_photon_history",
|
|
116
|
+
84: "scoring_routine",
|
|
117
|
+
85: "csv_file_content",
|
|
118
|
+
91: "voltage",
|
|
119
|
+
92: "mobility_life_electrons",
|
|
120
|
+
93: "mobility_life_holes",
|
|
121
|
+
94: "contact_pad_size",
|
|
122
|
+
95: "anode_element_pitch",
|
|
123
|
+
96: "exponential_decay_constant_tau",
|
|
124
|
+
97: "components_hecht_formula",
|
|
125
|
+
98: "energy_resolution_model",
|
|
126
|
+
99: "cloud_mobility",
|
|
127
|
+
100: "detector_array_size_i",
|
|
128
|
+
101: "detector_array_size_j",
|
|
129
|
+
}
|
|
130
|
+
self.flag_dict = {
|
|
131
|
+
1: "write_results_to_screen",
|
|
132
|
+
2: "write_images_to_files",
|
|
133
|
+
3: "write_pulse_height_distribution_to_file",
|
|
134
|
+
4: "include_collimator",
|
|
135
|
+
5: "simulate_spect_study",
|
|
136
|
+
6: "include_characteristic_xray_emissions",
|
|
137
|
+
7: "include_backscattering_material",
|
|
138
|
+
8: "use_random_seed_value",
|
|
139
|
+
9: "currently_not_in_use",
|
|
140
|
+
10: "include_interactions_in_cover",
|
|
141
|
+
11: "include_interactions_in_phantom",
|
|
142
|
+
12: "include_energy_resolution_in_crystal",
|
|
143
|
+
13: "include_forced_interactions_in_crystal",
|
|
144
|
+
14: "write_interfile_header_files",
|
|
145
|
+
15: "save_aligned_phantom_images",
|
|
146
|
+
}
|
|
147
|
+
self.data_file_dict = {
|
|
148
|
+
1: "phantom_soft_tissue",
|
|
149
|
+
2: "phantom_bone",
|
|
150
|
+
3: "cover_material",
|
|
151
|
+
4: "crystal_material",
|
|
152
|
+
5: "image_file_phantom",
|
|
153
|
+
6: "image_file_source",
|
|
154
|
+
7: "backscatter_material",
|
|
155
|
+
8: "energy_resolution_file",
|
|
156
|
+
9: "unknown_file_1",
|
|
157
|
+
10: "unknown_file_2",
|
|
158
|
+
11: "unknown_file_3",
|
|
159
|
+
12: "unknown_file_4",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Create organized parameter groups for better YAML structure
|
|
163
|
+
self.parameter_groups = {
|
|
164
|
+
"source": [1, 2, 3, 4, 15, 16, 17, 18, 19, 24, 25, 26, 79, 82],
|
|
165
|
+
"phantom": [5, 6, 7, 14, 31, 32, 33, 34, 35, 36, 37, 39, 40, 45, 78, 81],
|
|
166
|
+
"detector_crystal": [
|
|
167
|
+
8,
|
|
168
|
+
9,
|
|
169
|
+
10,
|
|
170
|
+
12,
|
|
171
|
+
13,
|
|
172
|
+
22,
|
|
173
|
+
23,
|
|
174
|
+
91,
|
|
175
|
+
92,
|
|
176
|
+
93,
|
|
177
|
+
94,
|
|
178
|
+
95,
|
|
179
|
+
96,
|
|
180
|
+
97,
|
|
181
|
+
98,
|
|
182
|
+
99,
|
|
183
|
+
100,
|
|
184
|
+
101,
|
|
185
|
+
],
|
|
186
|
+
"collimator": [46, 47, 48, 49, 50, 51, 52, 53, 54, 56, 59],
|
|
187
|
+
"energy_analysis": [20, 21, 27, 80, 83],
|
|
188
|
+
"spect_imaging": [28, 29, 30, 41, 42, 43, 44, 76, 77],
|
|
189
|
+
"simulation_control": [38, 55, 84, 85],
|
|
190
|
+
"unused_parameters": [57, 58, 60],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
self.data = None
|
|
194
|
+
self.flags = None
|
|
195
|
+
self.text_variables = {}
|
|
196
|
+
self.data_files = {}
|
|
197
|
+
self.comment = None
|
|
198
|
+
|
|
199
|
+
print(filepath)
|
|
200
|
+
|
|
201
|
+
# Detect file type and load accordingly
|
|
202
|
+
if str(filepath).endswith(".yaml") or str(filepath).endswith(".yml"):
|
|
203
|
+
# Initialize with default values first
|
|
204
|
+
self._initialise_yaml_defaults()
|
|
205
|
+
self.import_yaml(filepath)
|
|
206
|
+
else:
|
|
207
|
+
# Assume .smc format
|
|
208
|
+
self.import_smc(filepath)
|
|
209
|
+
|
|
210
|
+
def _initialise_yaml_defaults(self):
|
|
211
|
+
"""Initialize with default values for YAML loading."""
|
|
212
|
+
# Set up defaults for when loading from YAML
|
|
213
|
+
self.data = [0.0] * 101 # Initialize with 101 zeros
|
|
214
|
+
self.flags = "F" * 15 # Initialize with 15 False flags
|
|
215
|
+
self.text_variables = {i: "none" for i in range(1, 13)}
|
|
216
|
+
self.data_files = {}
|
|
217
|
+
self.comment = "Loaded from YAML"
|
|
218
|
+
|
|
219
|
+
def _initialise_sms_defaults(self):
|
|
220
|
+
"""Initialize with default values for SMC loading."""
|
|
221
|
+
self.comment = "Loaded from SMC"
|
|
222
|
+
|
|
223
|
+
def import_smc(self, filepath):
|
|
224
|
+
"""
|
|
225
|
+
Parse the simulation configuration file and populate attributes.
|
|
226
|
+
"""
|
|
227
|
+
with open(filepath, "r") as file:
|
|
228
|
+
lines = file.readlines()
|
|
229
|
+
self.comment = lines[1].strip()
|
|
230
|
+
|
|
231
|
+
# Parsing Basic Change data
|
|
232
|
+
data_lines = lines[3:27]
|
|
233
|
+
data_string = " ".join(data_lines).replace("\n", "")
|
|
234
|
+
self.data = [
|
|
235
|
+
float(val) for val in re.findall(r"-?\d+\.\d+E[+-]\d+", data_string)
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
# Parsing Simulation flags
|
|
239
|
+
self.flags = lines[28].strip().replace(" ", "")
|
|
240
|
+
|
|
241
|
+
# Parsing Text Variables
|
|
242
|
+
text_variables_start = 29
|
|
243
|
+
text_variables_count = int(lines[text_variables_start].split()[0])
|
|
244
|
+
text_variables_lines = lines[
|
|
245
|
+
text_variables_start + 1 : text_variables_start
|
|
246
|
+
+ 1
|
|
247
|
+
+ text_variables_count
|
|
248
|
+
]
|
|
249
|
+
self.text_variables = {
|
|
250
|
+
i + 1: text_variables_lines[i].strip()
|
|
251
|
+
for i in range(text_variables_count)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# Parsing Data files
|
|
255
|
+
data_files_start = 38
|
|
256
|
+
data_files_count = int(lines[data_files_start].split()[0])
|
|
257
|
+
data_files_lines = lines[
|
|
258
|
+
data_files_start + 1 : data_files_start + 1 + data_files_count
|
|
259
|
+
]
|
|
260
|
+
self.data_files = {
|
|
261
|
+
i + 1: data_files_lines[i].strip()
|
|
262
|
+
for i in range(data_files_count) # Start from 1, not 7
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
def to_yaml_dict(self):
|
|
266
|
+
"""
|
|
267
|
+
Convert the configuration to a structured dictionary suitable for YAML export.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
dict: Organized configuration data
|
|
271
|
+
"""
|
|
272
|
+
yaml_dict = {
|
|
273
|
+
"metadata": {"comment": self.comment, "source_file": str(self.filepath)},
|
|
274
|
+
"parameters": {},
|
|
275
|
+
"simulation_flags": {},
|
|
276
|
+
"text_variables": dict(self.text_variables),
|
|
277
|
+
"data_files": {},
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# Organize parameters by groups
|
|
281
|
+
for group_name, indices in self.parameter_groups.items():
|
|
282
|
+
yaml_dict["parameters"][group_name] = {}
|
|
283
|
+
for idx in indices:
|
|
284
|
+
if idx in self.index_dict and idx <= len(self.data):
|
|
285
|
+
param_name = self.index_dict[idx]
|
|
286
|
+
value = self.data[idx - 1]
|
|
287
|
+
yaml_dict["parameters"][group_name][param_name] = {
|
|
288
|
+
"index": idx,
|
|
289
|
+
"value": float(value),
|
|
290
|
+
"description": self._get_parameter_description(param_name),
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# Add simulation flags
|
|
294
|
+
for idx, flag_name in self.flag_dict.items():
|
|
295
|
+
if idx <= len(self.flags):
|
|
296
|
+
yaml_dict["simulation_flags"][flag_name] = {
|
|
297
|
+
"index": idx,
|
|
298
|
+
"enabled": self.flags[idx - 1] == "T",
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# Add data files with descriptions
|
|
302
|
+
for idx, file_desc in self.data_file_dict.items():
|
|
303
|
+
if idx in self.data_files:
|
|
304
|
+
yaml_dict["data_files"][file_desc] = {
|
|
305
|
+
"index": idx,
|
|
306
|
+
"filepath": self.data_files[idx],
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return yaml_dict
|
|
310
|
+
|
|
311
|
+
def _get_parameter_description(self, param_name):
|
|
312
|
+
"""
|
|
313
|
+
Get a human-readable description for parameters.
|
|
314
|
+
|
|
315
|
+
WARNING: Parameter descriptions are based on research of SIMIND documentation
|
|
316
|
+
and may vary between SIMIND versions. SIMIND 7.0+ introduced significant
|
|
317
|
+
parameter reorganization. Always verify against your specific SIMIND version's
|
|
318
|
+
official manual for accurate parameter definitions and valid ranges.
|
|
319
|
+
|
|
320
|
+
For definitive parameter specifications, consult:
|
|
321
|
+
- Official SIMIND manual at simind.blogg.lu.se
|
|
322
|
+
- "The SIMIND Monte Carlo Program" chapter in Monte Carlo Calculations
|
|
323
|
+
in Nuclear Medicine (CRC Press, 2012)
|
|
324
|
+
"""
|
|
325
|
+
descriptions = {
|
|
326
|
+
# Source parameters
|
|
327
|
+
"photon_energy": (
|
|
328
|
+
"Photon energy in keV (e.g., 140 for 99mTc, 208 for 177Lu)"
|
|
329
|
+
),
|
|
330
|
+
"source_half_length": "Source half-length in cm",
|
|
331
|
+
"source_half_width": "Source half-width in cm",
|
|
332
|
+
"source_half_height": "Source half-height in cm",
|
|
333
|
+
"source_type": (
|
|
334
|
+
"Source type code (0=sphere, 1=cylinder, etc.) - check SIMIND manual"
|
|
335
|
+
),
|
|
336
|
+
"shift_source_x": "Shift of source in x-direction (cm)",
|
|
337
|
+
"shift_source_y": "Shift of source in y-direction (cm)",
|
|
338
|
+
"shift_source_z": "Shift of source in z-direction (cm)",
|
|
339
|
+
"photon_direction": "Photon direction code (2=isotropic typical)",
|
|
340
|
+
"emitted_photons_per_decay": "Number of photons emitted per decay",
|
|
341
|
+
"source_activity": "Source activity in MBq",
|
|
342
|
+
"number_photon_histories": (
|
|
343
|
+
"Number of photon histories to simulate (10^6 typical minimum)"
|
|
344
|
+
),
|
|
345
|
+
"matrix_size_source_map_i": (
|
|
346
|
+
"Matrix size for source map (i-direction) - 128x128 standard"
|
|
347
|
+
),
|
|
348
|
+
"matrix_size_source_map_j": (
|
|
349
|
+
"Matrix size for source map (j-direction) - 128x128 standard"
|
|
350
|
+
),
|
|
351
|
+
# Phantom parameters
|
|
352
|
+
"phantom_half_length": "Phantom half-length in cm",
|
|
353
|
+
"phantom_half_width": "Phantom half-width in cm",
|
|
354
|
+
"phantom_half_height": "Phantom half-height in cm",
|
|
355
|
+
"phantom_type": "Phantom type code",
|
|
356
|
+
"pixel_size_density_images": "Pixel size for density images (cm)",
|
|
357
|
+
"orientation_density_images": "Orientation of density images",
|
|
358
|
+
"first_image_density_images": "First image number for density images",
|
|
359
|
+
"number_density_images": "Number of density images",
|
|
360
|
+
"density_limit_border": "Density limit at border",
|
|
361
|
+
"shift_density_images_x": "Shift of density images in x-direction (cm)",
|
|
362
|
+
"shift_density_images_y": "Shift of density images in y-direction (cm)",
|
|
363
|
+
"shift_density_images_z": "Shift of density images in z-direction (cm)",
|
|
364
|
+
"density_threshold_soft_bone": "Density threshold for soft bone",
|
|
365
|
+
"code_definitions_zubal_phantom": "Code definitions for Zubal phantom",
|
|
366
|
+
"matrix_size_density_map_i": "Matrix size for density map (i-direction)",
|
|
367
|
+
"matrix_size_density_map_j": "Matrix size for density map (j-direction)",
|
|
368
|
+
# Detector/Crystal parameters
|
|
369
|
+
"crystal_half_length_radius": (
|
|
370
|
+
"Crystal half-length/radius in cm (circular detectors use radius)"
|
|
371
|
+
),
|
|
372
|
+
"crystal_thickness": ("Crystal thickness in cm (NaI(Tl) typical: 0.95cm)"),
|
|
373
|
+
"crystal_half_width": (
|
|
374
|
+
"Crystal half-width in cm (for rectangular crystals)"
|
|
375
|
+
),
|
|
376
|
+
"height_to_detector_surface": (
|
|
377
|
+
"Height from collimator to detector surface (cm)"
|
|
378
|
+
),
|
|
379
|
+
"cover_thickness": ("Cover thickness in cm (typically Al or Be window)"),
|
|
380
|
+
"energy_resolution": (
|
|
381
|
+
"Energy resolution FWHM (%) at reference energy "
|
|
382
|
+
"(9-12% typical at 140keV)"
|
|
383
|
+
),
|
|
384
|
+
"intrinsic_resolution": (
|
|
385
|
+
"Intrinsic spatial resolution FWHM (cm) (3-4mm typical)"
|
|
386
|
+
),
|
|
387
|
+
"voltage": "Applied voltage (V) for semiconductor detectors",
|
|
388
|
+
"mobility_life_electrons": (
|
|
389
|
+
"Mobility-life product for electrons (semiconductor detectors)"
|
|
390
|
+
),
|
|
391
|
+
"mobility_life_holes": (
|
|
392
|
+
"Mobility-life product for holes (semiconductor detectors)"
|
|
393
|
+
),
|
|
394
|
+
"contact_pad_size": ("Contact pad size (cm) for pixelated detectors"),
|
|
395
|
+
"anode_element_pitch": ("Anode element pitch (cm) for pixelated detectors"),
|
|
396
|
+
"exponential_decay_constant_tau": (
|
|
397
|
+
"Exponential decay constant tau for charge collection"
|
|
398
|
+
),
|
|
399
|
+
"components_hecht_formula": (
|
|
400
|
+
"Components for Hecht formula (charge collection efficiency)"
|
|
401
|
+
),
|
|
402
|
+
"energy_resolution_model": (
|
|
403
|
+
"Energy resolution model code (check SIMIND manual for options)"
|
|
404
|
+
),
|
|
405
|
+
"cloud_mobility": "Cloud mobility parameter for charge collection",
|
|
406
|
+
"detector_array_size_i": (
|
|
407
|
+
"Detector array size (i-direction) for pixelated systems"
|
|
408
|
+
),
|
|
409
|
+
"detector_array_size_j": (
|
|
410
|
+
"Detector array size (j-direction) for pixelated systems"
|
|
411
|
+
),
|
|
412
|
+
# Collimator parameters
|
|
413
|
+
"hole_size_x": (
|
|
414
|
+
"Collimator hole diameter (cm) - LEHR: 0.111cm, HEGP: 0.24cm"
|
|
415
|
+
),
|
|
416
|
+
"hole_size_y": (
|
|
417
|
+
"Collimator hole diameter (cm) - should match hole_size_x for "
|
|
418
|
+
"round holes"
|
|
419
|
+
),
|
|
420
|
+
"distance_between_holes_x": (
|
|
421
|
+
"Distance between hole centers (cm) - LEHR: 0.16cm septal thickness"
|
|
422
|
+
),
|
|
423
|
+
"distance_between_holes_y": (
|
|
424
|
+
"Distance between hole centers (cm) - hexagonal pattern spacing"
|
|
425
|
+
),
|
|
426
|
+
"shift_center_hole_x": (
|
|
427
|
+
"Shift of center hole in x-direction (cm) for alignment"
|
|
428
|
+
),
|
|
429
|
+
"shift_center_hole_y": (
|
|
430
|
+
"Shift of center hole in y-direction (cm) for alignment"
|
|
431
|
+
),
|
|
432
|
+
"collimator_thickness": (
|
|
433
|
+
"Collimator thickness (cm) - LEHR: 2.405cm, HEGP: 5.9cm"
|
|
434
|
+
),
|
|
435
|
+
"collimator_routine": (
|
|
436
|
+
"Collimator routine code (0=no collimator, 1=parallel holes, etc.)"
|
|
437
|
+
),
|
|
438
|
+
"hole_shape": "Hole shape code (0=round, 1=square, 2=hexagonal)",
|
|
439
|
+
"distance_collimator_detector": (
|
|
440
|
+
"Distance from collimator face to detector surface (cm)"
|
|
441
|
+
),
|
|
442
|
+
"random_collimator_movement": (
|
|
443
|
+
"Random collimator movement parameter (for manufacturing variations)"
|
|
444
|
+
),
|
|
445
|
+
# Energy analysis parameters
|
|
446
|
+
"upper_window_threshold": (
|
|
447
|
+
"Upper energy window threshold (keV) - set to -100 for automatic"
|
|
448
|
+
),
|
|
449
|
+
"lower_window_threshold": (
|
|
450
|
+
"Lower energy window threshold (keV) - set to -100 for automatic"
|
|
451
|
+
),
|
|
452
|
+
"kev_per_channel": "keV per channel for energy spectrum binning",
|
|
453
|
+
"energy_spectra_channels": (
|
|
454
|
+
"Number of energy spectra channels (512 typical)"
|
|
455
|
+
),
|
|
456
|
+
"cutoff_energy_terminate_photon_history": (
|
|
457
|
+
"Cutoff energy to terminate photon history (keV)"
|
|
458
|
+
),
|
|
459
|
+
# SPECT imaging parameters
|
|
460
|
+
"pixel_size_simulated_image": (
|
|
461
|
+
"Pixel size for simulated images (cm) - affects resolution vs FOV"
|
|
462
|
+
),
|
|
463
|
+
"spect_no_projections": (
|
|
464
|
+
"Number of SPECT projections (64, 120, 128 typical)"
|
|
465
|
+
),
|
|
466
|
+
"spect_rotation": "SPECT rotation parameter (2=360° typical)",
|
|
467
|
+
"spect_starting_angle": ("SPECT starting angle (degrees) - 0° = anterior"),
|
|
468
|
+
"spect_orbital_rotation_fraction": (
|
|
469
|
+
"SPECT orbital rotation fraction (1.0 = full orbit)"
|
|
470
|
+
),
|
|
471
|
+
"camera_offset_x": (
|
|
472
|
+
"Camera offset in x-direction (cm) from rotation center"
|
|
473
|
+
),
|
|
474
|
+
"camera_offset_y": (
|
|
475
|
+
"Camera offset in y-direction (cm) from rotation center"
|
|
476
|
+
),
|
|
477
|
+
"matrix_size_image_i": (
|
|
478
|
+
"Matrix size for images (i-direction) - 128x128 standard"
|
|
479
|
+
),
|
|
480
|
+
"matrix_size_image_j": (
|
|
481
|
+
"Matrix size for images (j-direction) - 128x128 standard"
|
|
482
|
+
),
|
|
483
|
+
# Simulation control parameters
|
|
484
|
+
"step_size_photon_path_simulation": (
|
|
485
|
+
"Step size for photon path simulation (cm) - smaller = more accurate"
|
|
486
|
+
),
|
|
487
|
+
"type": "General type parameter - check SIMIND manual for current meaning",
|
|
488
|
+
"scoring_routine": "Scoring routine code - affects output data collection",
|
|
489
|
+
"csv_file_content": "CSV file content parameter - for custom data output",
|
|
490
|
+
"backscattering_material_thickness": (
|
|
491
|
+
"Backscattering material thickness (cm)"
|
|
492
|
+
),
|
|
493
|
+
# Unused parameters - NOTE: May be used in newer SIMIND versions
|
|
494
|
+
"unused_parameter_1": "Unused parameter 1 - reserved for future use",
|
|
495
|
+
"unused_parameter_2": "Unused parameter 2 - reserved for future use",
|
|
496
|
+
"unused_parameter_3": "Unused parameter 3 - reserved for future use",
|
|
497
|
+
}
|
|
498
|
+
return descriptions.get(param_name, "No description available")
|
|
499
|
+
|
|
500
|
+
def export_yaml(self, filepath):
|
|
501
|
+
"""
|
|
502
|
+
Export the configuration to a YAML file.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
filepath (str): Path for the output YAML file
|
|
506
|
+
"""
|
|
507
|
+
yaml_dict = self.to_yaml_dict()
|
|
508
|
+
|
|
509
|
+
filepath = Path(filepath)
|
|
510
|
+
if filepath.suffix != ".yaml":
|
|
511
|
+
filepath = filepath.with_suffix(".yaml")
|
|
512
|
+
|
|
513
|
+
with open(filepath, "w") as file:
|
|
514
|
+
yaml.dump(
|
|
515
|
+
yaml_dict, file, default_flow_style=False, indent=2, sort_keys=False
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
print(f"Configuration exported to {filepath}")
|
|
519
|
+
|
|
520
|
+
def import_yaml(self, filepath):
|
|
521
|
+
"""
|
|
522
|
+
Import configuration from a YAML file.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
filepath (str): Path to the input YAML file
|
|
526
|
+
"""
|
|
527
|
+
with open(filepath, "r") as file:
|
|
528
|
+
yaml_dict = yaml.safe_load(file)
|
|
529
|
+
|
|
530
|
+
# Update comment
|
|
531
|
+
if "metadata" in yaml_dict and "comment" in yaml_dict["metadata"]:
|
|
532
|
+
self.comment = yaml_dict["metadata"]["comment"]
|
|
533
|
+
|
|
534
|
+
# Update parameters
|
|
535
|
+
if "parameters" in yaml_dict:
|
|
536
|
+
for group_name, group_params in yaml_dict["parameters"].items():
|
|
537
|
+
for param_name, param_data in group_params.items():
|
|
538
|
+
if "index" in param_data and "value" in param_data:
|
|
539
|
+
idx = param_data["index"]
|
|
540
|
+
value = param_data["value"]
|
|
541
|
+
if idx in self.index_dict and idx <= len(self.data):
|
|
542
|
+
self.data[idx - 1] = float(value)
|
|
543
|
+
|
|
544
|
+
# Update flags
|
|
545
|
+
if "simulation_flags" in yaml_dict:
|
|
546
|
+
flags = list(self.flags)
|
|
547
|
+
for flag_name, flag_data in yaml_dict["simulation_flags"].items():
|
|
548
|
+
if "index" in flag_data and "enabled" in flag_data:
|
|
549
|
+
idx = flag_data["index"]
|
|
550
|
+
enabled = flag_data["enabled"]
|
|
551
|
+
if idx in self.flag_dict and idx <= len(flags):
|
|
552
|
+
flags[idx - 1] = "T" if enabled else "F"
|
|
553
|
+
self.flags = "".join(flags)
|
|
554
|
+
|
|
555
|
+
# Update data files
|
|
556
|
+
if "data_files" in yaml_dict:
|
|
557
|
+
for file_desc, file_data in yaml_dict["data_files"].items():
|
|
558
|
+
if "index" in file_data and "filepath" in file_data:
|
|
559
|
+
idx = file_data["index"]
|
|
560
|
+
f = file_data["filepath"]
|
|
561
|
+
if idx in self.data_file_dict:
|
|
562
|
+
self.data_files[idx] = f
|
|
563
|
+
|
|
564
|
+
# Update text variables
|
|
565
|
+
if "text_variables" in yaml_dict:
|
|
566
|
+
self.text_variables = yaml_dict["text_variables"]
|
|
567
|
+
|
|
568
|
+
def validate_parameters(self):
|
|
569
|
+
"""
|
|
570
|
+
Basic parameter validation based on typical SIMIND ranges.
|
|
571
|
+
|
|
572
|
+
NOTE: This provides basic sanity checks only. Consult official SIMIND
|
|
573
|
+
documentation for complete parameter validation rules and constraints.
|
|
574
|
+
"""
|
|
575
|
+
warnings = []
|
|
576
|
+
|
|
577
|
+
# Energy validation
|
|
578
|
+
if (
|
|
579
|
+
self.get_value("photon_energy") < 10
|
|
580
|
+
or self.get_value("photon_energy") > 500
|
|
581
|
+
):
|
|
582
|
+
warnings.append("Photon energy outside typical range (10-500 keV)")
|
|
583
|
+
|
|
584
|
+
# Matrix size validation
|
|
585
|
+
matrix_i = self.get_value("matrix_size_image_i")
|
|
586
|
+
matrix_j = self.get_value("matrix_size_image_j")
|
|
587
|
+
if matrix_i != matrix_j:
|
|
588
|
+
warnings.append(
|
|
589
|
+
"Non-square matrix sizes may cause issues in some SIMIND versions"
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# Energy window validation
|
|
593
|
+
upper_window = self.get_value("upper_window_threshold")
|
|
594
|
+
lower_window = self.get_value("lower_window_threshold")
|
|
595
|
+
if upper_window > 0 and lower_window > 0 and lower_window >= upper_window:
|
|
596
|
+
warnings.append("Lower energy window >= upper energy window")
|
|
597
|
+
|
|
598
|
+
# Collimator validation
|
|
599
|
+
if self.get_flag("include_collimator"):
|
|
600
|
+
if self.get_value("collimator_thickness") <= 0:
|
|
601
|
+
warnings.append("Collimator enabled but thickness <= 0")
|
|
602
|
+
|
|
603
|
+
# Crystal validation
|
|
604
|
+
if self.get_value("crystal_thickness") <= 0:
|
|
605
|
+
warnings.append("Crystal thickness <= 0")
|
|
606
|
+
|
|
607
|
+
if warnings:
|
|
608
|
+
print("Parameter validation warnings:")
|
|
609
|
+
for warning in warnings:
|
|
610
|
+
print(f" - {warning}")
|
|
611
|
+
else:
|
|
612
|
+
print("Basic parameter validation passed")
|
|
613
|
+
|
|
614
|
+
return len(warnings) == 0
|
|
615
|
+
|
|
616
|
+
def get_simind_version_info(self):
|
|
617
|
+
"""
|
|
618
|
+
Extract version information from comment or suggest manual verification.
|
|
619
|
+
"""
|
|
620
|
+
print("SIMIND Version Detection:")
|
|
621
|
+
print(f"Comment field: '{self.comment}'")
|
|
622
|
+
print("\nIMPORTANT: Parameter meanings may vary between SIMIND versions.")
|
|
623
|
+
print("Version 7.0+ introduced significant parameter reorganization.")
|
|
624
|
+
print("Always verify parameters against your specific SIMIND version's manual.")
|
|
625
|
+
print("Official documentation: https://simind.blogg.lu.se/")
|
|
626
|
+
|
|
627
|
+
def print_config(self):
|
|
628
|
+
"""
|
|
629
|
+
Print the configuration details, including comments, basic change data,
|
|
630
|
+
flags, text variables, and data files.
|
|
631
|
+
"""
|
|
632
|
+
print(f"Comment: {self.comment}")
|
|
633
|
+
print("Basic Change data:")
|
|
634
|
+
for key, val in self.index_dict.items():
|
|
635
|
+
print(f"index {key}: {val}: {self.data[key - 1]}")
|
|
636
|
+
print("Simulation flags:")
|
|
637
|
+
for key, val in self.flag_dict.items():
|
|
638
|
+
print(f"flag {key}: {val}: {self.flags[key - 1]}")
|
|
639
|
+
print("Text Variables:")
|
|
640
|
+
for key, val in self.text_variables.items():
|
|
641
|
+
print(f"{key}: {val}")
|
|
642
|
+
print("Data Files:")
|
|
643
|
+
for key, val in self.data_files.items():
|
|
644
|
+
print(f"{key}: {val}")
|
|
645
|
+
|
|
646
|
+
def get_value(self, index):
|
|
647
|
+
"""
|
|
648
|
+
Get the value of a parameter by its index or description.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
index (int or str): Parameter index or description.
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
float: Parameter value.
|
|
655
|
+
"""
|
|
656
|
+
if isinstance(index, int) and index in self.index_dict:
|
|
657
|
+
return self.data[index - 1]
|
|
658
|
+
elif isinstance(index, str) and index in self.index_dict.values():
|
|
659
|
+
for key, val in self.index_dict.items():
|
|
660
|
+
if val == index:
|
|
661
|
+
return self.data[key - 1]
|
|
662
|
+
else:
|
|
663
|
+
raise ValueError("index must be a valid integer or string")
|
|
664
|
+
|
|
665
|
+
def set_value(self, index, value):
|
|
666
|
+
"""
|
|
667
|
+
Set the value of a parameter by its index or description.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
index (int or str): Parameter index or description.
|
|
671
|
+
value (float): New value for the parameter.
|
|
672
|
+
"""
|
|
673
|
+
if isinstance(index, int) and index in self.index_dict:
|
|
674
|
+
self.data[index - 1] = value
|
|
675
|
+
elif isinstance(index, str) and index in self.index_dict.values():
|
|
676
|
+
for key, val in self.index_dict.items():
|
|
677
|
+
if val == index:
|
|
678
|
+
self.data[key - 1] = value
|
|
679
|
+
else:
|
|
680
|
+
raise ValueError("index must be an integer or string")
|
|
681
|
+
|
|
682
|
+
def get_flag(self, index):
|
|
683
|
+
"""
|
|
684
|
+
Get the value of a simulation flag by its index or description.
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
index (int or str): Flag index or description.
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
bool: True if the flag is set, False otherwise.
|
|
691
|
+
"""
|
|
692
|
+
if isinstance(index, int) and index in self.flag_dict:
|
|
693
|
+
return self.flags[index - 1] == "T"
|
|
694
|
+
elif isinstance(index, str) and index in self.flag_dict.values():
|
|
695
|
+
for key, val in self.flag_dict.items():
|
|
696
|
+
if val == index:
|
|
697
|
+
return self.flags[key - 1] == "T"
|
|
698
|
+
else:
|
|
699
|
+
raise ValueError("index must be a valid integer or string")
|
|
700
|
+
|
|
701
|
+
def set_flag(self, index, value):
|
|
702
|
+
"""
|
|
703
|
+
Set the value of a simulation flag by its index or description.
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
index (int or str): Flag index or description.
|
|
707
|
+
value (bool): True to set the flag, False to clear it.
|
|
708
|
+
"""
|
|
709
|
+
if isinstance(index, int) and index in self.flag_dict:
|
|
710
|
+
flags = list(self.flags)
|
|
711
|
+
flags[index - 1] = "T" if value else "F"
|
|
712
|
+
self.flags = "".join(flags)
|
|
713
|
+
elif isinstance(index, str) and index in self.flag_dict.values():
|
|
714
|
+
for key, val in self.flag_dict.items():
|
|
715
|
+
if val == index:
|
|
716
|
+
flags = list(self.flags)
|
|
717
|
+
flags[key - 1] = "T" if value else "F"
|
|
718
|
+
self.flags = "".join(flags)
|
|
719
|
+
else:
|
|
720
|
+
raise ValueError("index must be an integer or string")
|
|
721
|
+
|
|
722
|
+
def set_data_file(self, index, filepath):
|
|
723
|
+
"""
|
|
724
|
+
Set the path to a data file by its index.
|
|
725
|
+
|
|
726
|
+
Args:
|
|
727
|
+
index (int or str): Data file index or description.
|
|
728
|
+
filepath (str): Path to the data file.
|
|
729
|
+
"""
|
|
730
|
+
if isinstance(index, int) and index in self.data_file_dict:
|
|
731
|
+
if index in self.data_files:
|
|
732
|
+
self.data_files[index] = filepath
|
|
733
|
+
elif isinstance(index, str) and index in self.data_file_dict.values():
|
|
734
|
+
for key, val in self.data_files.items():
|
|
735
|
+
if val == index:
|
|
736
|
+
self.data_files[key] = filepath
|
|
737
|
+
else:
|
|
738
|
+
raise ValueError("index must be an integer or string")
|
|
739
|
+
|
|
740
|
+
def get_data_file(self, index):
|
|
741
|
+
"""
|
|
742
|
+
Get the path to a data file by its index or description.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
index (int or str): Data file index or description.
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
str: Path to the data file.
|
|
749
|
+
"""
|
|
750
|
+
if isinstance(index, int) and index in self.data_file_dict:
|
|
751
|
+
return self.data_files[index]
|
|
752
|
+
elif isinstance(index, str) and index in self.data_file_dict.values():
|
|
753
|
+
for key, val in self.data_files.items():
|
|
754
|
+
if val == index:
|
|
755
|
+
return key
|
|
756
|
+
else:
|
|
757
|
+
raise ValueError("index must be an integer or string")
|
|
758
|
+
|
|
759
|
+
def get_comment(self):
|
|
760
|
+
return self.comment
|
|
761
|
+
|
|
762
|
+
def set_comment(self, comment):
|
|
763
|
+
self.comment = comment
|
|
764
|
+
|
|
765
|
+
def save_file(self, filepath):
|
|
766
|
+
# Ensure filepath is a Path object
|
|
767
|
+
filepath = Path(filepath)
|
|
768
|
+
|
|
769
|
+
# Check if the file has the correct suffix, add it if missing
|
|
770
|
+
if filepath.suffix != ".smc":
|
|
771
|
+
filepath = filepath.with_suffix(".smc")
|
|
772
|
+
|
|
773
|
+
with open(filepath, "w") as file:
|
|
774
|
+
comment = self.comment + " " * (70 - len(self.comment))
|
|
775
|
+
file.write(f"SMCV2\n{comment}\n")
|
|
776
|
+
file.write(" 120 # Basic Change data\n")
|
|
777
|
+
|
|
778
|
+
for i in range(0, 120, 5): # Force exactly 120 values
|
|
779
|
+
line = ""
|
|
780
|
+
for j in range(5):
|
|
781
|
+
if i + j < len(self.data):
|
|
782
|
+
val = self.data[i + j]
|
|
783
|
+
else:
|
|
784
|
+
val = 0.0 # Pad with zeros if needed
|
|
785
|
+
|
|
786
|
+
# Format the value in scientific notation with 5 decimal places
|
|
787
|
+
formatted_val = f"{val:.5E}"
|
|
788
|
+
if val != 0:
|
|
789
|
+
# Split the formatted value into its components: sign,
|
|
790
|
+
# digit, and exponent
|
|
791
|
+
sign = "-" if val < 0 else " "
|
|
792
|
+
parts = formatted_val.split("E")
|
|
793
|
+
digits = parts[0].replace("-", "")
|
|
794
|
+
# remove final 0
|
|
795
|
+
digits = digits[:-1]
|
|
796
|
+
exponent = int(parts[1])
|
|
797
|
+
|
|
798
|
+
# Ensure it starts with '0' after the sign
|
|
799
|
+
if "." in digits:
|
|
800
|
+
digits = digits.replace(".", "")
|
|
801
|
+
|
|
802
|
+
# Since we've moved the decimal place one position to the
|
|
803
|
+
# right, increment the exponent
|
|
804
|
+
new_exponent = exponent + 1
|
|
805
|
+
|
|
806
|
+
# Reconstruct the formatted value
|
|
807
|
+
formatted_val = f"{sign}0.{digits}E{new_exponent:+03d}"
|
|
808
|
+
else:
|
|
809
|
+
# If the value is 0, we don't need to format it
|
|
810
|
+
formatted_val = f" {val:.5E}"
|
|
811
|
+
|
|
812
|
+
# Add the formatted value to the line
|
|
813
|
+
line += f"{formatted_val}"
|
|
814
|
+
|
|
815
|
+
# Write the formatted line to the file
|
|
816
|
+
file.write(f"{line}\n")
|
|
817
|
+
|
|
818
|
+
file.write(f" 30 # Simulation flags\n{self.flags}\n")
|
|
819
|
+
file.write(f" {len(self.text_variables)} # Text Variables\n")
|
|
820
|
+
for i in range(1, len(self.text_variables) + 1):
|
|
821
|
+
file.write(f"{self.text_variables[i]}\n")
|
|
822
|
+
file.write(f" {len(self.data_files)} # Data files\n")
|
|
823
|
+
for i in range(1, 13):
|
|
824
|
+
filename = self.data_files.get(i, "none")
|
|
825
|
+
file.write(f"{filename:<60}\n")
|
|
826
|
+
|
|
827
|
+
return filepath.with_suffix(".smc")
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
class RuntimeSwitches:
|
|
831
|
+
def __init__(self):
|
|
832
|
+
self.standard_switch_dict = {
|
|
833
|
+
"CC": "Collimator code",
|
|
834
|
+
"DF": "Density file segment",
|
|
835
|
+
"ES": "Energy offset",
|
|
836
|
+
"FE": "Energy resolution file",
|
|
837
|
+
"FZ": "Zubal file",
|
|
838
|
+
"FI": "Input file",
|
|
839
|
+
"FD": "Density map base name",
|
|
840
|
+
"FS": "Source map base name",
|
|
841
|
+
"I2": "Image files stored as 16-bit integer matrices",
|
|
842
|
+
"IN": "Change simind.ini value",
|
|
843
|
+
"LO": "Photon histories before printout",
|
|
844
|
+
"LF": "Linear sampling of polar angle for photon direction",
|
|
845
|
+
"MP": "MPI parallel run",
|
|
846
|
+
"OR": "Change orientation of the density map",
|
|
847
|
+
"PR": "Start simulation at projection number",
|
|
848
|
+
"PU": "Shift of the source in pixel units",
|
|
849
|
+
"QF": "Quit simulation if earlier result file exists",
|
|
850
|
+
"RR": "Random number generator seed",
|
|
851
|
+
"SC": "Maximum number of scatter orders",
|
|
852
|
+
"SF": "Segment for source map",
|
|
853
|
+
"TS": "Time shift for interfile header",
|
|
854
|
+
"UA": "Set density equal to data buffer or 1.0",
|
|
855
|
+
"WB": "Whole-body simulation of anterior and posterior views",
|
|
856
|
+
"Xn": "Change cross sections",
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
self.image_based_switch_dict = {
|
|
860
|
+
"PX": "Pixel size of the source maps",
|
|
861
|
+
"DI": "General direction of the source map",
|
|
862
|
+
"TH": "Slice thickness for the images",
|
|
863
|
+
"SB": "Start block when reading source maps",
|
|
864
|
+
"1S": "Position of the first image to be used",
|
|
865
|
+
"NN": "Multiplier for scaling the number of counts",
|
|
866
|
+
"IF": "Input tumour file",
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
self.myocardiac_switch_dict = {
|
|
870
|
+
"A1": "Shift of the heart in the xy-direction",
|
|
871
|
+
"A2": "Shift of the heart in the yz-direction",
|
|
872
|
+
"A3": "Shift of the heart in the zx-direction",
|
|
873
|
+
"L1": "Location of defect",
|
|
874
|
+
"L2": "Angular size of the defect",
|
|
875
|
+
"L3": "Start from Base",
|
|
876
|
+
"L4": "Extent of defect in axis direction",
|
|
877
|
+
"L5": "Transgression in %",
|
|
878
|
+
"L6": "Activity ratio in defect",
|
|
879
|
+
"M1": "Thickness of the myocardial wall",
|
|
880
|
+
"M2": "Thickness of the plastic wall",
|
|
881
|
+
"M3": "Total length of the chamber",
|
|
882
|
+
"M4": "Total diameter of the chamber",
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
self.multiple_spheres_switch_dict = {
|
|
886
|
+
"C1": "Number of spheres",
|
|
887
|
+
"C2": "Radius of spheres",
|
|
888
|
+
"C3": "Activity of spheres",
|
|
889
|
+
"C4": "Shift of spheres in the x-direction",
|
|
890
|
+
"C5": "Shift of spheres in the y-direction",
|
|
891
|
+
"C6": "Shift of spheres in the z-direction",
|
|
892
|
+
}
|
|
893
|
+
self.switches = {}
|
|
894
|
+
|
|
895
|
+
self.switch_dict = {
|
|
896
|
+
"Standard": self.standard_switch_dict,
|
|
897
|
+
"Image-based": self.image_based_switch_dict,
|
|
898
|
+
"Myocardiac": self.myocardiac_switch_dict,
|
|
899
|
+
"Multiple spheres": self.multiple_spheres_switch_dict,
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
@property
|
|
903
|
+
def combined_switch_dict(self):
|
|
904
|
+
combined_dict = {}
|
|
905
|
+
for sub_dict in self.switch_dict.values():
|
|
906
|
+
combined_dict.update(sub_dict)
|
|
907
|
+
return combined_dict
|
|
908
|
+
|
|
909
|
+
def _set_switch_by_switch(self, switch, value):
|
|
910
|
+
if switch in self.combined_switch_dict:
|
|
911
|
+
self.switches[switch] = value
|
|
912
|
+
else:
|
|
913
|
+
raise ValueError(f"Switch {switch} is not recognised.")
|
|
914
|
+
|
|
915
|
+
def _set_switch_by_name(self, name, value):
|
|
916
|
+
for switch, description in self.combined_switch_dict.items():
|
|
917
|
+
if description == name:
|
|
918
|
+
self.switches[switch] = value
|
|
919
|
+
return
|
|
920
|
+
raise ValueError(f"Switch {name} is not recognised.")
|
|
921
|
+
|
|
922
|
+
def set_switch(self, identifier, value):
|
|
923
|
+
if identifier in self.combined_switch_dict.values():
|
|
924
|
+
self._set_switch_by_name(identifier, value)
|
|
925
|
+
elif identifier in self.combined_switch_dict.keys():
|
|
926
|
+
self._set_switch_by_switch(identifier, value)
|
|
927
|
+
else:
|
|
928
|
+
raise ValueError(f"Switch {identifier} is not recognised.")
|
|
929
|
+
|
|
930
|
+
def print_switches(self):
|
|
931
|
+
for switch, value in self.switches.items():
|
|
932
|
+
description = self.combined_switch_dict[switch]
|
|
933
|
+
print(f"{switch} ({description}): {value}")
|
|
934
|
+
|
|
935
|
+
def print_available_switches(self):
|
|
936
|
+
for switch_dict in self.switch_dict.values():
|
|
937
|
+
print(f"Switches for {switch_dict}:")
|
|
938
|
+
for switch, description in switch_dict.items():
|
|
939
|
+
print(f"{switch}: {description}")
|