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,387 @@
1
+ """
2
+ Base interfaces for SIRF and STIR Python backends.
3
+
4
+ This module defines abstract base classes that provide a unified interface
5
+ for working with both SIRF and STIR Python libraries.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Tuple, Union
10
+
11
+ import numpy as np
12
+
13
+
14
+ class ImageDataInterface(ABC):
15
+ """Abstract interface for image data objects.
16
+
17
+ Includes SIRF ``ImageData`` and STIR ``FloatVoxelsOnCartesianGrid`` types.
18
+ """
19
+
20
+ @classmethod
21
+ @abstractmethod
22
+ def read_from_file(cls, filepath: str) -> "ImageDataInterface":
23
+ """Read image data from file.
24
+
25
+ Args:
26
+ filepath: Path to image file (.hv)
27
+
28
+ Returns:
29
+ ImageDataInterface: Wrapped image object
30
+ """
31
+ pass
32
+
33
+ @abstractmethod
34
+ def write(self, filepath: str) -> None:
35
+ """Write image data to file.
36
+
37
+ Args:
38
+ filepath: Output file path
39
+ """
40
+ pass
41
+
42
+ @abstractmethod
43
+ def fill(self, data: Union[np.ndarray, float]) -> None:
44
+ """Fill image with data.
45
+
46
+ Args:
47
+ data: Numpy array or scalar value
48
+ """
49
+ pass
50
+
51
+ @abstractmethod
52
+ def clone(self) -> "ImageDataInterface":
53
+ """Create a deep copy of the image.
54
+
55
+ Returns:
56
+ ImageDataInterface: Cloned image object
57
+ """
58
+ pass
59
+
60
+ @abstractmethod
61
+ def get_uniform_copy(self, value: float) -> "ImageDataInterface":
62
+ """Create a uniform copy with specified value.
63
+
64
+ Args:
65
+ value: Value to fill the image with
66
+
67
+ Returns:
68
+ ImageDataInterface: New uniform image
69
+ """
70
+ pass
71
+
72
+ @abstractmethod
73
+ def dimensions(self) -> Tuple[int, int, int]:
74
+ """Get image dimensions.
75
+
76
+ Returns:
77
+ Tuple[int, int, int]: (z, y, x) dimensions
78
+ """
79
+ pass
80
+
81
+ @abstractmethod
82
+ def voxel_sizes(self) -> Tuple[float, float, float]:
83
+ """Get voxel sizes in mm.
84
+
85
+ Returns:
86
+ Tuple[float, float, float]: (z, y, x) voxel sizes
87
+ """
88
+ pass
89
+
90
+ @abstractmethod
91
+ def as_array(self) -> np.ndarray:
92
+ """Convert to numpy array.
93
+
94
+ Returns:
95
+ np.ndarray: Image data as numpy array
96
+ """
97
+ pass
98
+
99
+ @abstractmethod
100
+ def sum(self) -> float:
101
+ """Sum all values in the image.
102
+
103
+ Returns:
104
+ float: Sum of all voxel values
105
+ """
106
+ pass
107
+
108
+ @abstractmethod
109
+ def max(self) -> float:
110
+ """Return the maximum voxel value.
111
+
112
+ Returns:
113
+ float: Maximum voxel value
114
+ """
115
+ pass
116
+
117
+ @abstractmethod
118
+ def maximum(self, value: float) -> None:
119
+ """Apply element-wise maximum (clipping).
120
+
121
+ Args:
122
+ value: Minimum value to clip to
123
+ """
124
+ pass
125
+
126
+ @property
127
+ @abstractmethod
128
+ def native_object(self):
129
+ """Get the underlying native object.
130
+
131
+ Returns either SIRF ``ImageData`` or STIR
132
+ ``FloatVoxelsOnCartesianGrid``.
133
+ """
134
+ pass
135
+
136
+ # Arithmetic operations
137
+ @abstractmethod
138
+ def __add__(self, other):
139
+ """Add two images or image and scalar."""
140
+ pass
141
+
142
+ @abstractmethod
143
+ def __sub__(self, other):
144
+ """Subtract two images or image and scalar."""
145
+ pass
146
+
147
+ @abstractmethod
148
+ def __mul__(self, other):
149
+ """Multiply image by scalar or element-wise."""
150
+ pass
151
+
152
+ @abstractmethod
153
+ def __truediv__(self, other):
154
+ """Divide image by scalar or element-wise."""
155
+ pass
156
+
157
+ @abstractmethod
158
+ def __radd__(self, other):
159
+ """Right-hand addition."""
160
+ pass
161
+
162
+ @abstractmethod
163
+ def __rsub__(self, other):
164
+ """Right-hand subtraction."""
165
+ pass
166
+
167
+ @abstractmethod
168
+ def __rmul__(self, other):
169
+ """Right-hand multiplication."""
170
+ pass
171
+
172
+ @abstractmethod
173
+ def __rtruediv__(self, other):
174
+ """Right-hand division."""
175
+ pass
176
+
177
+ @abstractmethod
178
+ def __neg__(self):
179
+ """Negate image."""
180
+ pass
181
+
182
+ @abstractmethod
183
+ def sapyb(
184
+ self, a: float, y: "ImageDataInterface", b: float
185
+ ) -> "ImageDataInterface":
186
+ """Compute self * a + y * b (element-wise), modifying self in place.
187
+
188
+ This method modifies self in place: self = self * a + y * b
189
+
190
+ Args:
191
+ a: Scalar multiplier for self
192
+ y: Other image
193
+ b: Scalar multiplier for y
194
+
195
+ Returns:
196
+ ImageDataInterface: self after modification (for chaining)
197
+ """
198
+ pass
199
+
200
+
201
+ class AcquisitionDataInterface(ABC):
202
+ """Abstract interface for acquisition data (AcquisitionData or ProjData)."""
203
+
204
+ @classmethod
205
+ @abstractmethod
206
+ def read_from_file(cls, filepath: str) -> "AcquisitionDataInterface":
207
+ """Read acquisition data from file.
208
+
209
+ Args:
210
+ filepath: Path to header file (.hs)
211
+
212
+ Returns:
213
+ AcquisitionDataInterface: Wrapped acquisition object
214
+ """
215
+ pass
216
+
217
+ @abstractmethod
218
+ def write(self, filepath: str) -> None:
219
+ """Write acquisition data to file.
220
+
221
+ Args:
222
+ filepath: Output file path
223
+ """
224
+ pass
225
+
226
+ @abstractmethod
227
+ def fill(self, data: Union[np.ndarray, float]) -> None:
228
+ """Fill acquisition data.
229
+
230
+ Args:
231
+ data: Numpy array or scalar value
232
+ """
233
+ pass
234
+
235
+ @abstractmethod
236
+ def clone(self) -> "AcquisitionDataInterface":
237
+ """Create a deep copy.
238
+
239
+ Returns:
240
+ AcquisitionDataInterface: Cloned object
241
+ """
242
+ pass
243
+
244
+ @abstractmethod
245
+ def get_uniform_copy(self, value: float) -> "AcquisitionDataInterface":
246
+ """Create a uniform copy with specified value.
247
+
248
+ Args:
249
+ value: Value to fill the acquisition data with
250
+
251
+ Returns:
252
+ AcquisitionDataInterface: New uniform acquisition data
253
+ """
254
+ pass
255
+
256
+ @abstractmethod
257
+ def dimensions(self) -> Tuple:
258
+ """Get acquisition data dimensions.
259
+
260
+ Returns:
261
+ Tuple: Dimensions (varies by format)
262
+ """
263
+ pass
264
+
265
+ @property
266
+ def shape(self) -> Tuple:
267
+ """Get acquisition data shape (alias for dimensions()).
268
+
269
+ Returns:
270
+ Tuple: Dimensions (varies by format)
271
+ """
272
+ return self.dimensions()
273
+
274
+ @abstractmethod
275
+ def as_array(self) -> np.ndarray:
276
+ """Convert to numpy array.
277
+
278
+ Returns:
279
+ np.ndarray: Acquisition data as numpy array
280
+ """
281
+ pass
282
+
283
+ @abstractmethod
284
+ def sum(self) -> float:
285
+ """Sum all values.
286
+
287
+ Returns:
288
+ float: Sum of all counts
289
+ """
290
+ pass
291
+
292
+ @abstractmethod
293
+ def max(self) -> float:
294
+ """Return the maximum bin value.
295
+
296
+ Returns:
297
+ float: Maximum bin value
298
+ """
299
+ pass
300
+
301
+ @abstractmethod
302
+ def get_info(self) -> str:
303
+ """Get metadata information.
304
+
305
+ Returns:
306
+ str: Metadata string
307
+ """
308
+ pass
309
+
310
+ @abstractmethod
311
+ def get_energy_window_bounds(self) -> Tuple[float, float]:
312
+ """Get the energy window bounds for the acquisition.
313
+
314
+ Returns:
315
+ Tuple[float, float]: (lower_threshold, upper_threshold) in keV
316
+ """
317
+ pass
318
+
319
+ @property
320
+ @abstractmethod
321
+ def native_object(self):
322
+ """Get the underlying native object (AcquisitionData or ProjData)."""
323
+ pass
324
+
325
+ # Arithmetic operations
326
+ @abstractmethod
327
+ def __add__(self, other):
328
+ """Add two acquisition data or acquisition data and scalar."""
329
+ pass
330
+
331
+ @abstractmethod
332
+ def __sub__(self, other):
333
+ """Subtract two acquisition data or acquisition data and scalar."""
334
+ pass
335
+
336
+ @abstractmethod
337
+ def __mul__(self, other):
338
+ """Multiply acquisition data by scalar or element-wise."""
339
+ pass
340
+
341
+ @abstractmethod
342
+ def __truediv__(self, other):
343
+ """Divide acquisition data by scalar or element-wise."""
344
+ pass
345
+
346
+ @abstractmethod
347
+ def __radd__(self, other):
348
+ """Right-hand addition."""
349
+ pass
350
+
351
+ @abstractmethod
352
+ def __rsub__(self, other):
353
+ """Right-hand subtraction."""
354
+ pass
355
+
356
+ @abstractmethod
357
+ def __rmul__(self, other):
358
+ """Right-hand multiplication."""
359
+ pass
360
+
361
+ @abstractmethod
362
+ def __rtruediv__(self, other):
363
+ """Right-hand division."""
364
+ pass
365
+
366
+ @abstractmethod
367
+ def __neg__(self):
368
+ """Negate acquisition data."""
369
+ pass
370
+
371
+ @abstractmethod
372
+ def sapyb(
373
+ self, a: float, y: "AcquisitionDataInterface", b: float
374
+ ) -> "AcquisitionDataInterface":
375
+ """Compute self * a + y * b (element-wise), modifying self in place.
376
+
377
+ This method modifies self in place: self = self * a + y * b
378
+
379
+ Args:
380
+ a: Scalar multiplier for self
381
+ y: Other acquisition data
382
+ b: Scalar multiplier for y
383
+
384
+ Returns:
385
+ AcquisitionDataInterface: self after modification (for chaining)
386
+ """
387
+ pass
@@ -0,0 +1,309 @@
1
+ """
2
+ SIRF backend implementation.
3
+
4
+ Provides wrappers around sirf.STIR.ImageData and sirf.STIR.AcquisitionData
5
+ that conform to the backend interface.
6
+ """
7
+
8
+ from typing import Tuple, Union
9
+
10
+ import numpy as np
11
+
12
+ from simind_python_connector.utils.import_helpers import get_sirf_types
13
+
14
+ from .base import AcquisitionDataInterface, ImageDataInterface
15
+
16
+
17
+ ImageData, AcquisitionData, SIRF_AVAILABLE = get_sirf_types()
18
+
19
+
20
+ class SirfImageData(ImageDataInterface):
21
+ """Wrapper for sirf.STIR.ImageData."""
22
+
23
+ def __init__(self, sirf_obj: ImageData):
24
+ """Initialize wrapper.
25
+
26
+ Args:
27
+ sirf_obj: SIRF ImageData object
28
+ """
29
+ if not SIRF_AVAILABLE:
30
+ raise ImportError("SIRF is not available")
31
+ self._obj = sirf_obj
32
+
33
+ @classmethod
34
+ def read_from_file(cls, filepath: str) -> "SirfImageData":
35
+ """Read image from file."""
36
+ return cls(ImageData(filepath))
37
+
38
+ @classmethod
39
+ def create_empty(cls) -> "SirfImageData":
40
+ """Create empty image."""
41
+ return cls(ImageData())
42
+
43
+ def write(self, filepath: str) -> None:
44
+ """Write to file."""
45
+ self._obj.write(filepath)
46
+
47
+ def fill(self, data: Union[np.ndarray, float, "SirfImageData"]) -> None:
48
+ """Fill with data."""
49
+ # Unwrap if data is a wrapped object
50
+ if isinstance(data, SirfImageData):
51
+ self._obj.fill(data._obj)
52
+ else:
53
+ self._obj.fill(data)
54
+
55
+ def clone(self) -> "SirfImageData":
56
+ """Clone the image."""
57
+ return SirfImageData(self._obj.clone())
58
+
59
+ def get_uniform_copy(self, value: float) -> "SirfImageData":
60
+ """Create uniform copy."""
61
+ return SirfImageData(self._obj.get_uniform_copy(value))
62
+
63
+ def dimensions(self) -> Tuple[int, int, int]:
64
+ """Get dimensions as (z, y, x)."""
65
+ return self._obj.dimensions()
66
+
67
+ def voxel_sizes(self) -> Tuple[float, float, float]:
68
+ """Get voxel sizes as (z, y, x) in mm."""
69
+ return self._obj.voxel_sizes()
70
+
71
+ def as_array(self) -> np.ndarray:
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()
78
+
79
+ def sum(self) -> float:
80
+ """Sum all values."""
81
+ return float(self._obj.sum())
82
+
83
+ def max(self) -> float:
84
+ """Return the maximum voxel value."""
85
+ if hasattr(self._obj, "max"):
86
+ return float(self._obj.max())
87
+ return float(np.max(self.as_array()))
88
+
89
+ def maximum(self, value: float) -> None:
90
+ """Apply element-wise maximum."""
91
+ self._obj.maximum(value)
92
+
93
+ @property
94
+ def native_object(self):
95
+ """Get underlying SIRF object."""
96
+ return self._obj
97
+
98
+ def __repr__(self) -> str:
99
+ return f"SirfImageData(dims={self.dimensions()})"
100
+
101
+ # Arithmetic operations - delegate to native SIRF object
102
+ def __add__(self, other):
103
+ if isinstance(other, SirfImageData):
104
+ return SirfImageData(self._obj + other._obj)
105
+ else:
106
+ return SirfImageData(self._obj + other)
107
+
108
+ def __sub__(self, other):
109
+ if isinstance(other, SirfImageData):
110
+ return SirfImageData(self._obj - other._obj)
111
+ else:
112
+ return SirfImageData(self._obj - other)
113
+
114
+ def __mul__(self, other):
115
+ if isinstance(other, SirfImageData):
116
+ return SirfImageData(self._obj * other._obj)
117
+ else:
118
+ return SirfImageData(self._obj * other)
119
+
120
+ def __truediv__(self, other):
121
+ if isinstance(other, SirfImageData):
122
+ return SirfImageData(self._obj / other._obj)
123
+ else:
124
+ return SirfImageData(self._obj / other)
125
+
126
+ def __radd__(self, other):
127
+ return SirfImageData(other + self._obj)
128
+
129
+ def __rsub__(self, other):
130
+ return SirfImageData(other - self._obj)
131
+
132
+ def __rmul__(self, other):
133
+ return SirfImageData(other * self._obj)
134
+
135
+ def __rtruediv__(self, other):
136
+ return SirfImageData(other / self._obj)
137
+
138
+ def __neg__(self):
139
+ return SirfImageData(-self._obj)
140
+
141
+ def sapyb(self, a: float, y: "SirfImageData", b: float) -> "SirfImageData":
142
+ """Compute self * a + y * b (element-wise), modifying self in place.
143
+
144
+ Args:
145
+ a: Scalar multiplier for self
146
+ y: Other image
147
+ b: Scalar multiplier for y
148
+
149
+ Returns:
150
+ SirfImageData: self after modification (for chaining)
151
+ """
152
+ y_obj = y._obj if isinstance(y, SirfImageData) else y
153
+ self._obj.sapyb(a, y_obj, b)
154
+ return self
155
+
156
+
157
+ class SirfAcquisitionData(AcquisitionDataInterface):
158
+ """Wrapper for sirf.STIR.AcquisitionData."""
159
+
160
+ def __init__(self, sirf_obj: AcquisitionData):
161
+ """Initialize wrapper.
162
+
163
+ Args:
164
+ sirf_obj: SIRF AcquisitionData object
165
+ """
166
+ if not SIRF_AVAILABLE:
167
+ raise ImportError("SIRF is not available")
168
+ self._obj = sirf_obj
169
+
170
+ @classmethod
171
+ def read_from_file(cls, filepath: str) -> "SirfAcquisitionData":
172
+ """Read acquisition data from file."""
173
+ return cls(AcquisitionData(filepath))
174
+
175
+ @classmethod
176
+ def create_empty(cls) -> "SirfAcquisitionData":
177
+ """Create empty acquisition data."""
178
+ return cls(AcquisitionData())
179
+
180
+ def write(self, filepath: str) -> None:
181
+ """Write to file."""
182
+ self._obj.write(filepath)
183
+
184
+ def fill(self, data: Union[np.ndarray, float, "SirfAcquisitionData"]) -> None:
185
+ """Fill with data."""
186
+ # Unwrap if data is a wrapped object
187
+ if isinstance(data, SirfAcquisitionData):
188
+ self._obj.fill(data._obj)
189
+ else:
190
+ self._obj.fill(data)
191
+
192
+ def clone(self) -> "SirfAcquisitionData":
193
+ """Clone the acquisition data."""
194
+ return SirfAcquisitionData(self._obj.clone())
195
+
196
+ def get_uniform_copy(self, value: float) -> "SirfAcquisitionData":
197
+ """Create uniform copy."""
198
+ return SirfAcquisitionData(self._obj.get_uniform_copy(value))
199
+
200
+ def dimensions(self) -> Tuple:
201
+ """Get dimensions."""
202
+ return self._obj.dimensions()
203
+
204
+ def as_array(self) -> np.ndarray:
205
+ """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()
211
+
212
+ def sum(self) -> float:
213
+ """Sum all values."""
214
+ return float(self._obj.sum())
215
+
216
+ def max(self) -> float:
217
+ """Return the maximum bin value."""
218
+ if hasattr(self._obj, "max"):
219
+ return float(self._obj.max())
220
+ return float(np.max(self.as_array()))
221
+
222
+ def get_info(self) -> str:
223
+ """Get metadata info."""
224
+ return self._obj.get_info()
225
+
226
+ def get_energy_window_bounds(self) -> Tuple[float, float]:
227
+ """Get the energy window bounds for the acquisition.
228
+
229
+ Returns:
230
+ Tuple[float, float]: (lower_threshold, upper_threshold) in keV
231
+
232
+ Raises:
233
+ NotImplementedError: SIRF backend does not currently support
234
+ accessing energy window bounds.
235
+ """
236
+ raise NotImplementedError(
237
+ "SIRF backend does not currently support accessing energy window bounds. "
238
+ "Use STIR backend for this functionality."
239
+ )
240
+
241
+ def create_uniform_image(self, value: float = 0.0) -> SirfImageData:
242
+ """Create compatible uniform image."""
243
+ img = self._obj.create_uniform_image(value)
244
+ return SirfImageData(img)
245
+
246
+ @property
247
+ def native_object(self):
248
+ """Get underlying SIRF object."""
249
+ return self._obj
250
+
251
+ def __repr__(self) -> str:
252
+ return f"SirfAcquisitionData(dims={self.dimensions()})"
253
+
254
+ # Arithmetic operations - delegate to native SIRF object
255
+ def __add__(self, other):
256
+ if isinstance(other, SirfAcquisitionData):
257
+ return SirfAcquisitionData(self._obj + other._obj)
258
+ else:
259
+ return SirfAcquisitionData(self._obj + other)
260
+
261
+ def __sub__(self, other):
262
+ if isinstance(other, SirfAcquisitionData):
263
+ return SirfAcquisitionData(self._obj - other._obj)
264
+ else:
265
+ return SirfAcquisitionData(self._obj - other)
266
+
267
+ def __mul__(self, other):
268
+ if isinstance(other, SirfAcquisitionData):
269
+ return SirfAcquisitionData(self._obj * other._obj)
270
+ else:
271
+ return SirfAcquisitionData(self._obj * other)
272
+
273
+ def __truediv__(self, other):
274
+ if isinstance(other, SirfAcquisitionData):
275
+ return SirfAcquisitionData(self._obj / other._obj)
276
+ else:
277
+ return SirfAcquisitionData(self._obj / other)
278
+
279
+ def __radd__(self, other):
280
+ return SirfAcquisitionData(other + self._obj)
281
+
282
+ def __rsub__(self, other):
283
+ return SirfAcquisitionData(other - self._obj)
284
+
285
+ def __rmul__(self, other):
286
+ return SirfAcquisitionData(other * self._obj)
287
+
288
+ def __rtruediv__(self, other):
289
+ return SirfAcquisitionData(other / self._obj)
290
+
291
+ def __neg__(self):
292
+ return SirfAcquisitionData(-self._obj)
293
+
294
+ def sapyb(
295
+ self, a: float, y: "SirfAcquisitionData", b: float
296
+ ) -> "SirfAcquisitionData":
297
+ """Compute self * a + y * b (element-wise), modifying self in place.
298
+
299
+ Args:
300
+ a: Scalar multiplier for self
301
+ y: Other acquisition data
302
+ b: Scalar multiplier for y
303
+
304
+ Returns:
305
+ SirfAcquisitionData: self after modification (for chaining)
306
+ """
307
+ y_obj = y._obj if isinstance(y, SirfAcquisitionData) else y
308
+ self._obj.sapyb(a, y_obj, b)
309
+ return self