simind-python-connector 1.1.1__py3-none-any.whl → 1.2.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 (38) hide show
  1. simind_python_connector/__init__.py +2 -11
  2. simind_python_connector/_version.py +3 -0
  3. simind_python_connector/backends/__init__.py +34 -31
  4. simind_python_connector/backends/base.py +7 -52
  5. simind_python_connector/backends/sirf_backend.py +19 -23
  6. simind_python_connector/backends/stir_backend.py +74 -46
  7. simind_python_connector/builders/__init__.py +2 -2
  8. simind_python_connector/builders/acquisition_builder.py +8 -25
  9. simind_python_connector/builders/image_builder.py +4 -4
  10. simind_python_connector/configs/__init__.py +1 -2
  11. simind_python_connector/connectors/__init__.py +2 -2
  12. simind_python_connector/connectors/_spacing.py +115 -11
  13. simind_python_connector/connectors/base.py +35 -2
  14. simind_python_connector/connectors/python_connector.py +451 -106
  15. simind_python_connector/connectors/pytomography_adaptor.py +62 -29
  16. simind_python_connector/connectors/sirf_adaptor.py +47 -16
  17. simind_python_connector/connectors/stir_adaptor.py +50 -16
  18. simind_python_connector/converters/attenuation.py +1 -10
  19. simind_python_connector/converters/simind_to_stir.py +92 -60
  20. simind_python_connector/core/config.py +420 -108
  21. simind_python_connector/core/executor.py +268 -31
  22. simind_python_connector/core/types.py +21 -8
  23. simind_python_connector/data/__init__.py +5 -3
  24. simind_python_connector/utils/__init__.py +10 -20
  25. simind_python_connector/utils/backend_access.py +4 -4
  26. simind_python_connector/utils/geometry_phantom.py +42 -0
  27. simind_python_connector/utils/import_helpers.py +37 -10
  28. simind_python_connector/utils/interfile_numpy.py +26 -5
  29. simind_python_connector/utils/interfile_parser.py +9 -10
  30. simind_python_connector/utils/simind_utils.py +31 -8
  31. simind_python_connector/utils/sirf_stir_utils.py +11 -11
  32. simind_python_connector/utils/stir_utils.py +12 -18
  33. {simind_python_connector-1.1.1.dist-info → simind_python_connector-1.2.0.dist-info}/METADATA +30 -12
  34. simind_python_connector-1.2.0.dist-info/RECORD +49 -0
  35. simind_python_connector-1.1.1.dist-info/RECORD +0 -47
  36. {simind_python_connector-1.1.1.dist-info → simind_python_connector-1.2.0.dist-info}/WHEEL +0 -0
  37. {simind_python_connector-1.1.1.dist-info → simind_python_connector-1.2.0.dist-info}/licenses/LICENSE +0 -0
  38. {simind_python_connector-1.1.1.dist-info → simind_python_connector-1.2.0.dist-info}/top_level.txt +0 -0
@@ -3,18 +3,9 @@ simind-python-connector connector/adaptor API.
3
3
  """
4
4
 
5
5
  import importlib
6
- from importlib import metadata as _meta
7
6
  from typing import Any
8
7
 
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.1.1"
8
+ from ._version import __version__ as __version__
18
9
 
19
10
 
20
11
  def __getattr__(name: str) -> Any:
@@ -58,9 +49,9 @@ __all__ = [
58
49
  "ProjectionResult",
59
50
  "PyTomographySimindAdaptor",
60
51
  "RuntimeOperator",
61
- "SirfSimindAdaptor",
62
52
  "SimindPythonConnector",
63
53
  "SimulationConfig",
54
+ "SirfSimindAdaptor",
64
55
  "StirSimindAdaptor",
65
56
  "builders",
66
57
  "configs",
@@ -0,0 +1,3 @@
1
+ """Canonical package version, also read by packaging and release tooling."""
2
+
3
+ __version__ = "1.2.0"
@@ -20,8 +20,9 @@ Usage:
20
20
  import importlib
21
21
  import logging
22
22
  import os
23
- from typing import TYPE_CHECKING, Any, Literal, Optional, Union
23
+ from typing import TYPE_CHECKING, Any, Literal, Union
24
24
 
25
+ from ..utils.import_helpers import _import_stir_extra
25
26
  from .base import AcquisitionDataInterface, ImageDataInterface
26
27
 
27
28
 
@@ -43,11 +44,11 @@ if TYPE_CHECKING:
43
44
  FloatVoxelsOnCartesianGrid = Any # type: ignore[assignment]
44
45
 
45
46
  # Global backend state
46
- _backend: Optional[str] = None
47
+ _backend: str | None = None
47
48
  _backend_initialized = False
48
49
 
49
50
 
50
- def _import_sirf_types() -> tuple[Optional[type], Optional[type]]:
51
+ def _import_sirf_types() -> tuple[type | None, type | None]:
51
52
  """Import SIRF image/acquisition types lazily."""
52
53
  try:
53
54
  from sirf.STIR import AcquisitionData as SirfAcquisitionData # type: ignore
@@ -57,7 +58,7 @@ def _import_sirf_types() -> tuple[Optional[type], Optional[type]]:
57
58
  return SirfImageData, SirfAcquisitionData
58
59
 
59
60
 
60
- def _import_stir_types() -> tuple[Optional[type], Optional[type]]:
61
+ def _import_stir_types() -> tuple[type | None, type | None]:
61
62
  """Import STIR image/acquisition types lazily."""
62
63
  try:
63
64
  from stir import FloatVoxelsOnCartesianGrid as StirImageData # type: ignore
@@ -68,7 +69,7 @@ def _import_stir_types() -> tuple[Optional[type], Optional[type]]:
68
69
  return StirImageData, StirProjData
69
70
 
70
71
 
71
- def detect_image_backend(obj: Any) -> Optional[str]:
72
+ def detect_image_backend(obj: Any) -> str | None:
72
73
  """Return 'sirf' or 'stir' if obj is a native image type."""
73
74
  if obj is None:
74
75
  return None
@@ -84,7 +85,7 @@ def detect_image_backend(obj: Any) -> Optional[str]:
84
85
  return None
85
86
 
86
87
 
87
- def detect_acquisition_backend(obj: Any) -> Optional[str]:
88
+ def detect_acquisition_backend(obj: Any) -> str | None:
88
89
  """Return 'sirf' or 'stir' if obj is a native acquisition type."""
89
90
  if obj is None:
90
91
  return None
@@ -101,8 +102,8 @@ def detect_acquisition_backend(obj: Any) -> Optional[str]:
101
102
 
102
103
 
103
104
  def detect_backend_from_interface(
104
- obj: Union[ImageDataInterface, AcquisitionDataInterface, Any],
105
- ) -> Optional[str]:
105
+ obj: ImageDataInterface | AcquisitionDataInterface | Any,
106
+ ) -> str | None:
106
107
  """Detect backend by unwrapping known interface objects."""
107
108
  native = getattr(obj, "native_object", None)
108
109
  if native is None:
@@ -115,7 +116,7 @@ def detect_backend_from_interface(
115
116
  return detect_acquisition_backend(native)
116
117
 
117
118
 
118
- def _activate_backend(preferred: Optional[str]) -> Optional[str]:
119
+ def _activate_backend(preferred: str | None) -> str | None:
119
120
  """Force-activate a specific backend if requested."""
120
121
  if preferred is None:
121
122
  return None
@@ -150,7 +151,7 @@ def get_backend() -> str:
150
151
  except ImportError:
151
152
  try:
152
153
  importlib.import_module("stir")
153
- importlib.import_module("stirextra")
154
+ _import_stir_extra()
154
155
 
155
156
  _backend = "stir"
156
157
  logger.info("Auto-detected STIR Python backend")
@@ -159,7 +160,7 @@ def get_backend() -> str:
159
160
  "Neither SIRF nor STIR Python found. "
160
161
  "Please install one of: "
161
162
  "\n - SIRF (sirf.STIR)"
162
- "\n - STIR Python (stir + stirextra)"
163
+ "\n - STIR Python (stir + stir.extra)"
163
164
  )
164
165
 
165
166
  _backend_initialized = True
@@ -190,7 +191,7 @@ def set_backend(backend: Literal["sirf", "stir"]) -> None:
190
191
  elif backend == "stir":
191
192
  try:
192
193
  importlib.import_module("stir")
193
- importlib.import_module("stirextra")
194
+ _import_stir_extra()
194
195
  except ImportError:
195
196
  raise ImportError("STIR Python is not available")
196
197
 
@@ -210,9 +211,10 @@ def reset_backend() -> None:
210
211
 
211
212
 
212
213
  def create_image_data(
213
- filepath_or_object: Optional[
214
- Union[str, ImageDataInterface, "ImageData", "FloatVoxelsOnCartesianGrid"]
215
- ] = None,
214
+ filepath_or_object: Union[
215
+ str, ImageDataInterface, "ImageData", "FloatVoxelsOnCartesianGrid"
216
+ ]
217
+ | None = None,
216
218
  ) -> ImageDataInterface:
217
219
  """Create an image data object using the current backend.
218
220
 
@@ -294,9 +296,10 @@ def create_image_data(
294
296
 
295
297
 
296
298
  def create_acquisition_data(
297
- filepath_or_object: Optional[
298
- Union[str, AcquisitionDataInterface, "AcquisitionData", "ProjData"]
299
- ] = None,
299
+ filepath_or_object: Union[
300
+ str, AcquisitionDataInterface, "AcquisitionData", "ProjData"
301
+ ]
302
+ | None = None,
300
303
  ) -> AcquisitionDataInterface:
301
304
  """Create an acquisition data object using the current backend.
302
305
 
@@ -379,7 +382,7 @@ def create_acquisition_data(
379
382
 
380
383
 
381
384
  def load_image_data(
382
- filepath: str, backend: Optional[Literal["sirf", "stir"]] = None
385
+ filepath: str, backend: Literal["sirf", "stir"] | None = None
383
386
  ) -> ImageDataInterface:
384
387
  """Load image data from file for the requested backend.
385
388
 
@@ -408,7 +411,7 @@ def load_image_data(
408
411
 
409
412
 
410
413
  def load_acquisition_data(
411
- filepath: str, backend: Optional[Literal["sirf", "stir"]] = None
414
+ filepath: str, backend: Literal["sirf", "stir"] | None = None
412
415
  ) -> AcquisitionDataInterface:
413
416
  """Load acquisition data from file for the requested backend.
414
417
 
@@ -454,7 +457,7 @@ def is_stir_backend() -> bool:
454
457
  return get_backend() == "stir"
455
458
 
456
459
 
457
- def unwrap(obj: Union[ImageDataInterface, AcquisitionDataInterface]):
460
+ def unwrap(obj: ImageDataInterface | AcquisitionDataInterface):
458
461
  """Get the native object from a wrapped backend object.
459
462
 
460
463
  Args:
@@ -469,19 +472,19 @@ def unwrap(obj: Union[ImageDataInterface, AcquisitionDataInterface]):
469
472
 
470
473
 
471
474
  __all__ = [
472
- "get_backend",
473
- "set_backend",
474
- "reset_backend",
475
- "detect_image_backend",
475
+ "AcquisitionDataInterface",
476
+ "ImageDataInterface",
477
+ "create_acquisition_data",
478
+ "create_image_data",
476
479
  "detect_acquisition_backend",
477
480
  "detect_backend_from_interface",
478
- "create_image_data",
479
- "create_acquisition_data",
480
- "load_image_data",
481
- "load_acquisition_data",
481
+ "detect_image_backend",
482
+ "get_backend",
482
483
  "is_sirf_backend",
483
484
  "is_stir_backend",
485
+ "load_acquisition_data",
486
+ "load_image_data",
487
+ "reset_backend",
488
+ "set_backend",
484
489
  "unwrap",
485
- "ImageDataInterface",
486
- "AcquisitionDataInterface",
487
490
  ]
@@ -6,7 +6,6 @@ for working with both SIRF and STIR Python libraries.
6
6
  """
7
7
 
8
8
  from abc import ABC, abstractmethod
9
- from typing import Tuple, Union
10
9
 
11
10
  import numpy as np
12
11
 
@@ -28,7 +27,6 @@ class ImageDataInterface(ABC):
28
27
  Returns:
29
28
  ImageDataInterface: Wrapped image object
30
29
  """
31
- pass
32
30
 
33
31
  @abstractmethod
34
32
  def write(self, filepath: str) -> None:
@@ -37,16 +35,14 @@ class ImageDataInterface(ABC):
37
35
  Args:
38
36
  filepath: Output file path
39
37
  """
40
- pass
41
38
 
42
39
  @abstractmethod
43
- def fill(self, data: Union[np.ndarray, float]) -> None:
40
+ def fill(self, data: np.ndarray | float) -> None:
44
41
  """Fill image with data.
45
42
 
46
43
  Args:
47
44
  data: Numpy array or scalar value
48
45
  """
49
- pass
50
46
 
51
47
  @abstractmethod
52
48
  def clone(self) -> "ImageDataInterface":
@@ -55,7 +51,6 @@ class ImageDataInterface(ABC):
55
51
  Returns:
56
52
  ImageDataInterface: Cloned image object
57
53
  """
58
- pass
59
54
 
60
55
  @abstractmethod
61
56
  def get_uniform_copy(self, value: float) -> "ImageDataInterface":
@@ -67,25 +62,22 @@ class ImageDataInterface(ABC):
67
62
  Returns:
68
63
  ImageDataInterface: New uniform image
69
64
  """
70
- pass
71
65
 
72
66
  @abstractmethod
73
- def dimensions(self) -> Tuple[int, int, int]:
67
+ def dimensions(self) -> tuple[int, int, int]:
74
68
  """Get image dimensions.
75
69
 
76
70
  Returns:
77
71
  Tuple[int, int, int]: (z, y, x) dimensions
78
72
  """
79
- pass
80
73
 
81
74
  @abstractmethod
82
- def voxel_sizes(self) -> Tuple[float, float, float]:
75
+ def voxel_sizes(self) -> tuple[float, float, float]:
83
76
  """Get voxel sizes in mm.
84
77
 
85
78
  Returns:
86
79
  Tuple[float, float, float]: (z, y, x) voxel sizes
87
80
  """
88
- pass
89
81
 
90
82
  @abstractmethod
91
83
  def as_array(self) -> np.ndarray:
@@ -94,7 +86,6 @@ class ImageDataInterface(ABC):
94
86
  Returns:
95
87
  np.ndarray: Image data as numpy array
96
88
  """
97
- pass
98
89
 
99
90
  @abstractmethod
100
91
  def sum(self) -> float:
@@ -103,7 +94,6 @@ class ImageDataInterface(ABC):
103
94
  Returns:
104
95
  float: Sum of all voxel values
105
96
  """
106
- pass
107
97
 
108
98
  @abstractmethod
109
99
  def max(self) -> float:
@@ -112,7 +102,6 @@ class ImageDataInterface(ABC):
112
102
  Returns:
113
103
  float: Maximum voxel value
114
104
  """
115
- pass
116
105
 
117
106
  @abstractmethod
118
107
  def maximum(self, value: float) -> None:
@@ -121,7 +110,6 @@ class ImageDataInterface(ABC):
121
110
  Args:
122
111
  value: Minimum value to clip to
123
112
  """
124
- pass
125
113
 
126
114
  @property
127
115
  @abstractmethod
@@ -131,53 +119,43 @@ class ImageDataInterface(ABC):
131
119
  Returns either SIRF ``ImageData`` or STIR
132
120
  ``FloatVoxelsOnCartesianGrid``.
133
121
  """
134
- pass
135
122
 
136
123
  # Arithmetic operations
137
124
  @abstractmethod
138
125
  def __add__(self, other):
139
126
  """Add two images or image and scalar."""
140
- pass
141
127
 
142
128
  @abstractmethod
143
129
  def __sub__(self, other):
144
130
  """Subtract two images or image and scalar."""
145
- pass
146
131
 
147
132
  @abstractmethod
148
133
  def __mul__(self, other):
149
134
  """Multiply image by scalar or element-wise."""
150
- pass
151
135
 
152
136
  @abstractmethod
153
137
  def __truediv__(self, other):
154
138
  """Divide image by scalar or element-wise."""
155
- pass
156
139
 
157
140
  @abstractmethod
158
141
  def __radd__(self, other):
159
142
  """Right-hand addition."""
160
- pass
161
143
 
162
144
  @abstractmethod
163
145
  def __rsub__(self, other):
164
146
  """Right-hand subtraction."""
165
- pass
166
147
 
167
148
  @abstractmethod
168
149
  def __rmul__(self, other):
169
150
  """Right-hand multiplication."""
170
- pass
171
151
 
172
152
  @abstractmethod
173
153
  def __rtruediv__(self, other):
174
154
  """Right-hand division."""
175
- pass
176
155
 
177
156
  @abstractmethod
178
157
  def __neg__(self):
179
158
  """Negate image."""
180
- pass
181
159
 
182
160
  @abstractmethod
183
161
  def sapyb(
@@ -195,7 +173,6 @@ class ImageDataInterface(ABC):
195
173
  Returns:
196
174
  ImageDataInterface: self after modification (for chaining)
197
175
  """
198
- pass
199
176
 
200
177
 
201
178
  class AcquisitionDataInterface(ABC):
@@ -212,7 +189,6 @@ class AcquisitionDataInterface(ABC):
212
189
  Returns:
213
190
  AcquisitionDataInterface: Wrapped acquisition object
214
191
  """
215
- pass
216
192
 
217
193
  @abstractmethod
218
194
  def write(self, filepath: str) -> None:
@@ -221,16 +197,14 @@ class AcquisitionDataInterface(ABC):
221
197
  Args:
222
198
  filepath: Output file path
223
199
  """
224
- pass
225
200
 
226
201
  @abstractmethod
227
- def fill(self, data: Union[np.ndarray, float]) -> None:
202
+ def fill(self, data: np.ndarray | float) -> None:
228
203
  """Fill acquisition data.
229
204
 
230
205
  Args:
231
206
  data: Numpy array or scalar value
232
207
  """
233
- pass
234
208
 
235
209
  @abstractmethod
236
210
  def clone(self) -> "AcquisitionDataInterface":
@@ -239,7 +213,6 @@ class AcquisitionDataInterface(ABC):
239
213
  Returns:
240
214
  AcquisitionDataInterface: Cloned object
241
215
  """
242
- pass
243
216
 
244
217
  @abstractmethod
245
218
  def get_uniform_copy(self, value: float) -> "AcquisitionDataInterface":
@@ -251,19 +224,17 @@ class AcquisitionDataInterface(ABC):
251
224
  Returns:
252
225
  AcquisitionDataInterface: New uniform acquisition data
253
226
  """
254
- pass
255
227
 
256
228
  @abstractmethod
257
- def dimensions(self) -> Tuple:
229
+ def dimensions(self) -> tuple:
258
230
  """Get acquisition data dimensions.
259
231
 
260
232
  Returns:
261
233
  Tuple: Dimensions (varies by format)
262
234
  """
263
- pass
264
235
 
265
236
  @property
266
- def shape(self) -> Tuple:
237
+ def shape(self) -> tuple:
267
238
  """Get acquisition data shape (alias for dimensions()).
268
239
 
269
240
  Returns:
@@ -278,7 +249,6 @@ class AcquisitionDataInterface(ABC):
278
249
  Returns:
279
250
  np.ndarray: Acquisition data as numpy array
280
251
  """
281
- pass
282
252
 
283
253
  @abstractmethod
284
254
  def sum(self) -> float:
@@ -287,7 +257,6 @@ class AcquisitionDataInterface(ABC):
287
257
  Returns:
288
258
  float: Sum of all counts
289
259
  """
290
- pass
291
260
 
292
261
  @abstractmethod
293
262
  def max(self) -> float:
@@ -296,7 +265,6 @@ class AcquisitionDataInterface(ABC):
296
265
  Returns:
297
266
  float: Maximum bin value
298
267
  """
299
- pass
300
268
 
301
269
  @abstractmethod
302
270
  def get_info(self) -> str:
@@ -305,68 +273,56 @@ class AcquisitionDataInterface(ABC):
305
273
  Returns:
306
274
  str: Metadata string
307
275
  """
308
- pass
309
276
 
310
277
  @abstractmethod
311
- def get_energy_window_bounds(self) -> Tuple[float, float]:
278
+ def get_energy_window_bounds(self) -> tuple[float, float]:
312
279
  """Get the energy window bounds for the acquisition.
313
280
 
314
281
  Returns:
315
282
  Tuple[float, float]: (lower_threshold, upper_threshold) in keV
316
283
  """
317
- pass
318
284
 
319
285
  @property
320
286
  @abstractmethod
321
287
  def native_object(self):
322
288
  """Get the underlying native object (AcquisitionData or ProjData)."""
323
- pass
324
289
 
325
290
  # Arithmetic operations
326
291
  @abstractmethod
327
292
  def __add__(self, other):
328
293
  """Add two acquisition data or acquisition data and scalar."""
329
- pass
330
294
 
331
295
  @abstractmethod
332
296
  def __sub__(self, other):
333
297
  """Subtract two acquisition data or acquisition data and scalar."""
334
- pass
335
298
 
336
299
  @abstractmethod
337
300
  def __mul__(self, other):
338
301
  """Multiply acquisition data by scalar or element-wise."""
339
- pass
340
302
 
341
303
  @abstractmethod
342
304
  def __truediv__(self, other):
343
305
  """Divide acquisition data by scalar or element-wise."""
344
- pass
345
306
 
346
307
  @abstractmethod
347
308
  def __radd__(self, other):
348
309
  """Right-hand addition."""
349
- pass
350
310
 
351
311
  @abstractmethod
352
312
  def __rsub__(self, other):
353
313
  """Right-hand subtraction."""
354
- pass
355
314
 
356
315
  @abstractmethod
357
316
  def __rmul__(self, other):
358
317
  """Right-hand multiplication."""
359
- pass
360
318
 
361
319
  @abstractmethod
362
320
  def __rtruediv__(self, other):
363
321
  """Right-hand division."""
364
- pass
365
322
 
366
323
  @abstractmethod
367
324
  def __neg__(self):
368
325
  """Negate acquisition data."""
369
- pass
370
326
 
371
327
  @abstractmethod
372
328
  def sapyb(
@@ -384,4 +340,3 @@ class AcquisitionDataInterface(ABC):
384
340
  Returns:
385
341
  AcquisitionDataInterface: self after modification (for chaining)
386
342
  """
387
- pass
@@ -5,7 +5,7 @@ Provides wrappers around sirf.STIR.ImageData and sirf.STIR.AcquisitionData
5
5
  that conform to the backend interface.
6
6
  """
7
7
 
8
- from typing import Tuple, Union
8
+ from typing import Union
9
9
 
10
10
  import numpy as np
11
11
 
@@ -60,21 +60,17 @@ class SirfImageData(ImageDataInterface):
60
60
  """Create uniform copy."""
61
61
  return SirfImageData(self._obj.get_uniform_copy(value))
62
62
 
63
- def dimensions(self) -> Tuple[int, int, int]:
63
+ def dimensions(self) -> tuple[int, int, int]:
64
64
  """Get dimensions as (z, y, x)."""
65
65
  return self._obj.dimensions()
66
66
 
67
- def voxel_sizes(self) -> Tuple[float, float, float]:
67
+ def voxel_sizes(self) -> tuple[float, float, float]:
68
68
  """Get voxel sizes as (z, y, x) in mm."""
69
69
  return self._obj.voxel_sizes()
70
70
 
71
71
  def as_array(self) -> np.ndarray:
72
72
  """Convert to numpy array."""
73
- # Try asarray first (newer SIRF), fall back to as_array
74
- if hasattr(self._obj, "asarray"):
75
- return self._obj.asarray()
76
- else:
77
- return self._obj.as_array()
73
+ return self._obj.as_array()
78
74
 
79
75
  def sum(self) -> float:
80
76
  """Sum all values."""
@@ -124,16 +120,18 @@ class SirfImageData(ImageDataInterface):
124
120
  return SirfImageData(self._obj / other)
125
121
 
126
122
  def __radd__(self, other):
127
- return SirfImageData(other + self._obj)
123
+ return self + other
128
124
 
129
125
  def __rsub__(self, other):
130
- return SirfImageData(other - self._obj)
126
+ return -self + other
131
127
 
132
128
  def __rmul__(self, other):
133
- return SirfImageData(other * self._obj)
129
+ return self * other
134
130
 
135
131
  def __rtruediv__(self, other):
136
- return SirfImageData(other / self._obj)
132
+ if isinstance(other, ImageData):
133
+ return SirfImageData(other) / self
134
+ return self.get_uniform_copy(float(other)) / self
137
135
 
138
136
  def __neg__(self):
139
137
  return SirfImageData(-self._obj)
@@ -197,17 +195,13 @@ class SirfAcquisitionData(AcquisitionDataInterface):
197
195
  """Create uniform copy."""
198
196
  return SirfAcquisitionData(self._obj.get_uniform_copy(value))
199
197
 
200
- def dimensions(self) -> Tuple:
198
+ def dimensions(self) -> tuple:
201
199
  """Get dimensions."""
202
200
  return self._obj.dimensions()
203
201
 
204
202
  def as_array(self) -> np.ndarray:
205
203
  """Convert to numpy array."""
206
- # Try asarray first (newer SIRF), fall back to as_array
207
- if hasattr(self._obj, "asarray"):
208
- return self._obj.asarray()
209
- else:
210
- return self._obj.as_array()
204
+ return self._obj.as_array()
211
205
 
212
206
  def sum(self) -> float:
213
207
  """Sum all values."""
@@ -223,7 +217,7 @@ class SirfAcquisitionData(AcquisitionDataInterface):
223
217
  """Get metadata info."""
224
218
  return self._obj.get_info()
225
219
 
226
- def get_energy_window_bounds(self) -> Tuple[float, float]:
220
+ def get_energy_window_bounds(self) -> tuple[float, float]:
227
221
  """Get the energy window bounds for the acquisition.
228
222
 
229
223
  Returns:
@@ -277,16 +271,18 @@ class SirfAcquisitionData(AcquisitionDataInterface):
277
271
  return SirfAcquisitionData(self._obj / other)
278
272
 
279
273
  def __radd__(self, other):
280
- return SirfAcquisitionData(other + self._obj)
274
+ return self + other
281
275
 
282
276
  def __rsub__(self, other):
283
- return SirfAcquisitionData(other - self._obj)
277
+ return -self + other
284
278
 
285
279
  def __rmul__(self, other):
286
- return SirfAcquisitionData(other * self._obj)
280
+ return self * other
287
281
 
288
282
  def __rtruediv__(self, other):
289
- return SirfAcquisitionData(other / self._obj)
283
+ if isinstance(other, AcquisitionData):
284
+ return SirfAcquisitionData(other) / self
285
+ return self.get_uniform_copy(float(other)) / self
290
286
 
291
287
  def __neg__(self):
292
288
  return SirfAcquisitionData(-self._obj)