Skip to content
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

Added new features to the ndcube.__add__ method #794

Open
wants to merge 66 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
9c38077
Added new features to the ndcube._add_ method
PCJY Dec 11, 2024
1d5d2ab
Merge branch 'main' of https://github.com/sunpy/ndcube into nddataAri…
PCJY Dec 16, 2024
aaa9ef0
Update ndcube/ndcube.py
PCJY Dec 16, 2024
ed4f61e
Update ndcube/ndcube.py
PCJY Dec 16, 2024
ea43a1d
Modified the _add_ method further.
PCJY Dec 18, 2024
a891ff9
Merge branch 'nddataArithmetic' of https://github.com/PCJY/ndcube int…
PCJY Dec 18, 2024
f575e2c
Further modifies the _add_ method.
PCJY Dec 23, 2024
8951635
Added a changelog file for this new feature.
PCJY Dec 23, 2024
58e4363
Merge branch 'main' of https://github.com/sunpy/ndcube into nddataAri…
PCJY Dec 23, 2024
bcf4fb9
Added a new method test_cube_add_uncertainty_and_mask to test_ndcube.py.
PCJY Dec 23, 2024
c4d639a
Modified the test_cube_add_uncertainty_and_mask method in test_ndcube.py
PCJY Dec 23, 2024
bd317e3
Modified the test_cube_add_uncertainty_and_mask further.
PCJY Dec 23, 2024
e0375ec
Fixed how the masks are combined.
PCJY Jan 14, 2025
0158737
Set masked uncertainty entries to 0.
PCJY Jan 18, 2025
9074f45
Moved uncertainty combination out of the mask-combining If Statements.
PCJY Jan 21, 2025
9e267d3
Merge branch 'main' of https://github.com/sunpy/ndcube into nddataAri…
PCJY Jan 21, 2025
d8c2db9
Merge branch 'main' into nddataArithmetic
PCJY Jan 21, 2025
5f422f5
Removed mask-dealing in the add method.
PCJY Jan 22, 2025
3369223
Merge branch 'nddataArithmetic' of https://github.com/PCJY/ndcube int…
PCJY Jan 22, 2025
f17da78
Removed mask-dealing in the Add method.
PCJY Jan 22, 2025
344b6f7
use a conditional statement to still check whether there is a mask.
PCJY Jan 22, 2025
7ff78aa
Changed mask to False and removed mask-checking in test_cube_add_unce…
PCJY Jan 22, 2025
5852daa
Added placeholders for using the new parameters and modified the no-m…
PCJY Jan 22, 2025
5dcb8ff
Set default of operation_ignores_mask to be True.
PCJY Jan 22, 2025
b385643
Make NDCube.__add__ call the NDCube.add method.
PCJY Jan 28, 2025
9941993
tidied up the __add__ method, copied the original test_cube_arithmeti…
PCJY Jan 29, 2025
4568ae2
Only check whether value has unit if it is not an NDData
PCJY Feb 3, 2025
b1cf478
Merge branch 'main' of https://github.com/sunpy/ndcube into nddataAri…
PCJY Feb 3, 2025
bb2c541
Update ndcube/ndcube.py
PCJY Feb 4, 2025
3f6ebed
Update ndcube/ndcube.py
PCJY Feb 4, 2025
3b76d54
Update ndcube/tests/test_ndcube.py
PCJY Feb 4, 2025
e7701b2
Merge branch 'nddataArithmetic' of https://github.com/PCJY/ndcube int…
PCJY Feb 4, 2025
64cc02e
Fix uncertainty propagation and ensure expected_uncertainty is numpy …
PCJY Feb 4, 2025
48b313e
Apply suggestions from code review
PCJY Feb 11, 2025
50d64c1
Merge branch 'main' of https://github.com/sunpy/ndcube into nddataAri…
PCJY Feb 11, 2025
23fef8a
check value and unit of addition
PCJY Feb 11, 2025
ed9d5f1
Update ndcube/ndcube.py
PCJY Feb 11, 2025
02f86b3
Merge branch 'main' of https://github.com/sunpy/ndcube into nddataAri…
PCJY Feb 17, 2025
7ea75f3
change values for uncertainty in a fixture to fixed values.
PCJY Feb 17, 2025
f31768d
Merge branch 'nddataArithmetic' of https://github.com/PCJY/ndcube int…
PCJY Feb 17, 2025
efafb89
Merge branch 'main' into nddataArithmetic
PCJY Feb 17, 2025
a32e474
Merge branch 'nddataArithmetic' of https://github.com/PCJY/ndcube int…
PCJY Feb 17, 2025
83d99cf
added unit in ndcube for kwargs['data'], changed values for uncertainty.
PCJY Feb 18, 2025
3b5a0ce
new test method for units of both objects being None.
PCJY Feb 18, 2025
9fbb9e3
Merge branch 'main' of https://github.com/sunpy/ndcube into nddataAri…
PCJY Feb 20, 2025
1f0ffc6
within a new ndcube-dev env, removed any unit involved for now.
PCJY Feb 20, 2025
fd78f6f
Added new test case for only one of them having a unit.
PCJY Feb 20, 2025
b284e1f
Test case for both objects having the same unit, and causes TypeError.
PCJY Feb 21, 2025
379faac
Fix test for adding nddata and ndcube uncertainties.
DanRyanIrish Feb 24, 2025
c6070c0
Added more test functions for full coverage.
PCJY Feb 24, 2025
d88838d
Fix pytest indirect issue: Added cube(request) fixture to correctly r…
PCJY Feb 26, 2025
f097110
Fix indirect fixture reference.
DanRyanIrish Feb 26, 2025
c920a10
Merge branch 'DanRyanIrish-nddataArithmetic' into nddataArithmetic
PCJY Feb 26, 2025
5565408
Written all tests and fixed an error in ndcube with test results.
PCJY Feb 27, 2025
51d26c1
Fixed a small error in a test function.
PCJY Feb 27, 2025
46cef9d
changed assert_cubes_equal, fixed self-referring of tests.
PCJY Feb 28, 2025
1eb0357
Update ndcube/tests/helpers.py
PCJY Mar 3, 2025
553b1d7
Changed the naming of test functions.
PCJY Mar 3, 2025
8f9baa4
Merge branch 'nddataArithmetic' of https://github.com/PCJY/ndcube int…
PCJY Mar 3, 2025
fdecacd
Changed the way to check whether both objects' uncertainty are None.
PCJY Mar 3, 2025
3ab2ead
Three conditional scenarios.
PCJY Mar 3, 2025
fdee146
Rewrote the uncertainty results checking.
PCJY Mar 4, 2025
d755849
Rewrote the uncertainty checking again.
PCJY Mar 4, 2025
5b9626f
Implementing mask.
PCJY Mar 11, 2025
3f1b8ef
Implementing mask.
PCJY Mar 11, 2025
22a673b
Added Fill() Method's skeleton.
PCJY Mar 14, 2025
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
1 change: 1 addition & 0 deletions changelog/794.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allows addition of an ``NDCube`` and ``NDData`` (with the WCS of ``NDData`` being set to None), and combines their uncertainties and masks.
36 changes: 36 additions & 0 deletions ndcube/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,39 @@ def ndcube_2d_ln_lt_units(wcs_2d_lt_ln):
return NDCube(data_cube, wcs=wcs_2d_lt_ln, unit=u.ct)


@pytest.fixture
def ndcube_2d_ln_lt_no_unit_no_unc(wcs_2d_lt_ln):
shape = (10, 12)
data_cube = data_nd(shape).astype(float)
return NDCube(data_cube, wcs=wcs_2d_lt_ln)


@pytest.fixture
def ndcube_2d_unit_unc(wcs_2d_lt_ln):
shape = (10, 12)
data_cube = data_nd(shape).astype(float)
uncertainty = StdDevUncertainty(np.ones(shape)*0.2, unit=u.ct)

return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty, unit=u.ct)


@pytest.fixture
def ndcube_2d_uncertainty_no_unit(wcs_2d_lt_ln):
shape = (10, 12)
data_cube = data_nd(shape).astype(float)
uncertainty = StdDevUncertainty(np.ones(shape)*0.2)

return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty)


@pytest.fixture
def ndcube_2d_ln_lt_mask(wcs_2d_lt_ln):
shape = (10, 12)
data_cube = data_nd(shape).astype(float)
mask = np.ones(data_cube.shape, dtype=bool)
return NDCube(data_cube, wcs=wcs_2d_lt_ln, mask=mask)


@pytest.fixture
def ndcube_2d_dask(wcs_2d_lt_ln):
shape = (8, 4)
Expand Down Expand Up @@ -719,6 +752,9 @@ def ndcube_1d_l(wcs_1d_l):
"ndcube_2d_ln_lt_units",
"ndcube_2d_dask",
"ndcube_1d_l",
"ndcube_2d_ln_lt_no_unit_no_unc",
"ndcube_2d_uncertainty_no_unit",
"ndcube_2d_unit_unc",
])
def all_ndcubes(request):
"""
Expand Down
122 changes: 117 additions & 5 deletions ndcube/ndcube.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import astropy.nddata
import astropy.units as u
from astropy.nddata import NDData
from astropy.units import UnitsError
from astropy.wcs.utils import _split_matrix

Expand Down Expand Up @@ -964,24 +965,113 @@
def __neg__(self):
return self._new_instance(data=-self.data)

def __add__(self, value):
if hasattr(value, 'unit'):
def add(self, value, operation_ignores_mask=True, handle_mask=np.logical_and):
"""
Users are allowed to choose whether they want operation_ignores_mask to be True or False,
and are allowed to choose whether they want handle_mask to be AND / OR .
"""
kwargs = {}

if isinstance(value, NDData) and value.wcs is None:
if self.unit is not None and value.unit is not None:
value_data = (value.data * value.unit).to_value(self.unit)
elif self.unit is None and value.unit is None:
value_data = value.data
else:
raise TypeError("Adding objects requires both have a unit or neither has a unit.") # change the test as well.

# check whether there is a mask.
# Neither self nor value has a mask
self_unmasked = self.mask is None or self.mask is False or not self.mask.any()
value_unmasked = value.mask is None or value.mask is False or not value.mask.any()

if (self_unmasked and value_unmasked) or operation_ignores_mask is True:
# addition
kwargs["data"] = self.data + value_data

# combine the uncertainty;
if self.uncertainty is not None and value.uncertainty is not None:
result_data = kwargs["data"]
if self.unit is not None:
result_data *= self.unit
new_uncertainty = self.uncertainty.propagate(
np.add, value, result_data=result_data, correlation=0
)
kwargs["uncertainty"] = new_uncertainty
elif self.uncertainty is not None:
new_uncertainty = self.uncertainty
kwargs["uncertainty"] = new_uncertainty
elif value.uncertainty is not None:
new_uncertainty = value.uncertainty
kwargs["uncertainty"] = new_uncertainty
else:
new_uncertainty = None
else:
# TODO
# When there is a mask, that is when the two new added parameters (OIM and HM) come into the picture.
# Conditional statements to permutate the two different scenarios (when it does not ignore the mask).
kwargs["data"] = self.data + value_data
self_data, value_data = self.data, value.data # May require a copy
self_mask, value_mask = self.mask, value.mask # May require handling/converting of cases when masks aren't boolean arrays but are None, True, or False.
if not operation_ignores_mask:
no_op_value = 0 # Value to set masked values since we are doing addition. (Would need to be 1 if we were doing multiplication.)
if (self_mask is True and value_mask is False):
idx = np.logical_and(self_mask, np.logical_not(value_mask))
self_data[idx] = no_op_value
elif (self_mask is False and value_mask is True):
idx = np.logical_and(value_mask, np.logical_not(self_mask))
value_data[idx] = no_op_value
elif (self_mask is True and value_mask is True):
idx = np.logical_and(self_mask, value_mask)
self_data[idx] = no_op_value
value_data[idx] = no_op_value

Check warning on line 1027 in ndcube/ndcube.py

View check run for this annotation

Codecov / codecov/patch

ndcube/ndcube.py#L1013-L1027

Added lines #L1013 - L1027 were not covered by tests

#self_data[idx], value_data[idx] = ?, ? # Handle case when both values are masked here. # We are yet to decide the best behaviour here.
# if both are F, no operation of setting the values to be 0 needs to be done.
else:
pass # If operation ignores mask, nothing needs to be done. This line not needed in actual code. Only here for clarity.

Check warning on line 1032 in ndcube/ndcube.py

View check run for this annotation

Codecov / codecov/patch

ndcube/ndcube.py#L1032

Added line #L1032 was not covered by tests

# Perform addition
new_data = self_data + value_data

Check warning on line 1035 in ndcube/ndcube.py

View check run for this annotation

Codecov / codecov/patch

ndcube/ndcube.py#L1035

Added line #L1035 was not covered by tests
# Calculate new mask.
new_mask = handle_mask(self_mask, value_mask) if handle_mask else None
kwargs["data"] = new_data
kwargs["mask"] = new_mask

Check warning on line 1039 in ndcube/ndcube.py

View check run for this annotation

Codecov / codecov/patch

ndcube/ndcube.py#L1037-L1039

Added lines #L1037 - L1039 were not covered by tests

elif hasattr(value, 'unit'):
if isinstance(value, u.Quantity):
# NOTE: if the cube does not have units, we cannot
# perform arithmetic between a unitful quantity.
# This forces a conversion to a dimensionless quantity
# so that an error is thrown if value is not dimensionless
cube_unit = u.Unit('') if self.unit is None else self.unit
new_data = self.data + value.to_value(cube_unit)
kwargs["data"] = self.data + value.to_value(cube_unit)
else:
# NOTE: This explicitly excludes other NDCube objects and NDData objects
# which could carry a different WCS than the NDCube
return NotImplemented
elif self.unit not in (None, u.Unit("")):
raise TypeError("Cannot add a unitless object to an NDCube with a unit.")
else:
new_data = self.data + value
return self._new_instance(data=new_data)
kwargs["data"] = self.data + value

Check warning on line 1056 in ndcube/ndcube.py

View check run for this annotation

Codecov / codecov/patch

ndcube/ndcube.py#L1056

Added line #L1056 was not covered by tests

# return the new NDCube instance
return self._new_instance(**kwargs)

def __add__(self, value):
# when value has a mask, raise error and point user to the add method. TODO
#
# check whether there is a mask.
# Neither self nor value has a mask

self_masked = not(self.mask is None or self.mask is False or not self.mask.any())
value_masked = not(value.mask is None or value.mask is False or not value.mask.any()) if hasattr(value, "mask") else False

if (value_masked or (self_masked and hasattr(value,'uncertainty') and value.uncertainty is not None)): # value has a mask,
# let the users call the add method
raise TypeError('Please use the add method.')

return self.add(value) # the mask keywords cannot be given by users.

def __radd__(self, value):
return self.__add__(value)
Expand Down Expand Up @@ -1330,6 +1420,28 @@
return self[tuple(item)]


def fill(fill_value, unmask=False, uncertainty_fill_value=None, fill_in_place=False):
"""
Replaces masked data values with input value.

Returns a new instance or alters values in place.

Parameters
----------
fill_value: `numbers.Number` or scalar `astropy.unit.Quantity`
The value to replace masked data with.
unmask: `bool`, optional
If True, the newly filled masked values are unmasked. If False, they remain masked
Default=False
uncertainty_fill_value: `numbers.Number` or scalar `astropy.unit.Quantity`, optional
The value to replace masked uncertainties with.
fill_in_place: `bool`, optional
If `True`, the masked values are filled in place. If `False`, a new instance is returned
with masked values filled. Default=False.
"""
# ...code implementation here.


def _create_masked_array_for_rebinning(data, mask, operation_ignores_mask):
m = None if (mask is None or mask is False or operation_ignores_mask) else mask
if m is None:
Expand Down
17 changes: 15 additions & 2 deletions ndcube/tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,26 @@
assert test_input[key] == expected_output[key]


def assert_cubes_equal(test_input, expected_cube, check_data=True):
def assert_cubes_equal(test_input, expected_cube, check_data=True, check_uncertainty_values=False):
assert isinstance(test_input, type(expected_cube))
assert np.all(test_input.mask == expected_cube.mask)
if check_data:
np.testing.assert_array_equal(test_input.data, expected_cube.data)
assert_wcs_are_equal(test_input.wcs, expected_cube.wcs)
if test_input.uncertainty:
if check_uncertainty_values:
# Check output and expected uncertainty are of same type. Remember they could be None.
# If the uncertainties are not None,...
# Check units, shape, and values of the uncertainty.
if (test_input.uncertainty is not None and expected_cube.uncertainty is not None):
assert type(test_input.uncertainty) is type(expected_cube.uncertainty)
assert np.allclose(test_input.uncertainty.array, expected_cube.uncertainty.array), \
f"Expected uncertainty: {expected_cube.uncertainty}, but got: {test_input.uncertainty.array}"
elif test_input.uncertainty is None:
assert expected_cube.uncertainty is None, "Test uncertainty should not be None."
elif expected_cube.uncertainty is None:
assert test_input.uncertainty is None, "Test uncertainty should be None."

Check warning on line 142 in ndcube/tests/helpers.py

View check run for this annotation

Codecov / codecov/patch

ndcube/tests/helpers.py#L139-L142

Added lines #L139 - L142 were not covered by tests

elif test_input.uncertainty:
assert test_input.uncertainty.array.shape == expected_cube.uncertainty.array.shape
assert np.all(test_input.shape == expected_cube.shape)
assert_metas_equal(test_input.meta, expected_cube.meta)
Expand Down
Loading