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,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"]