Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d95c7bb
updated files for release 3.9.0 [ci skip]
evgueni-ovtchinnikov Oct 28, 2025
54505e7
Merge branch 'master' of https://github.com/SyneRBI/SIRF
evgueni-ovtchinnikov Oct 30, 2025
dedf83c
Merged branch 'master' of https://github.com/SyneRBI/SIRF
evgueni-ovtchinnikov Nov 8, 2025
a164114
added numpy array to possible SIRF image data algebra operands
evgueni-ovtchinnikov Nov 17, 2025
c53d80b
added to User Guide a subsection on SIRF/numpy data algebra peculiari…
evgueni-ovtchinnikov Nov 24, 2025
9f2ecf8
updated CHANGES.md [ci skip]
evgueni-ovtchinnikov Nov 24, 2025
80b3c07
added tests for #1358
evgueni-ovtchinnikov Nov 25, 2025
57d9e15
attended to Codacy issues
evgueni-ovtchinnikov Nov 25, 2025
0477cc0
simpliied mixed data algebra
evgueni-ovtchinnikov Nov 27, 2025
17867e4
Merge branch 'master' of https://github.com/SyneRBI/SIRF
evgueni-ovtchinnikov Jan 15, 2026
f249547
resolved conflicts
evgueni-ovtchinnikov Jan 15, 2026
5a1e82f
Merge branch 'master' of https://github.com/SyneRBI/SIRF
evgueni-ovtchinnikov Jan 27, 2026
6bfdf52
fixed mixing SIRF and numpy data algebra issue #1357
evgueni-ovtchinnikov Feb 3, 2026
9b67e9e
added algebra tests for ax/x, 2/x etc.
evgueni-ovtchinnikov Feb 5, 2026
a00488d
Merge branch 'master' of https://github.com/SyneRBI/SIRF
evgueni-ovtchinnikov Feb 10, 2026
c3aa576
Merge branch 'master' into mda-priority-fix
evgueni-ovtchinnikov Feb 10, 2026
856c7ee
added the result type checks for mixed data algebra
evgueni-ovtchinnikov Feb 11, 2026
0d8f93f
[ci skip] updated User Guide description of data algebra and CHANGES.md
evgueni-ovtchinnikov Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* SIRF/STIR
- The implementation of the creation of `sirf.STIR.ImageData` from `sirf.STIR.AcquisitionData` has been revised to ensure compatibility of `ImageData` dimensions and voxel sizes with `AcquisitionData`.
* Python interface
- Restored functionality for algebraic operations mixing `STIR.ImageData` and numpy arrays. (Note that sirf objects need to be on the "left" of the operation.)
- Restored functionality for algebraic operations mixing SIRF data containers and numpy arrays and corrected the description of the result type in User Guide.

## v3.9.0

Expand Down
2 changes: 1 addition & 1 deletion doc/UserGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Some classes are _derived_ from other classes, which means that they have (_inhe

### SIRF data algebra <a name="SIRF_data_algebra"></a>

SIRF Python interface supports algebraic operations (`+`, `-`, `*` and `/`): e.g. elements of the data array stored in the object `a*b` are the products of the respective elements in `a` and `b`. Either or both `a` and `b` can be SIRF data objects of the same kind (either both `ImageData` or both `AcquisitionData`) or `numpy` arrays or scalars. One should be aware though that if `a` is a SIRF object then, just as one would expect, the product `a*b` will be a SIRF object of the same kind, but if `a` is a `numpy` object (array or scalar) then Python will try to convert `b` to a `numpy` object before computing `a*b`, and only if this fails it will compute `b*a` instead. To avoid confusion, the users are advised to check the type of `a*b` or, better still, always place a SIRF object on the left side of the product.
SIRF Python interface supports algebraic operations (`+`, `-`, `*` and `/`): e.g. elements of the data array stored in the object `a*b` are the products of the respective elements in `a` and `b`. Either or both `a` and `b` can be SIRF data objects or `numpy` arrays or scalars. If both are SIRF data objects, they must be of the same type (e.g. either both `ImageData` or both `AcquisitionData`), and the result will be a SIRF data object of the same type, the same kind of result being produced if one is a SIRF data object and the other is either `numpy` array or scalar. If none is a SIRF data object, then the type of result will be determined by Python.

### Error handling <a name="error_handling"></a>

Expand Down
11 changes: 11 additions & 0 deletions src/common/SIRF.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class ArrayContainer(ABC):
accessible via the address of the first data item.
"""
warnings.simplefilter('once', DeprecationWarning)
__array_priority__ = 10

def asarray(self, xp=numpy, copy=None, **kwargs):
"""Returns view (or fallback copy) of self"""
Expand Down Expand Up @@ -131,6 +132,10 @@ def __add__(self, other):
'''
return self.add(other)

def __radd__(self, other):
return self + other
#return other.add(self)

def __sub__(self, other):
'''
Overloads - for data containers.
Expand All @@ -141,6 +146,9 @@ def __sub__(self, other):
'''
return self.subtract(other)

def __rsub__(self, other):
return -other + self

def __mul__(self, other):
'''
Overloads * for data containers multiplication by a scalar or another
Expand Down Expand Up @@ -171,6 +179,9 @@ def __truediv__(self, other):
'''
return self.divide(other)

def __rtruediv__(self, other):
return other*self.power(-1)

def __iadd__(self, other):
self.add(other, out=self)
return self
Expand Down
40 changes: 40 additions & 0 deletions src/common/Utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -790,24 +790,50 @@ def data_container_algebra_tests(test, x, eps=1e-4):
t = numpy.linalg.norm(az)
test.check_if_zero_within_tolerance(s, eps * t)

z = ay*x
az = z.as_array()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we sure we really want to use as_array() for these tests? Obviously, if replacing it with asarray() we'd have to be very careful for any operatoins that modify the object.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better safe than sorry IMHO

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, what happens if the sizes are wrong?

assert_validities would raise AssertionError

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better safe than sorry IMHO

not necessarily for tests. Probably a good idea to do at least some of the tests with asarray() as well, as the future, it's what is must likely to hit us.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, replacing every as_array use with that of asarray is perfectly safe as asarray defaults to copy - to get a view, you need asarray(copy=False) (we probably should keep as_array method in our data container classes for backward compatibility). However, this needs a separate PR I believe, this one just fixes #1357.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asarray defaults to copy

no it doesn't. it defaults to view (if the container supports it).

Whatever, I still feel we need to test with the most used function, even if you know in the end it gives the same results (but at some point, it might not)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleting as_array will break compatibility with CIL.

s = numpy.linalg.norm(az - ax * ay)
t = numpy.linalg.norm(az)
test.check_if_zero_within_tolerance(s, eps * t)

y = x + 1
ay = y.as_array()
s = numpy.linalg.norm(ay - (ax + 1))
t = numpy.linalg.norm(ay)
test.check_if_zero_within_tolerance(s, eps * t)

y = 1 + x
test.check_if_equal(type(y), type(x))
ay = y.as_array()
s = numpy.linalg.norm(ay - (ax + 1))
t = numpy.linalg.norm(ay)
test.check_if_zero_within_tolerance(s, eps * t)

y = x + ax
ay = y.as_array()
s = numpy.linalg.norm(ay - (ax + ax))
t = numpy.linalg.norm(ay)
test.check_if_zero_within_tolerance(s, eps * t)

y = ax + x
test.check_if_equal(type(y), type(x))
ay = y.as_array()
s = numpy.linalg.norm(ay - (ax + ax))
t = numpy.linalg.norm(ay)
test.check_if_zero_within_tolerance(s, eps * t)

t = numpy.linalg.norm(ay)
y = x - ax
ay = y.as_array()
s = numpy.linalg.norm(ay)
test.check_if_zero_within_tolerance(s, eps * t)

y = ax - x
test.check_if_equal(type(y), type(x))
ay = y.as_array()
s = numpy.linalg.norm(ay)
test.check_if_zero_within_tolerance(s, eps * t)

y *= 0
x.add(1, out=y)
ay = y.as_array()
Expand All @@ -827,12 +853,26 @@ def data_container_algebra_tests(test, x, eps=1e-4):
t = numpy.linalg.norm(az)
test.check_if_zero_within_tolerance(s, eps * t)

z = ay/x
test.check_if_equal(type(z), type(x))
az = z.as_array()
s = numpy.linalg.norm(az - ay/ax)
t = numpy.linalg.norm(az)
test.check_if_zero_within_tolerance(s, eps * t)

z = x/2
az = z.as_array()
s = numpy.linalg.norm(az - ax/2)
t = numpy.linalg.norm(az)
test.check_if_zero_within_tolerance(s, eps * t)

z = 2/x
test.check_if_equal(type(z), type(x))
az = z.as_array()
s = numpy.linalg.norm(az - 2/ax)
t = numpy.linalg.norm(az)
test.check_if_zero_within_tolerance(s, eps * t)

z *= 0
x.divide(y, out=z)
az = z.as_array()
Expand Down