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.
Files changed (47) hide show
  1. simind_python_connector/__init__.py +72 -0
  2. simind_python_connector/backends/__init__.py +480 -0
  3. simind_python_connector/backends/base.py +387 -0
  4. simind_python_connector/backends/sirf_backend.py +309 -0
  5. simind_python_connector/backends/stir_backend.py +395 -0
  6. simind_python_connector/builders/__init__.py +19 -0
  7. simind_python_connector/builders/acquisition_builder.py +526 -0
  8. simind_python_connector/builders/image_builder.py +217 -0
  9. simind_python_connector/configs/AnyScan.yaml +420 -0
  10. simind_python_connector/configs/Discovery670.yaml +412 -0
  11. simind_python_connector/configs/Example.yaml +420 -0
  12. simind_python_connector/configs/MLD001_SCAN0.yaml +426 -0
  13. simind_python_connector/configs/__init__.py +41 -0
  14. simind_python_connector/configs/input.smc +51 -0
  15. simind_python_connector/connectors/__init__.py +24 -0
  16. simind_python_connector/connectors/_spacing.py +69 -0
  17. simind_python_connector/connectors/base.py +40 -0
  18. simind_python_connector/connectors/python_connector.py +355 -0
  19. simind_python_connector/connectors/pytomography_adaptor.py +263 -0
  20. simind_python_connector/connectors/sirf_adaptor.py +164 -0
  21. simind_python_connector/connectors/stir_adaptor.py +164 -0
  22. simind_python_connector/converters/__init__.py +16 -0
  23. simind_python_connector/converters/attenuation.py +367 -0
  24. simind_python_connector/converters/dicom_to_stir.py +3 -0
  25. simind_python_connector/converters/simind_to_stir.py +769 -0
  26. simind_python_connector/core/__init__.py +7 -0
  27. simind_python_connector/core/config.py +939 -0
  28. simind_python_connector/core/executor.py +96 -0
  29. simind_python_connector/core/types.py +203 -0
  30. simind_python_connector/data/Schneider2000.json +222 -0
  31. simind_python_connector/data/__init__.py +25 -0
  32. simind_python_connector/data/bone.atn +187 -0
  33. simind_python_connector/data/h2o.atn +92 -0
  34. simind_python_connector/utils/__init__.py +120 -0
  35. simind_python_connector/utils/backend_access.py +121 -0
  36. simind_python_connector/utils/import_helpers.py +74 -0
  37. simind_python_connector/utils/interfile_numpy.py +195 -0
  38. simind_python_connector/utils/interfile_parser.py +175 -0
  39. simind_python_connector/utils/io_utils.py +14 -0
  40. simind_python_connector/utils/simind_utils.py +70 -0
  41. simind_python_connector/utils/sirf_stir_utils.py +194 -0
  42. simind_python_connector/utils/stir_utils.py +485 -0
  43. simind_python_connector-1.0.0.dist-info/METADATA +274 -0
  44. simind_python_connector-1.0.0.dist-info/RECORD +47 -0
  45. simind_python_connector-1.0.0.dist-info/WHEEL +5 -0
  46. simind_python_connector-1.0.0.dist-info/licenses/LICENSE +195 -0
  47. simind_python_connector-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,96 @@
1
+ """Minimal SIMIND process runner used by connector-first APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Dict, Optional
9
+
10
+ from .types import SimulationError
11
+
12
+
13
+ class SimindExecutor:
14
+ """Run SIMIND as a subprocess with optional MPI switch support."""
15
+
16
+ def __init__(self) -> None:
17
+ self.logger = logging.getLogger(__name__)
18
+
19
+ def run_simulation(
20
+ self,
21
+ output_prefix: str,
22
+ orbit_file: Optional[Path] = None,
23
+ runtime_switches: Optional[Dict] = None,
24
+ ) -> None:
25
+ mp_value = None
26
+ if runtime_switches:
27
+ for key, value in runtime_switches.items():
28
+ if str(key).lower() == "mp":
29
+ mp_value = value
30
+ break
31
+
32
+ validated_output_prefix = self._validate_cli_token(output_prefix)
33
+ validated_orbit_name = (
34
+ self._validate_cli_token(orbit_file.name) if orbit_file else None
35
+ )
36
+ validated_mp_value = (
37
+ self._validate_cli_token(mp_value) if mp_value is not None else None
38
+ )
39
+
40
+ validated_switch_blob = None
41
+ if runtime_switches:
42
+ switch_parts = []
43
+ for key, value in runtime_switches.items():
44
+ if str(key).upper() == "MP":
45
+ switch_parts.append(f"/{key}")
46
+ else:
47
+ switch_parts.append(f"/{key}:{value}")
48
+ if switch_parts:
49
+ validated_switch_blob = self._validate_cli_token("".join(switch_parts))
50
+
51
+ if validated_mp_value is not None:
52
+ command = [
53
+ "mpirun",
54
+ "-np",
55
+ validated_mp_value,
56
+ "simind",
57
+ validated_output_prefix,
58
+ validated_output_prefix,
59
+ ]
60
+ if validated_orbit_name is not None:
61
+ command.append(validated_orbit_name)
62
+ command.append("-p")
63
+ if validated_switch_blob is not None:
64
+ command.append(validated_switch_blob)
65
+ else:
66
+ command = ["simind", validated_output_prefix, validated_output_prefix]
67
+ if validated_orbit_name is not None:
68
+ command.append(validated_orbit_name)
69
+ if validated_switch_blob is not None:
70
+ command.append(validated_switch_blob)
71
+
72
+ self.logger.info("Running SIMIND: %s", " ".join(command))
73
+ try:
74
+ subprocess.run(command, check=True, shell=False)
75
+ except OSError as exc:
76
+ raise SimulationError(f"Unable to execute SIMIND command: {exc}") from exc
77
+ except subprocess.CalledProcessError as exc:
78
+ raise SimulationError(f"SIMIND execution failed: {exc}") from exc
79
+
80
+ @staticmethod
81
+ def _validate_cli_token(value: object) -> str:
82
+ """Validate command tokens before subprocess invocation.
83
+
84
+ This executor always runs with ``shell=False``, and each token is
85
+ validated to reject empty values, NUL bytes, and whitespace.
86
+ """
87
+ token = str(value)
88
+ if not token:
89
+ raise SimulationError("Encountered empty command token for SIMIND call.")
90
+ if "\x00" in token:
91
+ raise SimulationError("SIMIND command token contains NUL byte.")
92
+ if any(char.isspace() for char in token):
93
+ raise SimulationError(
94
+ f"SIMIND command token contains whitespace: {token!r}"
95
+ )
96
+ return token
@@ -0,0 +1,203 @@
1
+ """
2
+ Type definitions, enums, exceptions, and constants for SIMIND simulation.
3
+
4
+ This module contains types that don't depend on SIRF, allowing them to be
5
+ imported without triggering SIRF dependencies.
6
+ """
7
+
8
+ from enum import Enum
9
+
10
+
11
+ # =============================================================================
12
+ # EXCEPTIONS
13
+ # =============================================================================
14
+
15
+
16
+ class SimindError(Exception):
17
+ """Base exception for SIMIND simulation errors."""
18
+
19
+
20
+ class ValidationError(SimindError):
21
+ """Raised when validation fails."""
22
+
23
+
24
+ class SimulationError(SimindError):
25
+ """Raised when simulation execution fails."""
26
+
27
+
28
+ class OutputError(SimindError):
29
+ """Raised when output processing fails."""
30
+
31
+
32
+ # =============================================================================
33
+ # ENUMS
34
+ # =============================================================================
35
+
36
+
37
+ class RotationDirection(Enum):
38
+ """Rotation direction for SPECT acquisition."""
39
+
40
+ CCW = "ccw"
41
+ CW = "cw"
42
+
43
+
44
+ class ScatterType(Enum):
45
+ """SIMIND scatter output types."""
46
+
47
+ TOTAL = "tot"
48
+ SCATTER = "sca"
49
+ PRIMARY = "pri"
50
+ AIR = "air"
51
+
52
+
53
+ class ScoringRoutine(Enum):
54
+ """Enum for different SIMIND scoring routines."""
55
+
56
+ DUMMY = 0
57
+ SCATTWIN = 1
58
+ LIST_MODE = 2
59
+ FORCED_COLLIMATION = 3
60
+ PENETRATE = 4
61
+
62
+
63
+ class PenetrateOutputType(Enum):
64
+ """Enum for different penetrate routine output components."""
65
+
66
+ def __new__(cls, value: int, slug: str, description: str):
67
+ obj = object.__new__(cls)
68
+ obj._value_ = value
69
+ obj.slug = slug
70
+ obj.description = description
71
+ return obj
72
+
73
+ # Without backscatter (*.b01-*.b09)
74
+ ALL_INTERACTIONS = (
75
+ 1,
76
+ "all_interactions",
77
+ "All type of interactions",
78
+ )
79
+ GEOM_COLL_PRIMARY_ATT = (
80
+ 2,
81
+ "geom_coll_primary",
82
+ "Geometrically collimated primary attenuated photons",
83
+ )
84
+ SEPTAL_PENETRATION_PRIMARY_ATT = (
85
+ 3,
86
+ "septal_pen_primary",
87
+ "Septal penetration from primary attenuated photons",
88
+ )
89
+ COLL_SCATTER_PRIMARY_ATT = (
90
+ 4,
91
+ "coll_scatter_primary",
92
+ "Collimator scatter from primary attenuated photons",
93
+ )
94
+ COLL_XRAY_PRIMARY_ATT = (
95
+ 5,
96
+ "coll_xray_primary",
97
+ "X-rays from collimator (primary attenuated photons)",
98
+ )
99
+ GEOM_COLL_SCATTERED = (
100
+ 6,
101
+ "geom_coll_scattered",
102
+ "Geometrically collimated scattered photons",
103
+ )
104
+ SEPTAL_PENETRATION_SCATTERED = (
105
+ 7,
106
+ "septal_pen_scattered",
107
+ "Septal penetration from scattered photons",
108
+ )
109
+ COLL_SCATTER_SCATTERED = (
110
+ 8,
111
+ "coll_scatter_scattered",
112
+ "Collimator scatter from scattered photons",
113
+ )
114
+ COLL_XRAY_SCATTERED = (
115
+ 9,
116
+ "coll_xray_scattered",
117
+ "X-rays from collimator (scattered photons)",
118
+ )
119
+ # With backscatter (*.b10-*.b17)
120
+ GEOM_COLL_PRIMARY_ATT_BACK = (
121
+ 10,
122
+ "geom_coll_primary_back",
123
+ "Geometrically collimated primary attenuated photons (with backscatter)",
124
+ )
125
+ SEPTAL_PENETRATION_PRIMARY_ATT_BACK = (
126
+ 11,
127
+ "septal_pen_primary_back",
128
+ "Septal penetration from primary attenuated photons (with backscatter)",
129
+ )
130
+ COLL_SCATTER_PRIMARY_ATT_BACK = (
131
+ 12,
132
+ "coll_scatter_primary_back",
133
+ "Collimator scatter from primary attenuated photons (with backscatter)",
134
+ )
135
+ COLL_XRAY_PRIMARY_ATT_BACK = (
136
+ 13,
137
+ "coll_xray_primary_back",
138
+ "X-rays from collimator, primary attenuated photons (with backscatter)",
139
+ )
140
+ GEOM_COLL_SCATTERED_BACK = (
141
+ 14,
142
+ "geom_coll_scattered_back",
143
+ "Geometrically collimated scattered photons (with backscatter)",
144
+ )
145
+ SEPTAL_PENETRATION_SCATTERED_BACK = (
146
+ 15,
147
+ "septal_pen_scattered_back",
148
+ "Septal penetration from scattered photons (with backscatter)",
149
+ )
150
+ COLL_SCATTER_SCATTERED_BACK = (
151
+ 16,
152
+ "coll_scatter_scattered_back",
153
+ "Collimator scatter from scattered photons (with backscatter)",
154
+ )
155
+ COLL_XRAY_SCATTERED_BACK = (
156
+ 17,
157
+ "coll_xray_scattered_back",
158
+ "X-rays from collimator, scattered photons (with backscatter)",
159
+ )
160
+ ALL_UNSCATTERED_UNATTENUATED = (
161
+ 18,
162
+ "unscattered_unattenuated",
163
+ "Photons without scattering and attenuation in phantom",
164
+ )
165
+ ALL_UNSCATTERED_UNATTENUATED_GEOM_COLL = (
166
+ 19,
167
+ "unscattered_unattenuated_geom_coll",
168
+ "Photons without scattering/attenuation, geometrically collimated",
169
+ )
170
+
171
+
172
+ # =============================================================================
173
+ # CONSTANTS
174
+ # =============================================================================
175
+
176
+ SIMIND_VOXEL_UNIT_CONVERSION = 10 # mm to cm
177
+ # Maximum normalised value of source image
178
+ # You would have expected this to be 65535, but it is not
179
+ # I have no understanding why, but it is the case
180
+ # 500 seems a reasonable value that improves precision
181
+ # whilst not exceeding the maximum value (weird things happen)
182
+ MAX_SOURCE = 500
183
+ ORBIT_FILE_EXTENSION = ".cor"
184
+ OUTPUT_EXTENSIONS = [".h00", ".a00", ".hs"]
185
+
186
+
187
+ __all__ = [
188
+ # Exceptions
189
+ "SimindError",
190
+ "ValidationError",
191
+ "SimulationError",
192
+ "OutputError",
193
+ # Enums
194
+ "RotationDirection",
195
+ "ScatterType",
196
+ "ScoringRoutine",
197
+ "PenetrateOutputType",
198
+ # Constants
199
+ "SIMIND_VOXEL_UNIT_CONVERSION",
200
+ "MAX_SOURCE",
201
+ "ORBIT_FILE_EXTENSION",
202
+ "OUTPUT_EXTENSIONS",
203
+ ]
@@ -0,0 +1,222 @@
1
+ {
2
+ "Air_0": {
3
+ "density (mg/cm3)": 1.21,
4
+ "HU_lo (HU)": -1050.0,
5
+ "HU_hi (HU)": -950.0
6
+ },
7
+ "Lung_1": {
8
+ "density (mg/cm3)": 102.695,
9
+ "HU_lo (HU)": -950.0,
10
+ "HU_hi (HU)": -852.884
11
+ },
12
+ "Lung_2": {
13
+ "density (mg/cm3)": 202.695,
14
+ "HU_lo (HU)": -852.884,
15
+ "HU_hi (HU)": -755.769
16
+ },
17
+ "Lung_3": {
18
+ "density (mg/cm3)": 302.695,
19
+ "HU_lo (HU)": -755.769,
20
+ "HU_hi (HU)": -658.653
21
+ },
22
+ "Lung_4": {
23
+ "density (mg/cm3)": 402.695,
24
+ "HU_lo (HU)": -658.653,
25
+ "HU_hi (HU)": -561.538
26
+ },
27
+ "Lung_5": {
28
+ "density (mg/cm3)": 502.695,
29
+ "HU_lo (HU)": -561.538,
30
+ "HU_hi (HU)": -464.422
31
+ },
32
+ "Lung_6": {
33
+ "density (mg/cm3)": 602.695,
34
+ "HU_lo (HU)": -464.422,
35
+ "HU_hi (HU)": -367.306
36
+ },
37
+ "Lung_7": {
38
+ "density (mg/cm3)": 702.695,
39
+ "HU_lo (HU)": -367.306,
40
+ "HU_hi (HU)": -270.191
41
+ },
42
+ "Lung_8": {
43
+ "density (mg/cm3)": 802.695,
44
+ "HU_lo (HU)": -270.191,
45
+ "HU_hi (HU)": -173.075
46
+ },
47
+ "Lung_9": {
48
+ "density (mg/cm3)": 880.021,
49
+ "HU_lo (HU)": -173.075,
50
+ "HU_hi (HU)": -120.0
51
+ },
52
+ "AT_AG_SI1_10": {
53
+ "density (mg/cm3)": 926.911,
54
+ "HU_lo (HU)": -120.0,
55
+ "HU_hi (HU)": -82.0
56
+ },
57
+ "AT_AG_SI2_11": {
58
+ "density (mg/cm3)": 957.382,
59
+ "HU_lo (HU)": -82.0,
60
+ "HU_hi (HU)": -52.0
61
+ },
62
+ "AT_AG_SI3_12": {
63
+ "density (mg/cm3)": 984.277,
64
+ "HU_lo (HU)": -52.0,
65
+ "HU_hi (HU)": -22.0
66
+ },
67
+ "AT_AG_SI4_13": {
68
+ "density (mg/cm3)": 1011.1699999999998,
69
+ "HU_lo (HU)": -22.0,
70
+ "HU_hi (HU)": 8.0
71
+ },
72
+ "AT_AG_SI5_14": {
73
+ "density (mg/cm3)": 1029.55,
74
+ "HU_lo (HU)": 8.0,
75
+ "HU_hi (HU)": 19.0
76
+ },
77
+ "SoftTissus_15": {
78
+ "density (mg/cm3)": 1061.6000000000001,
79
+ "HU_lo (HU)": 19.0,
80
+ "HU_hi (HU)": 80.0
81
+ },
82
+ "ConnectiveTissue_16": {
83
+ "density (mg/cm3)": 1119.8999999999999,
84
+ "HU_lo (HU)": 80.0,
85
+ "HU_hi (HU)": 120.0
86
+ },
87
+ "Marrow_Bone01_17": {
88
+ "density (mg/cm3)": 1111.15,
89
+ "HU_lo (HU)": 120.0,
90
+ "HU_hi (HU)": 200.0
91
+ },
92
+ "Marrow_Bone02_18": {
93
+ "density (mg/cm3)": 1164.4699999999998,
94
+ "HU_lo (HU)": 200.0,
95
+ "HU_hi (HU)": 300.0
96
+ },
97
+ "Marrow_Bone03_19": {
98
+ "density (mg/cm3)": 1223.71,
99
+ "HU_lo (HU)": 300.0,
100
+ "HU_hi (HU)": 400.0
101
+ },
102
+ "Marrow_Bone04_20": {
103
+ "density (mg/cm3)": 1282.95,
104
+ "HU_lo (HU)": 400.0,
105
+ "HU_hi (HU)": 500.0
106
+ },
107
+ "Marrow_Bone05_21": {
108
+ "density (mg/cm3)": 1342.19,
109
+ "HU_lo (HU)": 500.0,
110
+ "HU_hi (HU)": 600.0
111
+ },
112
+ "Marrow_Bone06_22": {
113
+ "density (mg/cm3)": 1401.42,
114
+ "HU_lo (HU)": 600.0,
115
+ "HU_hi (HU)": 700.0
116
+ },
117
+ "Marrow_Bone07_23": {
118
+ "density (mg/cm3)": 1460.66,
119
+ "HU_lo (HU)": 700.0,
120
+ "HU_hi (HU)": 800.0
121
+ },
122
+ "Marrow_Bone08_24": {
123
+ "density (mg/cm3)": 1519.9,
124
+ "HU_lo (HU)": 800.0,
125
+ "HU_hi (HU)": 900.0
126
+ },
127
+ "Marrow_Bone09_25": {
128
+ "density (mg/cm3)": 1579.14,
129
+ "HU_lo (HU)": 900.0,
130
+ "HU_hi (HU)": 1000.0
131
+ },
132
+ "Marrow_Bone10_26": {
133
+ "density (mg/cm3)": 1638.3799999999999,
134
+ "HU_lo (HU)": 1000.0,
135
+ "HU_hi (HU)": 1100.0
136
+ },
137
+ "Marrow_Bone11_27": {
138
+ "density (mg/cm3)": 1697.62,
139
+ "HU_lo (HU)": 1100.0,
140
+ "HU_hi (HU)": 1200.0
141
+ },
142
+ "Marrow_Bone12_28": {
143
+ "density (mg/cm3)": 1756.8600000000001,
144
+ "HU_lo (HU)": 1200.0,
145
+ "HU_hi (HU)": 1300.0
146
+ },
147
+ "Marrow_Bone13_29": {
148
+ "density (mg/cm3)": 1816.1000000000001,
149
+ "HU_lo (HU)": 1300.0,
150
+ "HU_hi (HU)": 1400.0
151
+ },
152
+ "Marrow_Bone14_30": {
153
+ "density (mg/cm3)": 1875.34,
154
+ "HU_lo (HU)": 1400.0,
155
+ "HU_hi (HU)": 1500.0
156
+ },
157
+ "Marrow_Bone15_31": {
158
+ "density (mg/cm3)": 1946.43,
159
+ "HU_lo (HU)": 1500.0,
160
+ "HU_hi (HU)": 1640.0
161
+ },
162
+ "AmalgamTooth_32": {
163
+ "density (mg/cm3)": 2038.08,
164
+ "HU_lo (HU)": 1640.0,
165
+ "HU_hi (HU)": 1807.5
166
+ },
167
+ "AmalgamTooth_33": {
168
+ "density (mg/cm3)": 2138.08,
169
+ "HU_lo (HU)": 1807.5,
170
+ "HU_hi (HU)": 1975.01
171
+ },
172
+ "AmalgamTooth_34": {
173
+ "density (mg/cm3)": 2238.08,
174
+ "HU_lo (HU)": 1975.01,
175
+ "HU_hi (HU)": 2142.51
176
+ },
177
+ "AmalgamTooth_35": {
178
+ "density (mg/cm3)": 2335.09,
179
+ "HU_lo (HU)": 2142.51,
180
+ "HU_hi (HU)": 2300.0
181
+ },
182
+ "MetallImplants_36": {
183
+ "density (mg/cm3)": 2432.1000000000004,
184
+ "HU_lo (HU)": 2300.0,
185
+ "HU_hi (HU)": 2467.5
186
+ },
187
+ "MetallImplants_37": {
188
+ "density (mg/cm3)": 2532.1,
189
+ "HU_lo (HU)": 2467.5,
190
+ "HU_hi (HU)": 2635.01
191
+ },
192
+ "MetallImplants_38": {
193
+ "density (mg/cm3)": 2632.1,
194
+ "HU_lo (HU)": 2635.01,
195
+ "HU_hi (HU)": 2802.51
196
+ },
197
+ "MetallImplants_39": {
198
+ "density (mg/cm3)": 2732.1,
199
+ "HU_lo (HU)": 2802.51,
200
+ "HU_hi (HU)": 2970.02
201
+ },
202
+ "MetallImplants_40": {
203
+ "density (mg/cm3)": 2791.0499999999997,
204
+ "HU_lo (HU)": 2970.02,
205
+ "HU_hi (HU)": 3000.0
206
+ },
207
+ "MetallImplants_41": {
208
+ "density (mg/cm3)": 2900.0,
209
+ "HU_lo (HU)": 3000.0,
210
+ "HU_hi (HU)": 4000.0
211
+ },
212
+ "MetallImplants_42": {
213
+ "density (mg/cm3)": 2900.0,
214
+ "HU_lo (HU)": 4000.0,
215
+ "HU_hi (HU)": 4000.0
216
+ },
217
+ "MetallImplants_43": {
218
+ "density (mg/cm3)": 2900.0,
219
+ "HU_lo (HU)": 4000.0,
220
+ "HU_hi (HU)": 4001.0
221
+ }
222
+ }
@@ -0,0 +1,25 @@
1
+ """
2
+ Access to the built-in attenuation tables bundled with the package.
3
+ """
4
+
5
+ from importlib import resources as _res
6
+ from pathlib import Path
7
+
8
+ import numpy as _np
9
+
10
+
11
+ __all__ = ["data_path", "load_table"]
12
+
13
+
14
+ def data_path(filename: str) -> Path:
15
+ """Return a path to a packaged data file."""
16
+ return _res.files(__package__).joinpath(filename)
17
+
18
+
19
+ def load_table(filename: str) -> _np.ndarray:
20
+ """
21
+ Load an attenuation table (energy [keV], μ [cm⁻¹]).
22
+ Example filenames: 'bone.atn', 'h2o.atn'.
23
+ """
24
+ with _res.as_file(data_path(filename)) as fp:
25
+ return _np.loadtxt(fp, dtype=float)