-
Notifications
You must be signed in to change notification settings - Fork 61
Tigre algorithm wrapping #2158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Tigre algorithm wrapping #2158
Conversation
|
Discussed with developers today:
|
|
Discussed with developers:
|
|
@gfardell - I was having another think about this - we discussed writing the wrapper as a processor but to force the code to have an I was wondering if we could write it as a reconstructor like in the recon class with just an init and run method? It also already has the machinery to check the image and acquisition geometries. I think it might make sense if the processors go from the same type of data e.g. image to image and acquisition to acquisition but the reconstructors go from acquisition to image |
gfardell
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @MargaretDuff. I think this is a nice solution to wrapping different algorithms. I think the comments are all fairly minor, just about useability and cleaning up some code. We can discuss if you have the resources to make the changes yourself or if you want one of us to take it over from here.
| from tigre.utilities.gpu import GpuIds | ||
| has_gpu_sel = True | ||
| except ModuleNotFoundError: | ||
| has_gpu_sel = False | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might all be to support an older version of tigre so probably not necessary here.
| def __init__(self, name=None, initial=None, image_geometry=None, data=None, niter=0, **kwargs): | ||
| """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The argument order seems strange to me. I would say that the algorithm string, data and number of iterations are necessary, and that Image geometry and initial are optional.
There might be other CIL conventions that motivated this order and would be worth discussing.
I would also go with algorithm_name and num_iterations or iterations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the choice was the ordering that tigre uses, with the addition of 'name' at the beginning. Agree algorithm_name is better.
| Raises | ||
| ------ | ||
| ValueError | ||
| If `image_geometry` or `data` is None. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if image_geometry is None then a default should be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok - we will just require passing the data
| missing = [] | ||
| if image_geometry is None: | ||
| missing.append("`image_geometry`") | ||
| if data is None: | ||
| missing.append("`data`") | ||
|
|
||
| if missing: | ||
| raise ValueError(f"You must pass {', '.join(missing)}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should only care about data here, and I suspect the parent class will handle the checking that it's AcquisitionData and it exists - it's worth checking that's a useful error message though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I have changed this so it only raises an error if data is not passed
| if initial is None: | ||
| initial = image_geometry.allocate(0) | ||
|
|
||
| self.tigre_initial = initial.copy().as_array() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the case where initial is None, allocating zeros and then copying isn't necessary.
I'd also consider carefully if we need a copy at all. Does tigre change the value? Or is this array reused for the solution, in which case we can hijack this behaviour for our out.
| from tigre.utilities.gpu import GpuIds | ||
| gpuids = GpuIds() | ||
| gpuids.devices = [0] # Specify the GPU device IDs you want to use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this is something we should be exposing elsewhere and have a consistent interface (i.e. take a list of IDs).
But in this case it seems like a kwarg passed directly to the algorithm, which suits out 'light touch' approach, are there other kwargs they might pass?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, they can pass a huge number and it differs per algorithm which ones they can pass
| def set_input(self, input): | ||
| """ | ||
| When called by the parent class during initialisation, sets the input data to run the reconstructor on. The geometry of the dataset must be compatible with the reconstructor. | ||
| When called after initialisation, raises NotImplementedError as changing the input is not currently supported. | ||
| Parameters | ||
| ---------- | ||
| input : AcquisitionData | ||
| A dataset with a compatible geometry | ||
| """ | ||
| if self._input is None: | ||
| if input.geometry != self.acquisition_geometry: | ||
| raise ValueError ("Input not compatible with configured reconstructor. Initialise a new reconstructor with this geometry") | ||
| else: | ||
| self._input = weakref.ref(input) | ||
|
|
||
| else: | ||
| raise NotImplementedError("Setting the input after initialisation is not currently supported.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't understood what this is about, is it that you don't want to inherit the method from the parent class? If it's not something we want in the API maybe we could separate it's use in the parent so we have a private method for checking the input, and remove the interface for this method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's to make it comptible for being a "reconstructor" and is called by the parent class at initialisation but I don't want the user to use it (at the moment). Agree, making it private method would be useful
| if has_gpu_sel: | ||
| result = self.tigre_algo( | ||
| proj=self.tigre_projections, | ||
| geo=self.tigre_geom, | ||
| angles=self.tigre_angles, | ||
| init=self.tigre_initial, | ||
| niter=self.niter, | ||
| gpuids=self.gpuids, | ||
| **self.kwargs | ||
| ) | ||
| else: | ||
| result = self.tigre_algo( | ||
| proj=self.tigre_projections, | ||
| geo=self.tigre_geom, | ||
| angles=self.tigre_angles, | ||
| init=self.tigre_initial, | ||
| niter=self.niter, | ||
| **self.kwargs | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's worth being certain we need to support both of these.
|
|
||
| img = result[0] if isinstance(result, tuple) else result | ||
|
|
||
| quality = result[1] if len(result) > 1 else None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is result is the data array this would still pass. I think it should be under the isinstance(result,tuple) logic to unpack the tuple.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have made this clearer
| if out is None: | ||
| out = self._image_geometry.allocate(0) | ||
|
|
||
| out.fill(img) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If out=None then we should wrap the numpy array as a CIL ImageData and remove the allocate and fill.
If out is passed and we are only using it to fill the result maybe that's something we should consider carefully. I would be tempted to not take out in this case, but maybe that's not consistent with CIL elsewhere.
|
I have addressed the simple comments. I think there are two major issues left - how Happy for the core developer team to take this over. |
Changes
This has been tested on all geometries for most of the currently available tigre algorithms, see here https://github.com/MargaretDuff/cil_test_notebooks/blob/master/os-sart-tigre/basic_tigre_wrapping.ipynb and the unit tests.
There is a bug with those Tigre algorithms that enforce TV regularisation using the "im3ddeoniser" in tigre, if you have a 2d geometry and if you are running on multiple gpus. A warning will show for users and setting the
gpuidsin the kwargs works ( see the test notebook). A bug report has been put into tigre CERN/TIGRE#681Discarded options:
Alternative discarded solution
### Option 1 - wrapped tigre's OS SART, SIRT and SART as CIL algorithms so they could be called e.g. ```python from cil.plugins.tigre import ART, SIRT, SART, OSSART OSSART_algorithm_wrap = OSSART(initial=None, image_geometry=ig, data=b, blocksize=10) OSSART_algorithm_wrap.run(5) solution= OSSART_algorithm_wrap.solution ``` To do: - [x] Test on 2D geometries and cone beam geometries - [ ] Test the callback functionality and algorithm stopping - [ ] Decide if we want to explicitly expose more options from Tigre e.g. OrderStrategy for OS SART. Any unused kwargs will be passed. - [x] Investigate memory-efficient ways of calculating the objective - [ ] Decide what the user needs to provide for the algorithm to run. At minimum, data (with acquisition geometry) and image geometry. Image geometry could be extracted from an initial, if that is passed. If both image geometry and initial is passed, do we check their geometries are the same? - [ ] Look at ways we can save/reuse memory - [ ] Write unit tests - [x] Write doc strings - including explaining that this is just for CT forward problems.
See here: https://github.com/MargaretDuff/cil_test_notebooks/blob/master/os-sart-tigre/cil_data_tigre_sart_sirt.ipynb
Related issues/links
Closes #2159
Checklist
Contribution Notes
Please read and adhere to the developer guide and local patterns and conventions.