Skip to content

Commit 6092705

Browse files
authored
Merge pull request #829 from PCJY/NDCubefill
NDCube.fill_masked() method
2 parents b2f1282 + c85c74a commit 6092705

File tree

5 files changed

+256
-3
lines changed

5 files changed

+256
-3
lines changed

changelog/829.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added ``fill_masked`` method to ``NDCube``, a new feature which allows users to replace masked values and uncertainty values with user-given fill values,
2+
to change the mask values back to False or not (Default), and to set whether the new instance is returned (Default) or not.

ndcube/conftest.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,18 @@ def ndcube_2d_ln_lt_uncert(wcs_2d_lt_ln):
646646
return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty)
647647

648648

649+
@pytest.fixture
650+
def ndcube_2d_ln_lt_mask(wcs_2d_lt_ln):
651+
shape = (10, 12)
652+
data_cube = data_nd(shape)
653+
mask = np.zeros(shape, dtype=bool)
654+
mask[1, 1] = True
655+
mask[2, 0] = True
656+
mask[3, 3] = True
657+
mask[4:6, :4] = True
658+
return NDCube(data_cube, wcs=wcs_2d_lt_ln, mask=mask)
659+
660+
649661
@pytest.fixture
650662
def ndcube_2d_ln_lt_mask_uncert(wcs_2d_lt_ln):
651663
shape = (10, 12)
@@ -659,6 +671,79 @@ def ndcube_2d_ln_lt_mask_uncert(wcs_2d_lt_ln):
659671
return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty, mask=mask)
660672

661673

674+
@pytest.fixture
675+
def ndcube_2d_ln_lt_mask_uncert_unit_mask_false(wcs_2d_lt_ln):
676+
shape = (2, 3)
677+
unit = u.ct
678+
data_cube = data_nd(shape)
679+
uncertainty = astropy.nddata.StdDevUncertainty(data_cube * 0.1)
680+
mask = False
681+
return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty, mask=mask, unit=unit)
682+
683+
684+
@pytest.fixture
685+
def ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true(wcs_2d_lt_ln):
686+
shape = (2, 3)
687+
unit = u.ct
688+
data_cube = data_nd(shape)
689+
uncertainty = astropy.nddata.StdDevUncertainty(data_cube * 0.1)
690+
mask = np.zeros(shape, dtype=bool)
691+
mask[0:1, 0] = True
692+
return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty, mask=mask, unit=unit)
693+
694+
695+
@pytest.fixture
696+
def ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true_expected_unmask_false(wcs_2d_lt_ln):
697+
shape = (2, 3)
698+
unit = u.ct
699+
data_cube = np.array([[1.0, 1.0, 2.0],
700+
[3.0, 4.0, 5.0]])
701+
uncertainty = astropy.nddata.StdDevUncertainty(data_cube * 0.1)
702+
mask = np.zeros(shape, dtype=bool)
703+
mask[0:1, 0] = True
704+
return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty, mask=mask, unit=unit)
705+
706+
707+
@pytest.fixture
708+
def ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true_expected_unmask_true(wcs_2d_lt_ln):
709+
unit = u.ct
710+
data_cube = np.array([[1.0, 1.0, 2.0],
711+
[3.0, 4.0, 5.0]])
712+
uncertainty = astropy.nddata.StdDevUncertainty(data_cube * 0.1)
713+
mask = False
714+
return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty, mask=mask, unit=unit)
715+
716+
717+
@pytest.fixture
718+
def ndcube_2d_ln_lt_mask_uncert_unit_mask_true(wcs_2d_lt_ln):
719+
shape = (2, 3)
720+
unit = u.ct
721+
data_cube = data_nd(shape)
722+
uncertainty = astropy.nddata.StdDevUncertainty(data_cube * 0.1)
723+
mask = True
724+
return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty, mask=mask, unit=unit)
725+
726+
727+
@pytest.fixture
728+
def ndcube_2d_ln_lt_mask_uncert_unit_mask_true_expected_unmask_true(wcs_2d_lt_ln):
729+
unit = u.ct
730+
data_cube = np.array([[1.0, 1.0, 1.0],
731+
[1.0, 1.0, 1.0]])
732+
uncertainty = astropy.nddata.StdDevUncertainty(data_cube * 0.1)
733+
mask = False
734+
return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty, mask=mask, unit=unit)
735+
736+
737+
@pytest.fixture
738+
def ndcube_2d_ln_lt_mask_uncert_unit_mask_true_expected_unmask_false(wcs_2d_lt_ln):
739+
unit = u.ct
740+
data_cube = np.array([[1.0, 1.0, 1.0],
741+
[1.0, 1.0, 1.0]])
742+
uncertainty = astropy.nddata.StdDevUncertainty(data_cube * 0.1)
743+
mask = True
744+
return NDCube(data_cube, wcs=wcs_2d_lt_ln, uncertainty=uncertainty, mask=mask, unit=unit)
745+
746+
662747
@pytest.fixture
663748
def ndcube_2d_ln_lt_uncert_ec(wcs_2d_lt_ln):
664749
shape = (4, 9)
@@ -735,6 +820,14 @@ def ndc(request):
735820
return request.getfixturevalue(request.param)
736821

737822

823+
@pytest.fixture
824+
def expected_cube(request):
825+
"""
826+
A fixture for use with indirect to lookup other fixtures.
827+
"""
828+
return request.getfixturevalue(request.param)
829+
830+
738831
################################################################################
739832
# NDCubeSequence Fixtures
740833
################################################################################

ndcube/ndcube.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import abc
2+
import copy
23
import inspect
34
import numbers
45
import textwrap
@@ -1327,6 +1328,68 @@ def squeeze(self, axis=None):
13271328
return self[tuple(item)]
13281329

13291330

1331+
def fill_masked(self, fill_value, uncertainty_fill_value=None, unmask=False, fill_in_place=False):
1332+
"""
1333+
Replaces masked data values with input value.
1334+
1335+
Returns a new instance or alters values in place.
1336+
1337+
Parameters
1338+
----------
1339+
fill_value: `numbers.Number` or scalar `astropy.units.Quantity`
1340+
The value to replace masked data with.
1341+
unmask: `bool`, optional
1342+
If True, the newly filled masked values are unmasked. If False, they remain masked
1343+
Default=False
1344+
uncertainty_fill_value: `numbers.Number` or scalar `astropy.units.Quantity`, optional
1345+
The value to replace masked uncertainties with.
1346+
fill_in_place: `bool`, optional
1347+
If `True`, the masked values are filled in place. If `False`, a new instance is returned
1348+
with masked values filled. Default=False.
1349+
"""
1350+
# variable creations for later use.
1351+
# If fill_in_place is true, do: assign data and uncertainty to variables.
1352+
if fill_in_place:
1353+
new_data = self.data
1354+
new_uncertainty = self.uncertainty
1355+
# Unmasking in-place should be handled later.
1356+
1357+
# If fill_in_place is false, do: create new storage place for data and uncertainty and mask.
1358+
# TODO: is the logic repetitive? this else is the same with the if not fill_in_place below? No because the order matters.
1359+
else:
1360+
new_data = copy.deepcopy(self.data)
1361+
new_uncertainty = copy.deepcopy(self.uncertainty)
1362+
new_mask = False if unmask else copy.deepcopy(self.mask) # self.mask still exists.
1363+
1364+
masked = (
1365+
False if self.mask is None or self.mask is False
1366+
else self.mask is True if isinstance(self.mask, bool)
1367+
else self.mask.any()
1368+
)
1369+
if masked:
1370+
idx_mask = slice(None) if self.mask is True else self.mask # Ensure indexing mask can index the data array.
1371+
if hasattr(fill_value, "unit"):
1372+
fill_value = fill_value.to_value(self.unit)
1373+
new_data[idx_mask] = fill_value # python will error based on whether data array can accept the passed value.
1374+
1375+
if uncertainty_fill_value is not None:
1376+
if not self.uncertainty: # or new_uncertainty
1377+
raise TypeError("Cannot fill uncertainty as uncertainty is None.")
1378+
if hasattr(uncertainty_fill_value, "unit"):
1379+
uncertainty_fill_value = uncertainty_fill_value.to_value(self.unit)
1380+
new_uncertainty.array[idx_mask] = uncertainty_fill_value
1381+
1382+
if not fill_in_place:
1383+
# Create kwargs dictionary and return a new instance.
1384+
kwargs = {}
1385+
kwargs['data'] = new_data
1386+
kwargs['uncertainty'] = new_uncertainty
1387+
kwargs['mask'] = new_mask
1388+
return self._new_instance(**kwargs)
1389+
if unmask:
1390+
self.mask = False
1391+
return None
1392+
13301393
def _create_masked_array_for_rebinning(data, mask, operation_ignores_mask):
13311394
m = None if (mask is None or mask is False or operation_ignores_mask) else mask
13321395
if m is None:

ndcube/tests/helpers.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,33 @@ def assert_metas_equal(test_input, expected_output):
122122
assert test_input[key] == expected_output[key]
123123

124124

125-
def assert_cubes_equal(test_input, expected_cube, check_data=True):
125+
def assert_cubes_equal(test_input, expected_cube, check_data=True, check_uncertainty_values=False):
126126
assert isinstance(test_input, type(expected_cube))
127-
assert np.all(test_input.mask == expected_cube.mask)
127+
if isinstance(test_input.mask, bool):
128+
if not isinstance(expected_cube.mask, bool):
129+
raise AssertionError("Masks not of same type.")
130+
assert test_input.mask is expected_cube.mask
131+
else:
132+
assert np.all(test_input.mask == expected_cube.mask)
128133
if check_data:
129134
np.testing.assert_array_equal(test_input.data, expected_cube.data)
130135
assert_wcs_are_equal(test_input.wcs, expected_cube.wcs)
131-
if test_input.uncertainty:
136+
if check_uncertainty_values:
137+
# Check output and expected uncertainty are of same type. Remember they could be None.
138+
# If the uncertainties are not None,...
139+
# Check units, shape, and values of the uncertainty.
140+
if (test_input.uncertainty is not None and expected_cube.uncertainty is not None):
141+
assert type(test_input.uncertainty) is type(expected_cube.uncertainty)
142+
assert np.allclose(test_input.uncertainty.array, expected_cube.uncertainty.array), \
143+
f"Expected uncertainty: {expected_cube.uncertainty}, but got: {test_input.uncertainty.array}"
144+
elif test_input.uncertainty is None:
145+
assert expected_cube.uncertainty is None, "Test uncertainty should not be None." # pragma: no cover
146+
elif expected_cube.uncertainty is None:
147+
assert test_input.uncertainty is None, "Test uncertainty should be None." # pragma: no cover
148+
elif test_input.uncertainty:
132149
assert test_input.uncertainty.array.shape == expected_cube.uncertainty.array.shape
133150
assert np.all(test_input.shape == expected_cube.shape)
151+
134152
assert_metas_equal(test_input.meta, expected_cube.meta)
135153
if type(test_input.extra_coords) is not type(expected_cube.extra_coords):
136154
raise AssertionError(f"NDCube extra_coords not of same type: "

ndcube/tests/test_ndcube.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,3 +1384,80 @@ def test_set_data_mask(ndcube_4d_mask):
13841384

13851385
with pytest.raises(TypeError, match="Can not set the .data .* with a numpy masked array"):
13861386
cube.data = masked_array
1387+
1388+
1389+
@pytest.mark.parametrize(
1390+
("ndc", "fill_value", "uncertainty_fill_value", "unmask", "expected_cube"),
1391+
[
1392+
("ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true", 1.0, 0.1, False, "ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true_expected_unmask_false"), # when it changes the cube in place: its data, uncertainty; it does not unmask the mask.
1393+
("ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true", 1.0, 0.1, True, "ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true_expected_unmask_true"),
1394+
("ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true", 1.0 * u.ct, 0.1 * u.ct, False, "ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true_expected_unmask_false"), # fill_value has a unit
1395+
1396+
("ndcube_2d_ln_lt_mask_uncert_unit_mask_true", 1.0, 0.1, False, "ndcube_2d_ln_lt_mask_uncert_unit_mask_true_expected_unmask_false"), # when it changes the cube in place: its data, uncertainty; it does not unmask the mask.
1397+
("ndcube_2d_ln_lt_mask_uncert_unit_mask_true", 1.0, 0.1, True, "ndcube_2d_ln_lt_mask_uncert_unit_mask_true_expected_unmask_true"),
1398+
("ndcube_2d_ln_lt_mask_uncert_unit_mask_true", 1.0 * u.ct, 0.1* u.ct, False, "ndcube_2d_ln_lt_mask_uncert_unit_mask_true_expected_unmask_false"), # fill_value has a unit
1399+
# TODO: test unit not aligned??
1400+
1401+
("ndcube_2d_ln_lt_mask_uncert_unit_mask_false", 1.0, 0.1 * u.ct, False, "ndcube_2d_ln_lt_mask_uncert_unit_mask_false") # no change.
1402+
1403+
# TODO: are there more test cases needed?
1404+
],
1405+
indirect=("ndc", "expected_cube")
1406+
)
1407+
def test_fill_masked_fill_in_place_true(ndc, fill_value, uncertainty_fill_value, unmask, expected_cube):
1408+
# when the fill_masked method is applied on the fixture argument, it should
1409+
# give me the correct data value and type, uncertainty, mask, unit.
1410+
1411+
# original cube: [[0,1,2],[3,4,5]],
1412+
# original mask: scenario 1, [[T,F,F],[F,F,F]]; scenario 2, T; scenario 3, None.
1413+
# expected cube: [[1,1,2],[3,4,5]]; [[1,1,1], [1,1,1]]; [[0,1,2],[3,4,5]]
1414+
# expected mask: when unmask is T, becomes all false, when unmask is F, stays the same.
1415+
1416+
# perform the fill_masked method on the fixture, using parametrized as parameters.
1417+
ndc.fill_masked(fill_value, unmask=unmask, uncertainty_fill_value=uncertainty_fill_value, fill_in_place=True)
1418+
helpers.assert_cubes_equal(ndc, expected_cube, check_uncertainty_values=True)
1419+
1420+
1421+
@pytest.mark.parametrize(
1422+
("ndc", "fill_value", "uncertainty_fill_value", "unmask", "expected_cube"),
1423+
[
1424+
("ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true", 1.0, 0.1, False, "ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true_expected_unmask_false"), # when it changes the cube in place: its data, uncertainty; it does not unmask the mask.
1425+
("ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true", 1.0, 0.1, True, "ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true_expected_unmask_true"),
1426+
("ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true", 1.0 * u.ct, 0.1* u.ct, False, "ndcube_2d_ln_lt_mask_uncert_unit_one_maskele_true_expected_unmask_false"), # fill_value has a unit
1427+
1428+
("ndcube_2d_ln_lt_mask_uncert_unit_mask_true", 1.0, 0.1, False, "ndcube_2d_ln_lt_mask_uncert_unit_mask_true_expected_unmask_false"), # when it changes the cube in place: its data, uncertainty; it does not unmask the mask.
1429+
("ndcube_2d_ln_lt_mask_uncert_unit_mask_true", 1.0, 0.1, True, "ndcube_2d_ln_lt_mask_uncert_unit_mask_true_expected_unmask_true"),
1430+
("ndcube_2d_ln_lt_mask_uncert_unit_mask_true", 1.0 * u.ct, 0.1* u.ct, False, "ndcube_2d_ln_lt_mask_uncert_unit_mask_true_expected_unmask_false"), # fill_value has a unit
1431+
#TODO: test unit not aligned??
1432+
1433+
("ndcube_2d_ln_lt_mask_uncert_unit_mask_false", 1.0, 0.1 * u.ct, False, "ndcube_2d_ln_lt_mask_uncert_unit_mask_false") # no change.
1434+
1435+
# TODO: are there more test cases needed? yes: when uncertainty fill is not None but ndc's uncertainty is None.
1436+
],
1437+
indirect=("ndc", "expected_cube")
1438+
)
1439+
def test_fill_masked_fill_in_place_false(ndc, fill_value, uncertainty_fill_value, unmask, expected_cube):
1440+
# compare the expected cube with the cube saved in the new place
1441+
1442+
# perform the fill_masked method on the fixture, using parametrized as parameters.
1443+
filled_cube = ndc.fill_masked(fill_value, uncertainty_fill_value, unmask, fill_in_place=False)
1444+
helpers.assert_cubes_equal(filled_cube, expected_cube, check_uncertainty_values=True)
1445+
1446+
@pytest.mark.parametrize(
1447+
("ndc", "fill_value", "uncertainty_fill_value", "unmask"),
1448+
[
1449+
# cube has no uncertainty but uncertainty_fill_value has an uncertainty
1450+
("ndcube_2d_ln_lt_mask", 1.0, 0.1, False),
1451+
("ndcube_2d_ln_lt_mask", 1.0, 0.1 * u.ct, True),
1452+
],
1453+
indirect=("ndc",)
1454+
)
1455+
def test_fill_masked_ndc_uncertainty_none(ndc, fill_value, uncertainty_fill_value, unmask):
1456+
assert ndc.uncertainty is None
1457+
with pytest.raises(TypeError,match="Cannot fill uncertainty as uncertainty is None."):
1458+
ndc.fill_masked(
1459+
fill_value,
1460+
unmask=unmask,
1461+
uncertainty_fill_value=uncertainty_fill_value,
1462+
fill_in_place=True
1463+
)

0 commit comments

Comments
 (0)