From 0d6b067e15695f9d2b0c19340c728a112c79c029 Mon Sep 17 00:00:00 2001 From: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com> Date: Wed, 25 Sep 2024 12:44:51 +0100 Subject: [PATCH 1/2] Update ImageData and AcquisitionData equality checks (#1919) --------- Signed-off-by: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com> --- CHANGELOG.md | 5 +- .../Python/cil/framework/acquisition_data.py | 25 +++++ .../cil/framework/acquisition_geometry.py | 3 +- Wrappers/Python/cil/framework/image_data.py | 25 +++++ .../Python/test/test_AcquisitionGeometry.py | 2 + Wrappers/Python/test/test_DataContainer.py | 96 ++++++++++++++++--- 6 files changed, 140 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b7ee6ba09..0eb818e67e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,14 +12,17 @@ - Use ravel instead of flat in KullbackLeibler numba backend (#1874) - Upgrade Python wrapper (#1873, #1875) - Updated the documentation for the algorithm base class (#1809) - - Add checks on out argument passed to processors to ensure corrrect dtype and size (#1805) + - Add checks on out argument passed to processors to ensure correct dtype and size (#1805) - Internal refactor: Replaced string-based label checks with enum-based checks for improved type safety and consistency (#1692) - Internal refactor: Separate framework into multiple files (#1692) - Allow the SIRT algorithm to take `initial=None` (#1906) + - Add checks on equality method of `AcquisitionData` and `ImageData` for equality of data type and geometry (#1919) + - Add check on equality method of `AcquisitionGeometry` for equality of dimension labels (#1919) - Testing: - New unit tests for operators and functions to check for in place errors and the behaviour of `out` (#1805) - Updates in SPDHG vs PDHG unit test to reduce test time and adjustments to parameters (#1898) - Drop Jenkins in favour of GHA for conda builds (#1914) + - New unit tests for `DataContainer`, `AcquisitionData` and `ImageData` to check equality method (`__eq__`) behaves as expected (#1919) - Bug fixes: - `ImageData` removes dimensions of size 1 from the input array. This fixes an issue where single slice reconstructions from 3D data would fail due to shape mismatches (#1885) - Make Binner accept accelerated=False (#1887) diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index ece19f1946..207f5b947e 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -73,6 +73,31 @@ def __init__(self, super(AcquisitionData, self).__init__(array, deep_copy, geometry=geometry,**kwargs) + def __eq__(self, other): + ''' + Check if two AcquisitionData objects are equal. This is done by checking if the geometry, data and dtype are equal. + Also, if the other object is a numpy.ndarray, it will check if the data and dtype are equal. + + Parameters + ---------- + other: AcquisitionData or numpy.ndarray + The object to compare with. + + Returns + ------- + bool + True if the two objects are equal, False otherwise. + ''' + + if isinstance(other, AcquisitionData): + if numpy.array_equal(self.as_array(), other.as_array()) \ + and self.geometry == other.geometry \ + and self.dtype == other.dtype: + return True + elif numpy.array_equal(self.as_array(), other) and self.dtype==other.dtype: + return True + else: + return False def get_slice(self,channel=None, angle=None, vertical=None, horizontal=None, force=False): '''Returns a new dataset of a single slice in the requested direction.''' diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index cc2fdd8f25..662e6bc0b9 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -2095,7 +2095,8 @@ def __eq__(self, other): if isinstance(other, self.__class__) \ and self.config == other.config \ - and self.dtype == other.dtype: + and self.dtype == other.dtype \ + and self.dimension_labels == other.dimension_labels: return True return False diff --git a/Wrappers/Python/cil/framework/image_data.py b/Wrappers/Python/cil/framework/image_data.py index daca1f1ee5..65cac02214 100644 --- a/Wrappers/Python/cil/framework/image_data.py +++ b/Wrappers/Python/cil/framework/image_data.py @@ -76,7 +76,32 @@ def __init__(self, super(ImageData, self).__init__(array, deep_copy, geometry=geometry, **kwargs) + def __eq__(self, other): + ''' + Check if two ImageData objects are equal. This is done by checking if the geometry, data and dtype are equal. + Also, if the other object is a numpy.ndarray, it will check if the data and dtype are equal. + + Parameters + ---------- + other: ImageData or numpy.ndarray + The object to compare with. + + Returns + ------- + bool + True if the two objects are equal, False otherwise. + ''' + if isinstance(other, ImageData): + if numpy.array_equal(self.as_array(), other.as_array()) \ + and self.geometry == other.geometry \ + and self.dtype == other.dtype: + return True + elif numpy.array_equal(self.as_array(), other) and self.dtype==other.dtype: + return True + else: + return False + def get_slice(self,channel=None, vertical=None, horizontal_x=None, horizontal_y=None, force=False): ''' Returns a new ImageData of a single slice of in the requested direction. diff --git a/Wrappers/Python/test/test_AcquisitionGeometry.py b/Wrappers/Python/test/test_AcquisitionGeometry.py index 14ad35bae3..f19ff86b63 100644 --- a/Wrappers/Python/test/test_AcquisitionGeometry.py +++ b/Wrappers/Python/test/test_AcquisitionGeometry.py @@ -495,10 +495,12 @@ def test_copy(self): def test_get_centre_slice(self): AG = AcquisitionGeometry.create_Parallel3D(detector_direction_y=[0,1,1]) AG.set_panel([1000,2000],[1,1]) + AG.set_angles([0,1,2,3,5]) AG_cs = AG.get_centre_slice() AG2 = AcquisitionGeometry.create_Parallel2D() AG2.set_panel([1000,1],[1,math.sqrt(0.5)]) + AG2.set_angles([0,1,2,3,5]) self.assertEqual(AG2, AG_cs) diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py index 67c8922b88..b1003dddd2 100644 --- a/Wrappers/Python/test/test_DataContainer.py +++ b/Wrappers/Python/test/test_DataContainer.py @@ -38,20 +38,6 @@ def aid(x): class TestDataContainer(CCPiTestClass): - def create_simple_ImageData(self): - N = 64 - ig = ImageGeometry(voxel_num_x=N, voxel_num_y=N) - Phantom = ImageData(geometry=ig) - - x = Phantom.as_array() - - x[int(round(N/4)):int(round(3*N/4)), - int(round(N/4)):int(round(3*N/4))] = 0.5 - x[int(round(N/8)):int(round(7*N/8)), - int(round(3*N/8)):int(round(5*N/8))] = 1 - - return (ig, Phantom) - def create_DataContainer(self, X,Y,Z, value=1): a = value * np.ones((X, Y, Z), dtype='float32') #print("a refcount " , sys.getrefcount(a)) @@ -88,6 +74,88 @@ def test_ndim(self): self.assertEqual(x_np.ndim, x_cil.ndim) self.assertEqual(3, x_cil.ndim) + def test_DataContainer_equal(self): + array = np.linspace(-1, 1, 32, dtype=np.float32).reshape(4, 8) + data = DataContainer(array) + data1 = data.copy() + + # Check two identical DataContainers are equal + self.assertTrue((data == data1).all()) + # Check it works for comparing DataContainer with numpy array + self.assertTrue((data == data1.as_array()).all()) + + # # Check two different DataContainers are not equal + data1 += 1 + self.assertFalse((data == data1).all()) + + def test_AcquisitionData_equal(self): + array = np.linspace(-1, 1, 32, dtype=np.float32).reshape(4, 8) + geom = AcquisitionGeometry.create_Parallel3D().set_panel((8, 4)).set_channels(1).set_angles([1]) + data = AcquisitionData(array, geometry=geom) + + data1 = data.copy() + + # Check two identical AcquisitionData are equal + self.assertTrue(data == data1) + # Check it works for comparing AcquisitionData with numpy array + self.assertTrue(data == data1.as_array()) + + # # Check two different AcquisitionData are not equal + data1 += 1 + self.assertFalse(data == data1) + + # Check the equality of two AcquisitionData with different shapes + data_different_shape = data.copy() + data_different_shape.array = data_different_shape.array.reshape(8, 4) + + self.assertFalse(data == data_different_shape) + + # Check the equality of two AcquisitionData with different dtypes + data_different_dtype = data.copy() + data_different_dtype.array = data_different_dtype.array.astype(np.float64) + self.assertFalse(data == data_different_dtype) + + + # Check the equality of two AcquisitionData with different labels + data_different_labels = data.copy() + print(data_different_labels.geometry.dimension_labels) + data_different_labels.geometry.set_labels([AcquisitionDimension("ANGLE"), AcquisitionDimension("CHANNEL") ]) + self.assertFalse(data == data_different_labels) + + def test_ImageData_equal(self): + array = np.linspace(-1, 1, 32, dtype=np.float32).reshape(4, 8) + geom = ImageGeometry(voxel_num_x=8, voxel_num_y=4) + data = ImageData(array, geometry=geom) + + data1 = data.copy() + + # Check two identical ImageData are equal + self.assertTrue(data == data1) + # Check it works for comparing ImageData with numpy array + self.assertTrue(data == data1.as_array()) + + # # Check two different ImageData are not equal + data1 += 1 + self.assertFalse(data == data1) + + # Check the equality of two ImageData with different shapes + data_different_shape = data.copy() + data_different_shape.array = data_different_shape.array.reshape(8, 4) + + self.assertFalse(data == data_different_shape) + + # Check the equality of two ImageData with different dtypes + data_different_dtype = data.copy() + data_different_dtype.array = data_different_dtype.array.astype(np.float64) + self.assertFalse(data == data_different_dtype) + + + # Check the equality of two ImageData with different labels + data_different_labels = data.copy() + data_different_labels.geometry.set_labels([ImageDimension("VERTICAL"), ImageDimension("HORIZONTAL_X")]) + self.assertFalse(data == data_different_labels) + + def testInlineAlgebra(self): X, Y, Z = 8, 16, 32 a = np.ones((X, Y, Z), dtype='float32') From d026e6e56c18da518ca717f0d64ba331c39918ce Mon Sep 17 00:00:00 2001 From: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com> Date: Wed, 25 Sep 2024 12:46:03 +0100 Subject: [PATCH 2/2] Update README.md (#1930) Signed-off-by: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com> --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0c4937c318..e77b9a9768 100644 --- a/README.md +++ b/README.md @@ -27,15 +27,16 @@ conda create --name cil -c conda-forge -c https://software.repos.intel.com/pytho To install CIL and the additional packages and plugins needed to run the [CIL demos](https://github.com/TomographicImaging/CIL-Demos) install the environment with: ```sh -conda create --name cil -c conda-forge -c https://software.repos.intel.com/python/conda -c ccpi cil=24.1.0 astra-toolbox=*=cuda* tigre ccpi-regulariser tomophantom ipywidgets +conda create --name cil -c conda-forge -c https://software.repos.intel.com/python/conda -c ccpi cil=24.1.0 astra-toolbox=*=cuda* tigre ccpi-regulariser tomophantom ipykernel ipywidgets ``` where: -- `astra-toolbox` enables CIL support for [ASTRA toolbox](http://www.astra-toolbox.com) CPU projector (2D Parallel beam only) (GPLv3 license) +- `astra-toolbox=*=py*` enables CIL support for [ASTRA toolbox](http://www.astra-toolbox.com) CPU projector (2D Parallel beam only) (GPLv3 license) - `astra-toolbox=*=cuda*` (requires an NVIDIA GPU) enables CIL support for [ASTRA toolbox](http://www.astra-toolbox.com) GPU projectors (GPLv3 license) - `tigre` (requires an NVIDIA GPU) enables support for [TIGRE](https://github.com/CERN/TIGRE) toolbox projectors (BSD license) - `ccpi-regulariser` is the [CCPi Regularisation Toolkit](https://github.com/TomographicImaging/CCPi-Regularisation-Toolkit) - `tomophantom` can generate phantoms to use as test data [Tomophantom](https://github.com/dkazanc/TomoPhantom) +- `ipykernel` provides the IPython kernel for Jupyter (allowing jupyter notebooks to be run) - `ipywidgets` enables visulisation tools within jupyter noteboooks ### Dependencies