Enable multiplying NDCube and NDData#840
Conversation
ndcube/ndcube.py
Outdated
| # return the new NDCube instance | ||
| return self._new_instance(**kwargs) | ||
|
|
||
| def add(self, value, handle_mask = np.logical_and): |
There was a problem hiding this comment.
This method needs a proper docstring. For an example, see the NDCubeBase.reproject_to method's docstring.
There was a problem hiding this comment.
Do the _operate_arithmetic and the multiply method also need docstring?
Co-authored-by: DanRyanIrish <ryand5@tcd.ie>
Co-authored-by: DanRyanIrish <ryand5@tcd.ie>
|
Hi @PCJY. On reflection, I think it would be simpler if the def _arithmetic_operate_with_nddata(self, operation, value, handle_mask):
# Do stuff
def add(self, value, handle_mask=np.logical_and):
if isinstance(value, NDData):
kwargs = self._arithmetic_operate_with_nddata("add", value, handle_mask)
elif hasattr(value, 'unit'):
# Put non-NDData code from self.add here...
# return the new NDCube instance
return self._new_instance(**kwargs)What do you think? |
Do you mean, _operate_arithmetic() method handles the NDData case, and then, the add/mul method handles the Quantity case? |
|
Another idea for making this simpler, is to create a default def _arithmetic_handle_mask(self, self_mask, value_mask):
if self_mask is None and value_mask is None:
return None
elif self_mask is None:
return value_mask
elif value_mask is None:
return self_mask
else:
return np.logical_and(self_mask, value_mask)Then in if handle_mask is None:
handle_mask = self._arithmetic_handle_mask
kwargs["mask"] = handle_mask(self.mask, value.mask)This would mean that if the user provides a This does subtlely change the behaviour, so it might make the tests break. But that's OK if we agree that the new behaviour is better. |
Yes. But add/mul calls also |
|
Apologies for the delay in getting these comments to you @PCJY. I wrote them but realised I didn't actually post them to the PR! |
|
Also, we need a changelog file this PR too. |
|
Hi @DanRyanIrish , here is my summary for all the methods we have now for scaling and addition for NDCube and wcs-less NDData. Please let me know if you have any feedback on this, thanks. 1, The ndcube.fill_masked() method 2, The default ndcube._arithmetic_handle_mask() method 3, The ndcube._combine_uncertainty() method 4, The ndcube._arithmetic_operate_with_nddata() method 5, The ndcube.add() method 6, The ndcube.__add__() method, the + operator Previously it was necessary because the handle_mask and operation_ignore_mask arguments must be set by users, in order to deal with masked elements before using the uncertainty propagation method. Now that we have many methods to deal with masks, we assume when users come to arithmetic operations, they have already got the masked data. We would not specify the masks' impact on the data inside the arithmetic operation methods. Therefore, this method just calls the ndcube.add() method. 7, The ndcube.multiply() method 8, The ndcube.__mul__() method, the * operator |
Co-authored-by: DanRyanIrish <ryand5@tcd.ie>
Co-authored-by: DanRyanIrish <ryand5@tcd.ie>
|
Hi @PCJY. Thanks for the summary of our progress. I concur that the next steps are to change the Keep up the good work! |
|
@PCJY: Here is a suggestion to discuss that I have reflected on as the project has evolved. The new_cube = cube1 + nddata1
new_cube.mask = np.logical_and(cube1.mask, nddata1.mask)Therefore, the maintenance, documentation, and publicisation of the Therefore, should we consider removing the |
Hi @DanRyanIrish , thanks for sharing your new ideas. I am thinking, then what about the code components in the add and multiply methods that handle the uncertainty and units? Without the add and multiply methods, how can the users combine these two attributes when it comes to the arithmetic operations for NDCube and wcs-less NDData? Please let me know your thoughts on this and what I might be missing, thank you. |
Or is it because, all the unit handling and stuffs are handled by the _arithmetic_operate_with_nddata() method, and by changing the add and multiply methods to __add__ and __mul__ methods, it is just removing redundant wrappers but not contents? |
This is exactly right.
Yes, that's right. Using |
Then what should we do about the handle_mask argument? The operator cannot have an argument, but the argument is present in the add method, which calls the _arithmetic_operate_with_nddata, which calls the _arithmetic_handle_mask method when handle_mask is None. |
We can simply delete the new_cube.mask = np.logical_and(cube1.mask, nddata1.mask) |
| # Construct expected cube | ||
| expected_data = ndc.data + value.data | ||
| expected_uncertainty = ndc.uncertainty.propagate( | ||
| expected_uncertainty = ndc.uncertainty.propagate( # is this the correct way to test uncertainty? no need to test propagate |
There was a problem hiding this comment.
This is OK in this case because, as you say, we aren't testing propagate, and if we are using multiple parameterizations, we might not be able to hard-code the expected result as it may be different each time. In this case, though, we only have one test case, so strictly speaking, it's better practice to explicitly set the expected value. But I don't think it's important to change here.
ndcube/tests/test_ndcube.py
Outdated
| ) | ||
| ], | ||
| indirect=("ndc",)) | ||
| def test_cube_arithmetic_multiply_ndcube_nddata(ndc, value): |
There was a problem hiding this comment.
It would probably be better to simplify this test by including the expected_cube in the parameterization. Then the test only has to calculate the output_cube and compare it to the expected_cube.
|
Hi @DanRyanIrish , I think I have fixed the test for __multiply__. When you have a moment, could you please review it? I’d appreciate any feedback you might have, thank you. |
DanRyanIrish
left a comment
There was a problem hiding this comment.
This looks good. Just one suggestion regarding where the result cubes should be defined.
ndcube/conftest.py
Outdated
| @pytest.fixture | ||
| def ndcube_2d_ln_lt_res_ndc_mask_unc_unit(wcs_2d_lt_ln): | ||
| data_cube = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| return NDCube(data_cube, wcs=wcs_2d_lt_ln) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def ndcube_2d_ln_lt_res_nddata_mask_unc_unit(wcs_2d_lt_ln): | ||
| unit = u.ct | ||
| data_cube = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| uncertainty = astropy.nddata.StdDevUncertainty(np.ones((2, 3))*0.1) | ||
| mask = np.ones((2, 3), dtype=bool) | ||
| return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty, mask=mask, unit=unit) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def ndcube_2d_ln_lt_res_both_mask_unc_unit(wcs_2d_lt_ln): | ||
| unit = u.ct**2 | ||
| data_cube = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| uncertainty = astropy.nddata.StdDevUncertainty(np.array([[0.1118034, 0.1118034, 0.1118034], | ||
| [0.1118034, 0.1118034, 0.1118034]])) | ||
| mask = np.ones((2, 3), dtype=bool) | ||
| return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty, mask=mask, unit=unit) |
There was a problem hiding this comment.
Fixtures are typically defined in conftest so they can be used as input for multiple tests. As these are the expected results of specific tests, it would be better to move them to the parameterisation of the relevant tests.
There was a problem hiding this comment.
These still need to be deleted.
Co-authored-by: DanRyanIrish <ryand5@tcd.ie>
Co-authored-by: DanRyanIrish <ryand5@tcd.ie>
|
Congratulations and thank you, @PCJY , for this PR 🥳 |
Thank you for all the kind help and support! |
PR Description
Enable NDCube to be multiplied with wcs-less NDData.
To Do
Adapting
NDCube.addto support addition and multiplication.NDCube.addtoNDCube._operate_arithmeticand add anoperationarg.operationshould be a string of either"add"or"multiply".NDCube.addmethod that simply returnsself._operate_arithmeticwithoperationequal to'add'.NDCube._operate_arithmeticto check whetheroperation == 'add'. (The'multiply'case can be supported later.) There are three places where if statements are needed:NDDataandQuantitysections of the code.operationarg toNDCube._combine_uncertainty, i.e.def _combine_uncertainty(self, value, result_data, operation). Replacenp.addwithoperationin the code of this method. Make sure you addnp.addwhere you callself._combine_uncertaintyinNDCube._operation_arithmetic.Adding support for multiplication.
elifstatements toNDCube._operation_arithmeticto support multiplication. Remember, there are three scenarios where multiplication differs from addition:+->*)np.add->np.multiply)Quantitysection of the method, as well as theNDDatasection.NDCube.multiplymethod returnself._operate_arithmeticwithoperationset to'multiply'.NDCube.__mul__callNDCube.multiplyjust likeNDCube.__add__callsNDCube.add.NDCube.__mul__.