From d8b2524dfbe8f418394443217602ee90173ddeb4 Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Thu, 18 Dec 2025 15:21:51 +0000 Subject: [PATCH 1/3] fix(nddata): handle mask propagation gaps --- astropy/nddata/mixins/ndarithmetic.py | 24 +++--- .../nddata/mixins/tests/test_ndarithmetic.py | 73 +++++++++++++++++++ 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/astropy/nddata/mixins/ndarithmetic.py b/astropy/nddata/mixins/ndarithmetic.py index 4153dfccb20f..d64cbd3d6f39 100644 --- a/astropy/nddata/mixins/ndarithmetic.py +++ b/astropy/nddata/mixins/ndarithmetic.py @@ -513,18 +513,24 @@ def _arithmetic_mask(self, operation, operand, handle_mask, axis=None, **kwds): ``handle_mask`` must create (and copy) the returned mask. """ # If only one mask is present we need not bother about any type checks - if ( - self.mask is None and operand is not None and operand.mask is None - ) or handle_mask is None: + if handle_mask is None: return None - elif self.mask is None and operand is not None: + + if operand is None: + return deepcopy(self.mask) + + operand_mask = operand.mask + + if self.mask is None and operand_mask is None: + return None + if self.mask is None: # Make a copy so there is no reference in the result. - return deepcopy(operand.mask) - elif operand is None: + return deepcopy(operand_mask) + if operand_mask is None: return deepcopy(self.mask) - else: - # Now lets calculate the resulting mask (operation enforces copy) - return handle_mask(self.mask, operand.mask, **kwds) + + # Now lets calculate the resulting mask (operation enforces copy) + return handle_mask(self.mask, operand_mask, **kwds) def _arithmetic_wcs(self, operation, operand, compare_wcs, **kwds): """ diff --git a/astropy/nddata/mixins/tests/test_ndarithmetic.py b/astropy/nddata/mixins/tests/test_ndarithmetic.py index 8c461155ab36..bb49ab541a37 100644 --- a/astropy/nddata/mixins/tests/test_ndarithmetic.py +++ b/astropy/nddata/mixins/tests/test_ndarithmetic.py @@ -314,6 +314,79 @@ def test_arithmetics_data_masks_invalid(): nd1.divide(nd2) +@pytest.mark.parametrize( + "mask", + [ + np.array([[True, False], [False, True]], dtype=np.bool_), + np.array([[1, 0], [0, 1]], dtype=np.uint8), + ], +) +@pytest.mark.parametrize("handle_mask", [np.bitwise_or, np.logical_or]) +def test_arithmetics_single_mask_scalar(handle_mask, mask): + nd = NDDataArithmetic(np.ones_like(mask, dtype=float), mask=mask) + + result = nd.multiply(1.0, handle_mask=handle_mask) + + assert_array_equal(mask, result.mask) + assert result.mask.dtype == mask.dtype + assert not np.may_share_memory(result.mask, mask) + + +def test_arithmetics_single_mask_nddataref_commutativity_bitwise_or(): + mask = np.array([[True, False], [False, True]], dtype=np.bool_) + data = np.arange(4).reshape(2, 2) + masked = NDDataArithmetic(data, mask=mask) + unmasked = NDDataArithmetic(data) + + forward = masked.multiply(unmasked, handle_mask=np.bitwise_or) + reverse = unmasked.multiply(masked, handle_mask=np.bitwise_or) + + assert_array_equal(mask, forward.mask) + assert_array_equal(mask, reverse.mask) + assert forward.mask.dtype == mask.dtype + assert reverse.mask.dtype == mask.dtype + assert_array_equal(forward.mask, reverse.mask) + assert not np.may_share_memory(forward.mask, mask) + assert not np.may_share_memory(reverse.mask, mask) + + +@pytest.mark.parametrize("handle_mask", [np.bitwise_or, np.logical_or]) +def test_arithmetics_both_masked(handle_mask): + mask1 = np.array([[True, False], [False, True]], dtype=np.bool_) + mask2 = np.array([[False, True], [True, False]], dtype=np.bool_) + + nd1 = NDDataArithmetic(np.ones((2, 2)), mask=mask1) + nd2 = NDDataArithmetic(np.ones((2, 2)), mask=mask2) + + result = nd1.add(nd2, handle_mask=handle_mask) + + expected = handle_mask(mask1, mask2) + assert_array_equal(expected, result.mask) + assert np.issubdtype(result.mask.dtype, np.bool_) + + +@pytest.mark.parametrize("handle_mask", [np.bitwise_or, np.logical_or]) +def test_arithmetics_no_masks(handle_mask): + nd1 = NDDataArithmetic(np.ones((2, 2))) + nd2 = NDDataArithmetic(np.ones((2, 2))) + + result = nd1.add(nd2, handle_mask=handle_mask) + + assert result.mask is None + + +def test_arithmetics_mask_preserved_with_array_operand(): + mask = np.array([True, False, True, False], dtype=np.bool_) + data = np.arange(mask.size) + + nd = NDDataArithmetic(data, mask=mask) + result = nd.add(np.ones_like(data), handle_mask=np.bitwise_or) + + assert_array_equal(mask, result.mask) + assert result.mask.dtype == mask.dtype + assert not np.may_share_memory(result.mask, mask) + + # Covering: # both have uncertainties (data and uncertainty without unit) # tested against manually determined resulting uncertainties to verify the From fca3719f7c2b56dad1aabc2aef78a8d6121a7e17 Mon Sep 17 00:00:00 2001 From: Rowan Stein Date: Tue, 6 Jan 2026 14:23:00 +0000 Subject: [PATCH 2/3] ci: trigger workflow (no-op change in tests) --- astropy/nddata/mixins/tests/test_ndarithmetic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/astropy/nddata/mixins/tests/test_ndarithmetic.py b/astropy/nddata/mixins/tests/test_ndarithmetic.py index bb49ab541a37..27793c8dd3c4 100644 --- a/astropy/nddata/mixins/tests/test_ndarithmetic.py +++ b/astropy/nddata/mixins/tests/test_ndarithmetic.py @@ -1383,3 +1383,4 @@ def test_raise_method_not_supported(): # raise error for unsupported propagation operations: with pytest.raises(ValueError): ndd1.uncertainty.propagate(np.mod, ndd2, result, correlation) +# CI trigger: no-op change From ea25c558293d061a69d0b38ea8d5bcbc0625d899 Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Sun, 11 Jan 2026 18:34:56 +0000 Subject: [PATCH 3/3] chore(tests): remove ci trigger comment --- astropy/nddata/mixins/tests/test_ndarithmetic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/astropy/nddata/mixins/tests/test_ndarithmetic.py b/astropy/nddata/mixins/tests/test_ndarithmetic.py index 27793c8dd3c4..bb49ab541a37 100644 --- a/astropy/nddata/mixins/tests/test_ndarithmetic.py +++ b/astropy/nddata/mixins/tests/test_ndarithmetic.py @@ -1383,4 +1383,3 @@ def test_raise_method_not_supported(): # raise error for unsupported propagation operations: with pytest.raises(ValueError): ndd1.uncertainty.propagate(np.mod, ndd2, result, correlation) -# CI trigger: no-op change