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,164 @@
|
|
|
1
|
+
"""SIRF adaptor implemented on top of the connector-first NumPy pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from simind_python_connector.connectors._spacing import extract_voxel_size_mm
|
|
10
|
+
from simind_python_connector.connectors.base import BaseConnector
|
|
11
|
+
from simind_python_connector.connectors.python_connector import (
|
|
12
|
+
ConfigSource,
|
|
13
|
+
RuntimeOperator,
|
|
14
|
+
SimindPythonConnector,
|
|
15
|
+
)
|
|
16
|
+
from simind_python_connector.core.config import SimulationConfig
|
|
17
|
+
from simind_python_connector.core.types import PenetrateOutputType, ScoringRoutine
|
|
18
|
+
from simind_python_connector.utils import get_array
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import sirf.STIR as sirf
|
|
23
|
+
except ImportError: # pragma: no cover - optional dependency
|
|
24
|
+
sirf = None # type: ignore[assignment]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SirfSimindAdaptor(BaseConnector):
|
|
28
|
+
"""Adaptor consuming/returning SIRF-native objects."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
config_source: ConfigSource,
|
|
33
|
+
output_dir: str,
|
|
34
|
+
output_prefix: str = "output",
|
|
35
|
+
photon_multiplier: int = 1,
|
|
36
|
+
quantization_scale: float = 1.0,
|
|
37
|
+
scoring_routine: ScoringRoutine | int = ScoringRoutine.SCATTWIN,
|
|
38
|
+
) -> None:
|
|
39
|
+
if sirf is None:
|
|
40
|
+
raise ImportError("SirfSimindAdaptor requires the SIRF Python package.")
|
|
41
|
+
|
|
42
|
+
self.python_connector = SimindPythonConnector(
|
|
43
|
+
config_source=config_source,
|
|
44
|
+
output_dir=output_dir,
|
|
45
|
+
output_prefix=output_prefix,
|
|
46
|
+
quantization_scale=quantization_scale,
|
|
47
|
+
)
|
|
48
|
+
self._scoring_routine = (
|
|
49
|
+
ScoringRoutine(scoring_routine)
|
|
50
|
+
if isinstance(scoring_routine, int)
|
|
51
|
+
else scoring_routine
|
|
52
|
+
)
|
|
53
|
+
self._source: Any = None
|
|
54
|
+
self._mu_map: Any = None
|
|
55
|
+
self._outputs: Optional[dict[str, Any]] = None
|
|
56
|
+
|
|
57
|
+
self.add_runtime_switch("NN", photon_multiplier)
|
|
58
|
+
|
|
59
|
+
def set_source(self, source: Any) -> None:
|
|
60
|
+
self._source = source
|
|
61
|
+
|
|
62
|
+
def set_mu_map(self, mu_map: Any) -> None:
|
|
63
|
+
self._mu_map = mu_map
|
|
64
|
+
|
|
65
|
+
def set_energy_windows(
|
|
66
|
+
self,
|
|
67
|
+
lower_bounds: float | list[float],
|
|
68
|
+
upper_bounds: float | list[float],
|
|
69
|
+
scatter_orders: int | list[int],
|
|
70
|
+
) -> None:
|
|
71
|
+
self.python_connector.set_energy_windows(
|
|
72
|
+
lower_bounds, upper_bounds, scatter_orders
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def add_config_value(self, index: int, value: Any) -> None:
|
|
76
|
+
self.python_connector.add_config_value(index, value)
|
|
77
|
+
|
|
78
|
+
def add_runtime_switch(self, switch: str, value: Any) -> None:
|
|
79
|
+
self.python_connector.add_runtime_switch(switch, value)
|
|
80
|
+
|
|
81
|
+
def run(self, runtime_operator: Optional[RuntimeOperator] = None) -> dict[str, Any]:
|
|
82
|
+
self._validate_inputs()
|
|
83
|
+
assert self._source is not None
|
|
84
|
+
assert self._mu_map is not None
|
|
85
|
+
|
|
86
|
+
source_arr = np.asarray(get_array(self._source), dtype=np.float32)
|
|
87
|
+
mu_arr = np.asarray(get_array(self._mu_map), dtype=np.float32)
|
|
88
|
+
voxel_size_mm = self._extract_voxel_size_mm(self._source)
|
|
89
|
+
|
|
90
|
+
self.python_connector.configure_voxel_phantom(
|
|
91
|
+
source=source_arr,
|
|
92
|
+
mu_map=mu_arr,
|
|
93
|
+
voxel_size_mm=voxel_size_mm,
|
|
94
|
+
scoring_routine=self._scoring_routine,
|
|
95
|
+
)
|
|
96
|
+
raw_outputs = self.python_connector.run(runtime_operator=runtime_operator)
|
|
97
|
+
self._outputs = {
|
|
98
|
+
key: sirf.AcquisitionData(str(result.header_path))
|
|
99
|
+
for key, result in raw_outputs.items()
|
|
100
|
+
}
|
|
101
|
+
return self._outputs
|
|
102
|
+
|
|
103
|
+
def get_outputs(self) -> Dict[str, Any]:
|
|
104
|
+
if self._outputs is None:
|
|
105
|
+
raise RuntimeError("Run the adaptor first to produce outputs.")
|
|
106
|
+
return self._outputs
|
|
107
|
+
|
|
108
|
+
def get_total_output(self, window: int = 1) -> Any:
|
|
109
|
+
return self._get_component("tot", window)
|
|
110
|
+
|
|
111
|
+
def get_scatter_output(self, window: int = 1) -> Any:
|
|
112
|
+
return self._get_component("sca", window)
|
|
113
|
+
|
|
114
|
+
def get_primary_output(self, window: int = 1) -> Any:
|
|
115
|
+
return self._get_component("pri", window)
|
|
116
|
+
|
|
117
|
+
def get_air_output(self, window: int = 1) -> Any:
|
|
118
|
+
return self._get_component("air", window)
|
|
119
|
+
|
|
120
|
+
def get_penetrate_output(self, component: PenetrateOutputType | str) -> Any:
|
|
121
|
+
outputs = self.get_outputs()
|
|
122
|
+
key = (
|
|
123
|
+
component.slug if isinstance(component, PenetrateOutputType) else component
|
|
124
|
+
)
|
|
125
|
+
if key not in outputs:
|
|
126
|
+
available = ", ".join(sorted(outputs))
|
|
127
|
+
raise KeyError(f"Output {key!r} not available. Available: {available}")
|
|
128
|
+
return outputs[key]
|
|
129
|
+
|
|
130
|
+
def list_available_outputs(self) -> list[str]:
|
|
131
|
+
return sorted(self.get_outputs().keys())
|
|
132
|
+
|
|
133
|
+
def get_scoring_routine(self) -> ScoringRoutine:
|
|
134
|
+
return self._scoring_routine
|
|
135
|
+
|
|
136
|
+
def get_config(self) -> SimulationConfig:
|
|
137
|
+
return self.python_connector.get_config()
|
|
138
|
+
|
|
139
|
+
def _get_component(self, prefix: str, window: int) -> Any:
|
|
140
|
+
outputs = self.get_outputs()
|
|
141
|
+
key = f"{prefix}_w{window}"
|
|
142
|
+
if key not in outputs:
|
|
143
|
+
available = ", ".join(sorted(outputs))
|
|
144
|
+
raise KeyError(f"Output {key!r} not available. Available: {available}")
|
|
145
|
+
return outputs[key]
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _extract_voxel_size_mm(image: Any) -> float:
|
|
149
|
+
return extract_voxel_size_mm(image=image, backend_name="SIRF")
|
|
150
|
+
|
|
151
|
+
def _validate_inputs(self) -> None:
|
|
152
|
+
if self._source is None or self._mu_map is None:
|
|
153
|
+
raise ValueError("Both source and mu_map must be set before run().")
|
|
154
|
+
|
|
155
|
+
source_shape = np.asarray(get_array(self._source)).shape
|
|
156
|
+
mu_shape = np.asarray(get_array(self._mu_map)).shape
|
|
157
|
+
if source_shape != mu_shape:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"source and mu_map must have matching shapes, got "
|
|
160
|
+
f"{source_shape} and {mu_shape}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
__all__ = ["SirfSimindAdaptor"]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""STIR adaptor implemented on top of the connector-first NumPy pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from simind_python_connector.connectors._spacing import extract_voxel_size_mm
|
|
10
|
+
from simind_python_connector.connectors.base import BaseConnector
|
|
11
|
+
from simind_python_connector.connectors.python_connector import (
|
|
12
|
+
ConfigSource,
|
|
13
|
+
RuntimeOperator,
|
|
14
|
+
SimindPythonConnector,
|
|
15
|
+
)
|
|
16
|
+
from simind_python_connector.core.config import SimulationConfig
|
|
17
|
+
from simind_python_connector.core.types import PenetrateOutputType, ScoringRoutine
|
|
18
|
+
from simind_python_connector.utils import get_array
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import stir
|
|
23
|
+
except ImportError: # pragma: no cover - optional dependency
|
|
24
|
+
stir = None # type: ignore[assignment]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StirSimindAdaptor(BaseConnector):
|
|
28
|
+
"""Adaptor consuming/returning STIR-native objects."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
config_source: ConfigSource,
|
|
33
|
+
output_dir: str,
|
|
34
|
+
output_prefix: str = "output",
|
|
35
|
+
photon_multiplier: int = 1,
|
|
36
|
+
quantization_scale: float = 1.0,
|
|
37
|
+
scoring_routine: ScoringRoutine | int = ScoringRoutine.SCATTWIN,
|
|
38
|
+
) -> None:
|
|
39
|
+
if stir is None:
|
|
40
|
+
raise ImportError("StirSimindAdaptor requires the STIR Python package.")
|
|
41
|
+
|
|
42
|
+
self.python_connector = SimindPythonConnector(
|
|
43
|
+
config_source=config_source,
|
|
44
|
+
output_dir=output_dir,
|
|
45
|
+
output_prefix=output_prefix,
|
|
46
|
+
quantization_scale=quantization_scale,
|
|
47
|
+
)
|
|
48
|
+
self._scoring_routine = (
|
|
49
|
+
ScoringRoutine(scoring_routine)
|
|
50
|
+
if isinstance(scoring_routine, int)
|
|
51
|
+
else scoring_routine
|
|
52
|
+
)
|
|
53
|
+
self._source: Any = None
|
|
54
|
+
self._mu_map: Any = None
|
|
55
|
+
self._outputs: Optional[dict[str, Any]] = None
|
|
56
|
+
|
|
57
|
+
self.add_runtime_switch("NN", photon_multiplier)
|
|
58
|
+
|
|
59
|
+
def set_source(self, source: Any) -> None:
|
|
60
|
+
self._source = source
|
|
61
|
+
|
|
62
|
+
def set_mu_map(self, mu_map: Any) -> None:
|
|
63
|
+
self._mu_map = mu_map
|
|
64
|
+
|
|
65
|
+
def set_energy_windows(
|
|
66
|
+
self,
|
|
67
|
+
lower_bounds: float | list[float],
|
|
68
|
+
upper_bounds: float | list[float],
|
|
69
|
+
scatter_orders: int | list[int],
|
|
70
|
+
) -> None:
|
|
71
|
+
self.python_connector.set_energy_windows(
|
|
72
|
+
lower_bounds, upper_bounds, scatter_orders
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def add_config_value(self, index: int, value: Any) -> None:
|
|
76
|
+
self.python_connector.add_config_value(index, value)
|
|
77
|
+
|
|
78
|
+
def add_runtime_switch(self, switch: str, value: Any) -> None:
|
|
79
|
+
self.python_connector.add_runtime_switch(switch, value)
|
|
80
|
+
|
|
81
|
+
def run(self, runtime_operator: Optional[RuntimeOperator] = None) -> dict[str, Any]:
|
|
82
|
+
self._validate_inputs()
|
|
83
|
+
assert self._source is not None
|
|
84
|
+
assert self._mu_map is not None
|
|
85
|
+
|
|
86
|
+
source_arr = np.asarray(get_array(self._source), dtype=np.float32)
|
|
87
|
+
mu_arr = np.asarray(get_array(self._mu_map), dtype=np.float32)
|
|
88
|
+
voxel_size_mm = self._extract_voxel_size_mm(self._source)
|
|
89
|
+
|
|
90
|
+
self.python_connector.configure_voxel_phantom(
|
|
91
|
+
source=source_arr,
|
|
92
|
+
mu_map=mu_arr,
|
|
93
|
+
voxel_size_mm=voxel_size_mm,
|
|
94
|
+
scoring_routine=self._scoring_routine,
|
|
95
|
+
)
|
|
96
|
+
raw_outputs = self.python_connector.run(runtime_operator=runtime_operator)
|
|
97
|
+
self._outputs = {
|
|
98
|
+
key: stir.ProjData.read_from_file(str(result.header_path))
|
|
99
|
+
for key, result in raw_outputs.items()
|
|
100
|
+
}
|
|
101
|
+
return self._outputs
|
|
102
|
+
|
|
103
|
+
def get_outputs(self) -> Dict[str, Any]:
|
|
104
|
+
if self._outputs is None:
|
|
105
|
+
raise RuntimeError("Run the adaptor first to produce outputs.")
|
|
106
|
+
return self._outputs
|
|
107
|
+
|
|
108
|
+
def get_total_output(self, window: int = 1) -> Any:
|
|
109
|
+
return self._get_component("tot", window)
|
|
110
|
+
|
|
111
|
+
def get_scatter_output(self, window: int = 1) -> Any:
|
|
112
|
+
return self._get_component("sca", window)
|
|
113
|
+
|
|
114
|
+
def get_primary_output(self, window: int = 1) -> Any:
|
|
115
|
+
return self._get_component("pri", window)
|
|
116
|
+
|
|
117
|
+
def get_air_output(self, window: int = 1) -> Any:
|
|
118
|
+
return self._get_component("air", window)
|
|
119
|
+
|
|
120
|
+
def get_penetrate_output(self, component: PenetrateOutputType | str) -> Any:
|
|
121
|
+
outputs = self.get_outputs()
|
|
122
|
+
key = (
|
|
123
|
+
component.slug if isinstance(component, PenetrateOutputType) else component
|
|
124
|
+
)
|
|
125
|
+
if key not in outputs:
|
|
126
|
+
available = ", ".join(sorted(outputs))
|
|
127
|
+
raise KeyError(f"Output {key!r} not available. Available: {available}")
|
|
128
|
+
return outputs[key]
|
|
129
|
+
|
|
130
|
+
def list_available_outputs(self) -> list[str]:
|
|
131
|
+
return sorted(self.get_outputs().keys())
|
|
132
|
+
|
|
133
|
+
def get_scoring_routine(self) -> ScoringRoutine:
|
|
134
|
+
return self._scoring_routine
|
|
135
|
+
|
|
136
|
+
def get_config(self) -> SimulationConfig:
|
|
137
|
+
return self.python_connector.get_config()
|
|
138
|
+
|
|
139
|
+
def _get_component(self, prefix: str, window: int) -> Any:
|
|
140
|
+
outputs = self.get_outputs()
|
|
141
|
+
key = f"{prefix}_w{window}"
|
|
142
|
+
if key not in outputs:
|
|
143
|
+
available = ", ".join(sorted(outputs))
|
|
144
|
+
raise KeyError(f"Output {key!r} not available. Available: {available}")
|
|
145
|
+
return outputs[key]
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _extract_voxel_size_mm(image: Any) -> float:
|
|
149
|
+
return extract_voxel_size_mm(image=image, backend_name="STIR")
|
|
150
|
+
|
|
151
|
+
def _validate_inputs(self) -> None:
|
|
152
|
+
if self._source is None or self._mu_map is None:
|
|
153
|
+
raise ValueError("Both source and mu_map must be set before run().")
|
|
154
|
+
|
|
155
|
+
source_shape = np.asarray(get_array(self._source)).shape
|
|
156
|
+
mu_shape = np.asarray(get_array(self._mu_map)).shape
|
|
157
|
+
if source_shape != mu_shape:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"source and mu_map must have matching shapes, got "
|
|
160
|
+
f"{source_shape} and {mu_shape}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
__all__ = ["StirSimindAdaptor"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Converters between SIMIND/DICOM/STIR files and in-memory
|
|
3
|
+
representations used elsewhere in the package.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import importlib
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Lazy imports to avoid SIRF dependencies in CI
|
|
10
|
+
def __getattr__(name):
|
|
11
|
+
if name in ("attenuation", "dicom_to_stir", "simind_to_stir"):
|
|
12
|
+
return importlib.import_module(f".{name}", __name__)
|
|
13
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["attenuation", "dicom_to_stir", "simind_to_stir"]
|