diff --git a/CHANGES.md b/CHANGES.md index 9e4679a05..9fc60f454 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/doc/UserGuide.md b/doc/UserGuide.md index 4272a1adf..9fe4c6302 100644 --- a/doc/UserGuide.md +++ b/doc/UserGuide.md @@ -110,7 +110,7 @@ Some classes are _derived_ from other classes, which means that they have (_inhe ### SIRF data algebra -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 diff --git a/src/common/SIRF.py b/src/common/SIRF.py index c126e9854..5a2940d80 100644 --- a/src/common/SIRF.py +++ b/src/common/SIRF.py @@ -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""" @@ -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. @@ -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 @@ -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 diff --git a/src/common/Utilities.py b/src/common/Utilities.py index 7b44ff9a5..f20dad62c 100644 --- a/src/common/Utilities.py +++ b/src/common/Utilities.py @@ -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() + 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() @@ -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()