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,72 @@
1
+ """
2
+ simind-python-connector connector/adaptor API.
3
+ """
4
+
5
+ import importlib
6
+ from importlib import metadata as _meta
7
+ from typing import Any
8
+
9
+
10
+ for _dist_name in ("simind-python-connector", __name__):
11
+ try: # installed (pip/poetry)
12
+ __version__ = _meta.version(_dist_name)
13
+ break
14
+ except _meta.PackageNotFoundError:
15
+ continue
16
+ else: # editable / source checkout
17
+ __version__ = "1.0.0"
18
+
19
+
20
+ def __getattr__(name: str) -> Any:
21
+ if name in {
22
+ "builders",
23
+ "configs",
24
+ "connectors",
25
+ "converters",
26
+ "core",
27
+ "data",
28
+ "utils",
29
+ }:
30
+ mod = importlib.import_module(f".{name}", __name__)
31
+ globals()[name] = mod
32
+ return mod
33
+ elif name in {"SimulationConfig"}:
34
+ core = importlib.import_module(".core", __name__)
35
+ obj = getattr(core, name)
36
+ globals()[name] = obj
37
+ return obj
38
+ elif name in {
39
+ "BaseConnector",
40
+ "NumpyConnector",
41
+ "PyTomographySimindAdaptor",
42
+ "ProjectionResult",
43
+ "RuntimeOperator",
44
+ "SirfSimindAdaptor",
45
+ "SimindPythonConnector",
46
+ "StirSimindAdaptor",
47
+ }:
48
+ connectors = importlib.import_module(".connectors", __name__)
49
+ obj = getattr(connectors, name)
50
+ globals()[name] = obj
51
+ return obj
52
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
53
+
54
+
55
+ __all__ = [
56
+ "BaseConnector",
57
+ "NumpyConnector",
58
+ "ProjectionResult",
59
+ "PyTomographySimindAdaptor",
60
+ "RuntimeOperator",
61
+ "SirfSimindAdaptor",
62
+ "SimindPythonConnector",
63
+ "SimulationConfig",
64
+ "StirSimindAdaptor",
65
+ "builders",
66
+ "configs",
67
+ "connectors",
68
+ "converters",
69
+ "core",
70
+ "data",
71
+ "utils",
72
+ ]
@@ -0,0 +1,480 @@
1
+ """
2
+ Backend abstraction layer for SIRF and STIR Python.
3
+
4
+ This module provides automatic backend detection and factory functions
5
+ for creating image and acquisition data objects that work with both
6
+ SIRF and STIR Python interfaces.
7
+
8
+ Usage:
9
+ # Automatic detection
10
+ from simind_python_connector.backends import get_backend, create_image_data
11
+
12
+ print(f"Using backend: {get_backend()}")
13
+ img = create_image_data("phantom.hv")
14
+
15
+ # Manual selection
16
+ from simind_python_connector.backends import set_backend
17
+ set_backend("stir") # Force STIR Python
18
+ """
19
+
20
+ import importlib
21
+ import logging
22
+ from typing import TYPE_CHECKING, Any, Literal, Optional, Union
23
+
24
+ from .base import AcquisitionDataInterface, ImageDataInterface
25
+
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ if TYPE_CHECKING:
30
+ try:
31
+ from sirf.STIR import AcquisitionData, ImageData
32
+ except ImportError: # pragma: no cover - for type checkers without SIRF
33
+ AcquisitionData = Any # type: ignore[assignment]
34
+ ImageData = Any # type: ignore[assignment]
35
+ try:
36
+ import stir
37
+
38
+ ProjData = stir.ProjData # type: ignore[attr-defined]
39
+ FloatVoxelsOnCartesianGrid = stir.FloatVoxelsOnCartesianGrid # type: ignore[attr-defined]
40
+ except ImportError: # pragma: no cover - for type checkers without STIR
41
+ ProjData = Any # type: ignore[assignment]
42
+ FloatVoxelsOnCartesianGrid = Any # type: ignore[assignment]
43
+
44
+ # Global backend state
45
+ _backend: Optional[str] = None
46
+ _backend_initialized = False
47
+
48
+
49
+ def _import_sirf_types() -> tuple[Optional[type], Optional[type]]:
50
+ """Import SIRF image/acquisition types lazily."""
51
+ try:
52
+ from sirf.STIR import AcquisitionData as SirfAcquisitionData # type: ignore
53
+ from sirf.STIR import ImageData as SirfImageData # type: ignore
54
+ except ImportError:
55
+ return None, None
56
+ return SirfImageData, SirfAcquisitionData
57
+
58
+
59
+ def _import_stir_types() -> tuple[Optional[type], Optional[type]]:
60
+ """Import STIR image/acquisition types lazily."""
61
+ try:
62
+ from stir import FloatVoxelsOnCartesianGrid as StirImageData # type: ignore
63
+ from stir import ProjData as StirProjData # type: ignore
64
+ except ImportError:
65
+ return None, None
66
+
67
+ return StirImageData, StirProjData
68
+
69
+
70
+ def detect_image_backend(obj: Any) -> Optional[str]:
71
+ """Return 'sirf' or 'stir' if obj is a native image type."""
72
+ if obj is None:
73
+ return None
74
+
75
+ sirf_image, _ = _import_sirf_types()
76
+ if sirf_image is not None and isinstance(obj, sirf_image):
77
+ return "sirf"
78
+
79
+ stir_image, _ = _import_stir_types()
80
+ if stir_image is not None and isinstance(obj, stir_image):
81
+ return "stir"
82
+
83
+ return None
84
+
85
+
86
+ def detect_acquisition_backend(obj: Any) -> Optional[str]:
87
+ """Return 'sirf' or 'stir' if obj is a native acquisition type."""
88
+ if obj is None:
89
+ return None
90
+
91
+ _, sirf_acq = _import_sirf_types()
92
+ if sirf_acq is not None and isinstance(obj, sirf_acq):
93
+ return "sirf"
94
+
95
+ _, stir_proj = _import_stir_types()
96
+ if stir_proj is not None and isinstance(obj, stir_proj):
97
+ return "stir"
98
+
99
+ return None
100
+
101
+
102
+ def detect_backend_from_interface(
103
+ obj: Union[ImageDataInterface, AcquisitionDataInterface, Any],
104
+ ) -> Optional[str]:
105
+ """Detect backend by unwrapping known interface objects."""
106
+ native = getattr(obj, "native_object", None)
107
+ if native is None:
108
+ return None
109
+
110
+ backend = detect_image_backend(native)
111
+ if backend:
112
+ return backend
113
+
114
+ return detect_acquisition_backend(native)
115
+
116
+
117
+ def _activate_backend(preferred: Optional[str]) -> Optional[str]:
118
+ """Force-activate a specific backend if requested."""
119
+ if preferred is None:
120
+ return None
121
+ if preferred not in ("sirf", "stir"):
122
+ raise ValueError(
123
+ f"Invalid backend hint: {preferred}. Expected 'sirf' or 'stir'."
124
+ )
125
+ set_backend(preferred)
126
+ return preferred
127
+
128
+
129
+ def get_backend() -> str:
130
+ """Get the current backend or auto-detect.
131
+
132
+ Returns:
133
+ str: Either "sirf" or "stir"
134
+
135
+ Raises:
136
+ ImportError: If neither SIRF nor STIR Python is available
137
+ """
138
+ global _backend, _backend_initialized
139
+
140
+ if _backend is not None and _backend_initialized:
141
+ return _backend
142
+
143
+ # Auto-detect
144
+ try:
145
+ importlib.import_module("sirf.STIR")
146
+
147
+ _backend = "sirf"
148
+ logger.info("Auto-detected SIRF backend")
149
+ except ImportError:
150
+ try:
151
+ importlib.import_module("stir")
152
+ importlib.import_module("stirextra")
153
+
154
+ _backend = "stir"
155
+ logger.info("Auto-detected STIR Python backend")
156
+ except ImportError:
157
+ raise ImportError(
158
+ "Neither SIRF nor STIR Python found. "
159
+ "Please install one of: "
160
+ "\n - SIRF (sirf.STIR)"
161
+ "\n - STIR Python (stir + stirextra)"
162
+ )
163
+
164
+ _backend_initialized = True
165
+ return _backend
166
+
167
+
168
+ def set_backend(backend: Literal["sirf", "stir"]) -> None:
169
+ """Manually set the backend.
170
+
171
+ Args:
172
+ backend: Either "sirf" or "stir"
173
+
174
+ Raises:
175
+ ValueError: If backend is not valid
176
+ ImportError: If requested backend is not available
177
+ """
178
+ global _backend, _backend_initialized
179
+
180
+ if backend not in ["sirf", "stir"]:
181
+ raise ValueError(f"Invalid backend: {backend}. Must be 'sirf' or 'stir'")
182
+
183
+ # Verify the backend is available
184
+ if backend == "sirf":
185
+ try:
186
+ importlib.import_module("sirf.STIR")
187
+ except ImportError:
188
+ raise ImportError("SIRF is not available")
189
+ elif backend == "stir":
190
+ try:
191
+ importlib.import_module("stir")
192
+ importlib.import_module("stirextra")
193
+ except ImportError:
194
+ raise ImportError("STIR Python is not available")
195
+
196
+ _backend = backend
197
+ _backend_initialized = True
198
+ logger.info(f"Backend manually set to: {backend}")
199
+
200
+
201
+ def reset_backend() -> None:
202
+ """Reset backend to allow re-detection.
203
+
204
+ Useful for testing or switching backends dynamically.
205
+ """
206
+ global _backend, _backend_initialized
207
+ _backend = None
208
+ _backend_initialized = False
209
+
210
+
211
+ def create_image_data(
212
+ filepath_or_object: Optional[
213
+ Union[str, ImageDataInterface, "ImageData", "FloatVoxelsOnCartesianGrid"]
214
+ ] = None,
215
+ ) -> ImageDataInterface:
216
+ """Create an image data object using the current backend.
217
+
218
+ Args:
219
+ filepath_or_object: Can be:
220
+ - str: Path to image file to load
221
+ - ImageDataInterface: Already wrapped object (returned as-is)
222
+ - Native SIRF/STIR object: Will be wrapped
223
+ - None: Creates empty object (SIRF only)
224
+
225
+ Returns:
226
+ ImageDataInterface: Wrapped image object
227
+
228
+ Raises:
229
+ NotImplementedError: If trying to create empty image with STIR backend
230
+ TypeError: If input type is not supported
231
+ """
232
+ # Already wrapped?
233
+ if isinstance(filepath_or_object, ImageDataInterface):
234
+ return filepath_or_object
235
+
236
+ backend_hint = detect_image_backend(filepath_or_object)
237
+ if backend_hint is None:
238
+ backend_hint = detect_backend_from_interface(filepath_or_object)
239
+
240
+ backend = _activate_backend(backend_hint) or get_backend()
241
+
242
+ if backend == "sirf":
243
+ from .sirf_backend import SirfImageData
244
+
245
+ try:
246
+ from sirf.STIR import ImageData
247
+ except ImportError:
248
+ ImageData = type(None)
249
+
250
+ if filepath_or_object is None:
251
+ return SirfImageData.create_empty()
252
+ elif isinstance(filepath_or_object, str):
253
+ return SirfImageData.read_from_file(filepath_or_object)
254
+ elif ImageData is not type(None) and isinstance(filepath_or_object, ImageData):
255
+ # Wrap native SIRF object
256
+ return SirfImageData(filepath_or_object)
257
+ else:
258
+ raise TypeError(
259
+ f"Cannot create image from type {type(filepath_or_object)}. "
260
+ f"Expected str, ImageDataInterface, or ImageData."
261
+ )
262
+
263
+ elif backend == "stir":
264
+ from .stir_backend import StirImageData
265
+
266
+ try:
267
+ import stir
268
+
269
+ FloatVoxelsOnCartesianGrid = stir.FloatVoxelsOnCartesianGrid
270
+ except ImportError:
271
+ FloatVoxelsOnCartesianGrid = type(None)
272
+
273
+ if filepath_or_object is None:
274
+ raise NotImplementedError(
275
+ "Creating empty STIR images requires geometry. "
276
+ "Please provide a filepath or use a template."
277
+ )
278
+ elif isinstance(filepath_or_object, str):
279
+ return StirImageData.read_from_file(filepath_or_object)
280
+ elif FloatVoxelsOnCartesianGrid is not type(None) and isinstance(
281
+ filepath_or_object, FloatVoxelsOnCartesianGrid
282
+ ):
283
+ # Wrap native STIR object
284
+ return StirImageData(filepath_or_object)
285
+ else:
286
+ raise TypeError(
287
+ f"Cannot create image from type {type(filepath_or_object)}. "
288
+ f"Expected str, ImageDataInterface, or FloatVoxelsOnCartesianGrid."
289
+ )
290
+
291
+
292
+ def create_acquisition_data(
293
+ filepath_or_object: Optional[
294
+ Union[str, AcquisitionDataInterface, "AcquisitionData", "ProjData"]
295
+ ] = None,
296
+ ) -> AcquisitionDataInterface:
297
+ """Create an acquisition data object using the current backend.
298
+
299
+ Args:
300
+ filepath_or_object: Can be:
301
+ - str: Path to acquisition file to load
302
+ - AcquisitionDataInterface: Already wrapped object (returned as-is)
303
+ - Native SIRF/STIR object: Will be wrapped
304
+ - None: Creates empty object (SIRF only)
305
+
306
+ Returns:
307
+ AcquisitionDataInterface: Wrapped acquisition object
308
+
309
+ Raises:
310
+ NotImplementedError: If trying to create empty acquisition with STIR backend
311
+ TypeError: If input type is not supported
312
+ """
313
+ # Already wrapped?
314
+ if isinstance(filepath_or_object, AcquisitionDataInterface):
315
+ return filepath_or_object
316
+
317
+ backend_hint = detect_acquisition_backend(filepath_or_object)
318
+ if backend_hint is None:
319
+ backend_hint = detect_backend_from_interface(filepath_or_object)
320
+
321
+ backend = _activate_backend(backend_hint) or get_backend()
322
+
323
+ if backend == "sirf":
324
+ from .sirf_backend import SirfAcquisitionData
325
+
326
+ try:
327
+ from sirf.STIR import AcquisitionData
328
+ except ImportError:
329
+ AcquisitionData = type(None)
330
+
331
+ if filepath_or_object is None:
332
+ return SirfAcquisitionData.create_empty()
333
+ elif isinstance(filepath_or_object, str):
334
+ return SirfAcquisitionData.read_from_file(filepath_or_object)
335
+ elif AcquisitionData is not type(None) and isinstance(
336
+ filepath_or_object, AcquisitionData
337
+ ):
338
+ # Wrap native SIRF object
339
+ return SirfAcquisitionData(filepath_or_object)
340
+ else:
341
+ raise TypeError(
342
+ f"Cannot create acquisition from type {type(filepath_or_object)}. "
343
+ f"Expected str, AcquisitionDataInterface, or AcquisitionData."
344
+ )
345
+
346
+ elif backend == "stir":
347
+ from .stir_backend import StirAcquisitionData
348
+
349
+ try:
350
+ import stir
351
+
352
+ ProjData = stir.ProjData
353
+ except ImportError:
354
+ ProjData = type(None)
355
+
356
+ if filepath_or_object is None:
357
+ raise NotImplementedError(
358
+ "Creating empty STIR ProjData requires exam_info and proj_data_info. "
359
+ "Please provide a filepath or use "
360
+ "StirAcquisitionData.create_empty() directly."
361
+ )
362
+ elif isinstance(filepath_or_object, str):
363
+ return StirAcquisitionData.read_from_file(filepath_or_object)
364
+ elif ProjData is not type(None) and isinstance(filepath_or_object, ProjData):
365
+ # Wrap native STIR object
366
+ return StirAcquisitionData(filepath_or_object)
367
+ else:
368
+ raise TypeError(
369
+ f"Cannot create acquisition from type {type(filepath_or_object)}. "
370
+ f"Expected str, AcquisitionDataInterface, or ProjData."
371
+ )
372
+
373
+
374
+ def load_image_data(
375
+ filepath: str, backend: Optional[Literal["sirf", "stir"]] = None
376
+ ) -> ImageDataInterface:
377
+ """Load image data from file for the requested backend.
378
+
379
+ Args:
380
+ filepath: Path to the image header file to load.
381
+ backend: Optional backend override. If omitted, use auto-detected backend.
382
+
383
+ Returns:
384
+ ImageDataInterface: Wrapped backend-specific image object.
385
+
386
+ Raises:
387
+ ValueError: If backend is explicitly provided but invalid.
388
+ """
389
+ backend_to_use = backend or get_backend()
390
+
391
+ if backend_to_use == "sirf":
392
+ from .sirf_backend import SirfImageData
393
+
394
+ return SirfImageData.read_from_file(filepath)
395
+ if backend_to_use == "stir":
396
+ from .stir_backend import StirImageData
397
+
398
+ return StirImageData.read_from_file(filepath)
399
+
400
+ raise ValueError("Invalid backend. Expected 'sirf' or 'stir'.")
401
+
402
+
403
+ def load_acquisition_data(
404
+ filepath: str, backend: Optional[Literal["sirf", "stir"]] = None
405
+ ) -> AcquisitionDataInterface:
406
+ """Load acquisition data from file for the requested backend.
407
+
408
+ Args:
409
+ filepath: Path to the acquisition header file to load.
410
+ backend: Optional backend override. If omitted, use auto-detected backend.
411
+
412
+ Returns:
413
+ AcquisitionDataInterface: Wrapped backend-specific acquisition object.
414
+
415
+ Raises:
416
+ ValueError: If backend is explicitly provided but invalid.
417
+ """
418
+ backend_to_use = backend or get_backend()
419
+
420
+ if backend_to_use == "sirf":
421
+ from .sirf_backend import SirfAcquisitionData
422
+
423
+ return SirfAcquisitionData.read_from_file(filepath)
424
+ if backend_to_use == "stir":
425
+ from .stir_backend import StirAcquisitionData
426
+
427
+ return StirAcquisitionData.read_from_file(filepath)
428
+
429
+ raise ValueError("Invalid backend. Expected 'sirf' or 'stir'.")
430
+
431
+
432
+ def is_sirf_backend() -> bool:
433
+ """Check if SIRF backend is active.
434
+
435
+ Returns:
436
+ bool: True if SIRF is the active backend
437
+ """
438
+ return get_backend() == "sirf"
439
+
440
+
441
+ def is_stir_backend() -> bool:
442
+ """Check if STIR Python backend is active.
443
+
444
+ Returns:
445
+ bool: True if STIR is the active backend
446
+ """
447
+ return get_backend() == "stir"
448
+
449
+
450
+ def unwrap(obj: Union[ImageDataInterface, AcquisitionDataInterface]):
451
+ """Get the native object from a wrapped backend object.
452
+
453
+ Args:
454
+ obj: Wrapped image or acquisition data
455
+
456
+ Returns:
457
+ The underlying SIRF or STIR object
458
+ """
459
+ if hasattr(obj, "native_object"):
460
+ return obj.native_object
461
+ return obj
462
+
463
+
464
+ __all__ = [
465
+ "get_backend",
466
+ "set_backend",
467
+ "reset_backend",
468
+ "detect_image_backend",
469
+ "detect_acquisition_backend",
470
+ "detect_backend_from_interface",
471
+ "create_image_data",
472
+ "create_acquisition_data",
473
+ "load_image_data",
474
+ "load_acquisition_data",
475
+ "is_sirf_backend",
476
+ "is_stir_backend",
477
+ "unwrap",
478
+ "ImageDataInterface",
479
+ "AcquisitionDataInterface",
480
+ ]