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,395 @@
1
+ """
2
+ STIR Python backend implementation.
3
+
4
+ Provides wrappers around stir.FloatVoxelsOnCartesianGrid and stir.ProjData
5
+ that conform to the backend interface.
6
+ """
7
+
8
+ from typing import Tuple, Union
9
+
10
+ import numpy as np
11
+
12
+
13
+ try:
14
+ import stir
15
+ import stirextra
16
+
17
+ STIR_AVAILABLE = True
18
+ except ImportError:
19
+ STIR_AVAILABLE = False
20
+
21
+ from .base import AcquisitionDataInterface, ImageDataInterface
22
+
23
+
24
+ class StirImageData(ImageDataInterface):
25
+ """Wrapper for stir.FloatVoxelsOnCartesianGrid."""
26
+
27
+ def __init__(self, stir_obj):
28
+ """Initialize wrapper.
29
+
30
+ Args:
31
+ stir_obj: STIR FloatVoxelsOnCartesianGrid object
32
+ """
33
+ if not STIR_AVAILABLE:
34
+ raise ImportError("STIR Python is not available")
35
+ self._obj = stir_obj
36
+
37
+ @classmethod
38
+ def read_from_file(cls, filepath: str) -> "StirImageData":
39
+ """Read image from file."""
40
+ obj = stir.FloatVoxelsOnCartesianGrid.read_from_file(filepath)
41
+ return cls(obj)
42
+
43
+ @classmethod
44
+ def create_empty(
45
+ cls, exam_info=None, index_range=None, origin=None
46
+ ) -> "StirImageData":
47
+ """Create empty image.
48
+
49
+ Args:
50
+ exam_info: ExamInfo object (optional)
51
+ index_range: IndexRange object (optional)
52
+ origin: CartesianCoordinate3D for origin (optional)
53
+
54
+ Returns:
55
+ StirImageData: Empty image
56
+ """
57
+ # This is complex - STIR requires geometry info to create images
58
+ # For now, raise NotImplementedError
59
+ raise NotImplementedError(
60
+ "Creating empty STIR images requires geometry information. "
61
+ "Use read_from_file or create from template."
62
+ )
63
+
64
+ def write(self, filepath: str) -> None:
65
+ """Write to file.
66
+
67
+ Note: STIR uses write_to_file instead of write.
68
+ """
69
+ self._obj.write_to_file(filepath)
70
+
71
+ def fill(self, data: Union[np.ndarray, float]) -> None:
72
+ """Fill with data."""
73
+ if isinstance(data, (int, float)) or not isinstance(data, np.ndarray):
74
+ self._obj.fill(float(data))
75
+ else:
76
+ self._obj.fill(data.flat)
77
+
78
+ def clone(self) -> "StirImageData":
79
+ """Clone the image."""
80
+ # STIR doesn't have a direct clone method
81
+ # We need to create a new object and copy data
82
+ cloned = self._obj.get_empty_copy()
83
+ cloned.fill(self.as_array().flat)
84
+ return StirImageData(cloned)
85
+
86
+ def get_uniform_copy(self, value: float) -> "StirImageData":
87
+ """Create uniform copy."""
88
+ cloned = self.clone()
89
+ cloned._obj.fill(value)
90
+ return cloned
91
+
92
+ def dimensions(self) -> Tuple[int, int, int]:
93
+ """Get dimensions as (z, y, x)."""
94
+ # Extract from numpy array shape
95
+ arr = stirextra.to_numpy(self._obj)
96
+ # STIR arrays are typically (z, y, x)
97
+ return arr.shape
98
+
99
+ def voxel_sizes(self) -> Tuple[float, float, float]:
100
+ """Get voxel sizes as (z, y, x) in mm."""
101
+ # Access STIR's grid spacing
102
+ grid_spacing = self._obj.get_grid_spacing()
103
+ # grid_spacing is a CartesianCoordinate3D with [z, y, x] ordering
104
+ return (grid_spacing[1], grid_spacing[2], grid_spacing[3])
105
+
106
+ def as_array(self) -> np.ndarray:
107
+ """Convert to numpy array using stirextra."""
108
+ return stirextra.to_numpy(self._obj)
109
+
110
+ def sum(self) -> float:
111
+ """Sum all values."""
112
+ return float(self._obj.sum())
113
+
114
+ def max(self) -> float:
115
+ """Return the maximum voxel value."""
116
+ return float(np.max(self.as_array()))
117
+
118
+ def maximum(self, value: float) -> None:
119
+ """Apply element-wise maximum.
120
+
121
+ Note: STIR may not have this method directly.
122
+ """
123
+ # STIR doesn't have a direct maximum method so we need to extract array
124
+ # and fill a clone
125
+ arr = self.as_array()
126
+ arr = np.maximum(arr, value)
127
+ self.fill(arr)
128
+
129
+ def minimum(self, value: float) -> None:
130
+ """Apply element-wise minimum.
131
+
132
+ Note: STIR may not have this method directly.
133
+ """
134
+ arr = self.as_array()
135
+ arr = np.minimum(arr, value)
136
+ self.fill(arr)
137
+
138
+ @property
139
+ def native_object(self):
140
+ """Get underlying STIR object."""
141
+ return self._obj
142
+
143
+ def __repr__(self) -> str:
144
+ return f"StirImageData(dims={self.dimensions()})"
145
+
146
+ # Arithmetic operations - delegate to underlying STIR object
147
+ def __add__(self, other):
148
+ if isinstance(other, StirImageData):
149
+ return StirImageData(self._obj + other._obj)
150
+ else:
151
+ return StirImageData(self._obj + float(other))
152
+
153
+ def __sub__(self, other):
154
+ if isinstance(other, StirImageData):
155
+ return StirImageData(self._obj - other._obj)
156
+ else:
157
+ return StirImageData(self._obj - float(other))
158
+
159
+ def __mul__(self, other):
160
+ if isinstance(other, StirImageData):
161
+ return StirImageData(self._obj * other._obj)
162
+ else:
163
+ return StirImageData(self._obj * float(other))
164
+
165
+ def __truediv__(self, other):
166
+ if isinstance(other, StirImageData):
167
+ return StirImageData(self._obj / other._obj)
168
+ else:
169
+ return StirImageData(self._obj / float(other))
170
+
171
+ def __radd__(self, other):
172
+ return StirImageData(float(other) + self._obj)
173
+
174
+ def __rsub__(self, other):
175
+ return StirImageData(float(other) - self._obj)
176
+
177
+ def __rmul__(self, other):
178
+ return StirImageData(float(other) * self._obj)
179
+
180
+ def __rtruediv__(self, other):
181
+ return StirImageData(float(other) / self._obj)
182
+
183
+ def __neg__(self):
184
+ return StirImageData(-self._obj)
185
+
186
+ def sapyb(self, a: float, y: "StirImageData", b: float) -> "StirImageData":
187
+ """Compute self * a + y * b (element-wise), modifying self in place.
188
+
189
+ Args:
190
+ a: Scalar multiplier for self
191
+ y: Other image
192
+ b: Scalar multiplier for y
193
+
194
+ Returns:
195
+ StirImageData: self after modification (for chaining)
196
+ """
197
+ y_obj = y._obj if isinstance(y, StirImageData) else y
198
+ self._obj.sapyb(a, y_obj, b)
199
+ return self
200
+
201
+
202
+ class StirAcquisitionData(AcquisitionDataInterface):
203
+ """Wrapper for stir.ProjDataInMemory.
204
+
205
+ Always uses ProjDataInMemory to ensure arithmetic operations are available.
206
+ """
207
+
208
+ def __init__(self, stir_obj):
209
+ """Initialize wrapper.
210
+
211
+ Args:
212
+ stir_obj: STIR ProjData or ProjDataInMemory object
213
+ """
214
+ if not STIR_AVAILABLE:
215
+ raise ImportError("STIR Python is not available")
216
+
217
+ # Always convert to ProjDataInMemory for arithmetic support
218
+ if isinstance(stir_obj, stir.ProjDataInMemory):
219
+ self._obj = stir_obj
220
+ else:
221
+ self._obj = stir.ProjDataInMemory(stir_obj)
222
+
223
+ @classmethod
224
+ def read_from_file(cls, filepath: str) -> "StirAcquisitionData":
225
+ """Read projection data from file and convert to ProjDataInMemory."""
226
+ obj = stir.ProjData.read_from_file(filepath)
227
+ # Constructor will convert to ProjDataInMemory
228
+ return cls(obj)
229
+
230
+ @classmethod
231
+ def create_empty(cls, exam_info=None, proj_data_info=None) -> "StirAcquisitionData":
232
+ """Create empty projection data.
233
+
234
+ Args:
235
+ exam_info: ExamInfo object (required)
236
+ proj_data_info: ProjDataInfo object (required)
237
+
238
+ Returns:
239
+ StirAcquisitionData: Empty projection data
240
+ """
241
+ if exam_info is None or proj_data_info is None:
242
+ raise ValueError(
243
+ "Creating empty STIR ProjData requires exam_info and proj_data_info"
244
+ )
245
+ obj = stir.ProjDataInMemory(exam_info, proj_data_info)
246
+ return cls(obj)
247
+
248
+ def write(self, filepath: str) -> None:
249
+ """Write to file.
250
+
251
+ Note: STIR uses write_to_file instead of write.
252
+ """
253
+ self._obj.write_to_file(filepath)
254
+
255
+ def fill(self, data: Union[np.ndarray, float]) -> None:
256
+ """Fill with data."""
257
+ if isinstance(data, (int, float)):
258
+ self._obj.fill(float(data))
259
+ elif isinstance(data, np.ndarray):
260
+ self._obj.fill(data.flat)
261
+ else:
262
+ try:
263
+ self._obj.fill(data)
264
+ except Exception as e:
265
+ raise ValueError(
266
+ "Filling STIR ProjData with arbitrary data is not supported"
267
+ ) from e
268
+
269
+ def clone(self) -> "StirAcquisitionData":
270
+ """Clone the projection data."""
271
+ # ProjDataInMemory copy constructor creates a deep copy
272
+ cloned = stir.ProjDataInMemory(self._obj)
273
+ return StirAcquisitionData(cloned)
274
+
275
+ def get_uniform_copy(self, value: float) -> "StirAcquisitionData":
276
+ """Create uniform copy."""
277
+ cloned = self.clone()
278
+ cloned._obj.fill(value)
279
+ return cloned
280
+
281
+ def dimensions(self) -> Tuple:
282
+ """Get dimensions."""
283
+ # Extract from numpy array
284
+ arr = stirextra.to_numpy(self._obj)
285
+ return arr.shape
286
+
287
+ def as_array(self) -> np.ndarray:
288
+ """Convert to numpy array using stirextra."""
289
+ return stirextra.to_numpy(self._obj)
290
+
291
+ def sum(self) -> float:
292
+ """Sum all values."""
293
+ return float(self._obj.sum())
294
+
295
+ def max(self) -> float:
296
+ """Return the maximum bin value."""
297
+ return float(np.max(self.as_array()))
298
+
299
+ def maximum(self, value: float) -> None:
300
+ """Apply element-wise maximum."""
301
+ arr = self.as_array()
302
+ arr = np.maximum(arr, value)
303
+ self.fill(arr)
304
+
305
+ def minimum(self, value: float) -> None:
306
+ """Apply element-wise minimum.
307
+
308
+ Note: STIR may not have this method directly.
309
+ """
310
+ arr = self.as_array()
311
+ arr = np.minimum(arr, value)
312
+ self.fill(arr)
313
+
314
+ def get_info(self) -> str:
315
+ """Get metadata info."""
316
+ # STIR doesn't have a direct get_info() string method
317
+ # Construct info from proj_data_info
318
+ info = self._obj.get_proj_data_info()
319
+ return str(info)
320
+
321
+ def get_energy_window_bounds(self) -> Tuple[float, float]:
322
+ """Get the energy window bounds for the acquisition.
323
+
324
+ Returns:
325
+ Tuple[float, float]: (lower_threshold, upper_threshold) in keV
326
+ """
327
+ exam_info = self._obj.get_exam_info()
328
+ lower_thres = float(exam_info.get_low_energy_thres())
329
+ upper_thres = float(exam_info.get_high_energy_thres())
330
+ return (lower_thres, upper_thres)
331
+
332
+ @property
333
+ def native_object(self):
334
+ """Get underlying STIR object."""
335
+ return self._obj
336
+
337
+ def __repr__(self) -> str:
338
+ return f"StirAcquisitionData(dims={self.dimensions()})"
339
+
340
+ # Arithmetic operations - delegate to underlying ProjDataInMemory object
341
+ def __add__(self, other):
342
+ if isinstance(other, StirAcquisitionData):
343
+ return StirAcquisitionData(self._obj + other._obj)
344
+ else:
345
+ return StirAcquisitionData(self._obj + float(other))
346
+
347
+ def __sub__(self, other):
348
+ if isinstance(other, StirAcquisitionData):
349
+ return StirAcquisitionData(self._obj - other._obj)
350
+ else:
351
+ return StirAcquisitionData(self._obj - float(other))
352
+
353
+ def __mul__(self, other):
354
+ if isinstance(other, StirAcquisitionData):
355
+ return StirAcquisitionData(self._obj * other._obj)
356
+ else:
357
+ return StirAcquisitionData(self._obj * float(other))
358
+
359
+ def __truediv__(self, other):
360
+ if isinstance(other, StirAcquisitionData):
361
+ return StirAcquisitionData(self._obj / other._obj)
362
+ else:
363
+ return StirAcquisitionData(self._obj / float(other))
364
+
365
+ def __radd__(self, other):
366
+ return StirAcquisitionData(float(other) + self._obj)
367
+
368
+ def __rsub__(self, other):
369
+ return StirAcquisitionData(float(other) - self._obj)
370
+
371
+ def __rmul__(self, other):
372
+ return StirAcquisitionData(float(other) * self._obj)
373
+
374
+ def __rtruediv__(self, other):
375
+ return StirAcquisitionData(float(other) / self._obj)
376
+
377
+ def __neg__(self):
378
+ return StirAcquisitionData(-self._obj)
379
+
380
+ def sapyb(
381
+ self, a: float, y: "StirAcquisitionData", b: float
382
+ ) -> "StirAcquisitionData":
383
+ """Compute self * a + y * b (element-wise), modifying self in place.
384
+
385
+ Args:
386
+ a: Scalar multiplier for self
387
+ y: Other acquisition data
388
+ b: Scalar multiplier for y
389
+
390
+ Returns:
391
+ StirAcquisitionData: self after modification (for chaining)
392
+ """
393
+ y_obj = y._obj if isinstance(y, StirAcquisitionData) else y
394
+ self._obj.sapyb(a, y_obj, b)
395
+ return self
@@ -0,0 +1,19 @@
1
+ """
2
+ Numerical core: configuration acquisition data and image builders for SPECT.
3
+ """
4
+
5
+ import importlib
6
+
7
+
8
+ # Lazy imports to avoid SIRF dependencies in CI
9
+ def __getattr__(name):
10
+ if name == "STIRSPECTAcquisitionDataBuilder":
11
+ mod = importlib.import_module(".acquisition_builder", __name__)
12
+ return getattr(mod, "STIRSPECTAcquisitionDataBuilder")
13
+ elif name == "STIRSPECTImageDataBuilder":
14
+ mod = importlib.import_module(".image_builder", __name__)
15
+ return getattr(mod, "STIRSPECTImageDataBuilder")
16
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
17
+
18
+
19
+ __all__ = ["STIRSPECTAcquisitionDataBuilder", "STIRSPECTImageDataBuilder"]