From 0838785004e8f86f592e9fb0109f3c1e09927ba3 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Fri, 15 Aug 2025 11:08:14 +0100 Subject: [PATCH 01/26] Enable subtraction of NDCube and NDData. --- ndcube/ndcube.py | 3 ++ ndcube/tests/test_ndcube_arithmetic.py | 49 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/ndcube/ndcube.py b/ndcube/ndcube.py index 531935bfc..c57719c30 100644 --- a/ndcube/ndcube.py +++ b/ndcube/ndcube.py @@ -1048,6 +1048,9 @@ def __radd__(self, value): return self.__add__(value) def __sub__(self, value): + if isinstance(value, NDData): + value.data[:] = -value.data + return self.__add__(value) return self.__add__(-value) def __rsub__(self, value): diff --git a/ndcube/tests/test_ndcube_arithmetic.py b/ndcube/tests/test_ndcube_arithmetic.py index 23ee3839f..0fd81e6e7 100644 --- a/ndcube/tests/test_ndcube_arithmetic.py +++ b/ndcube/tests/test_ndcube_arithmetic.py @@ -122,6 +122,55 @@ def test_cube_arithmetic_subtract(ndcube_2d_ln_lt_units, value): check_arithmetic_value_and_units(new_cube, cube_quantity - value) +@pytest.mark.parametrize(("ndc", "value", "expected_kwargs"), + [( + "ndcube_2d_ln_lt_no_unit_no_unc_no_mask_2", + NDData(np.ones((2, 3)), wcs=None), + {"data": np.array([[-1, 0, 1], [2, 3, 4]])} + ), + ( + "ndcube_2d_ln_lt_no_unit_no_unc_no_mask_2", + NDData(np.ones((2, 3)), + wcs=None, + uncertainty=StdDevUncertainty(np.ones((2, 3))*0.1), + mask=np.array([[True, False, False], [False, True, False]])), + {"data": np.array([[-1, 0, 1], [2, 3, 4]]), + "uncertainty": astropy.nddata.StdDevUncertainty(np.ones((2, 3))*0.1), + "mask": np.array([[True, False, False], [False, True, False]])} + ), # ndc has no mask no uncertainty no unit, but nddata has all. + ( + "ndcube_2d_ln_lt_unit_unc_mask", + NDData(np.ones((2, 3)), wcs=None, unit=u.ct), + {"data": np.array([[-1, 0, 1], [2, 3, 4]]), + "uncertainty": astropy.nddata.StdDevUncertainty(np.array([[0, 0.05, 0.1], + [0.15, 0.2, 0.25]])), + "mask": np.array([[False, True, True], [False, True, True]])} + ), # ndc has mask and uncertainty unit, but nddata doesn't. + ( + "ndcube_2d_ln_lt_unit_unc_mask", + NDData(np.ones((2, 3)), + wcs=None, + uncertainty=StdDevUncertainty(np.ones((2, 3))*0.1), + mask=np.array([[True, False, False], [False, True, False]]), + unit=u.ct), + {"unit": u.ct, + "data": np.array([[-1, 0, 1], [2, 3, 4]]), + "uncertainty": astropy.nddata.StdDevUncertainty(np.array([[0.1 , 0.1118034 , 0.14142136], + [0.18027756, 0.2236068 , 0.26925824]])), + "mask": np.array([[True, True, True], [False, True, True]])} + ) # both of them have uncertainty and mask and unit. + ], + indirect=("ndc",)) +def test_cube_arithmetic_subtract_nddata(ndc, value, expected_kwargs, wcs_2d_lt_ln): + output_cube = ndc - value # perform the subtraction + + expected_kwargs["wcs"] = wcs_2d_lt_ln + expected_cube = NDCube(**expected_kwargs) + + # Assert output cube is same as expected cube + assert_cubes_equal(output_cube, expected_cube, check_uncertainty_values=True) + + @pytest.mark.parametrize('value', [ 10 * u.ct, u.Quantity([10], u.ct), From af3220f4fc0ab5c0dee2828a058623abd759b93e Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Fri, 15 Aug 2025 16:06:38 +0100 Subject: [PATCH 02/26] Enable division of NDCube by NDData. --- ndcube/ndcube.py | 32 +++++++++++---- ndcube/tests/test_ndcube_arithmetic.py | 54 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/ndcube/ndcube.py b/ndcube/ndcube.py index c57719c30..31367dcf0 100644 --- a/ndcube/ndcube.py +++ b/ndcube/ndcube.py @@ -977,7 +977,6 @@ def _arithmetic_operate_with_nddata(self, operation, value): handle_mask = self._arithmetic_handle_mask if value.wcs is not None: return TypeError("Cannot add coordinate-aware NDCubes together.") - kwargs = {} if operation == "add": # Handle units @@ -990,20 +989,36 @@ def _arithmetic_operate_with_nddata(self, operation, value): # Handle data and uncertainty kwargs["data"] = self.data + value_data uncert_op = np.add - elif operation == "multiply": + elif operation in ("multiply", "true_divide"): # Handle units if self.unit is not None or value.unit is not None: cube_unit = u.Unit('') if self.unit is None else self.unit value_unit = u.Unit('') if value.unit is None else value.unit - kwargs["unit"] = cube_unit * value_unit - kwargs["data"] = self.data * value.data - uncert_op = np.multiply + kwargs["unit"] = (cube_unit * value_unit if operation == "multiply" + else cube_unit / value_unit) + if operation == "multiply": + kwargs["data"] = self.data * value.data + uncert_op = np.multiply + else: + kwargs["data"] = self.data / value.data + uncert_op = np.true_divide else: raise ValueError("Value of operation argument is not recognized.") - kwargs["uncertainty"] = self._combine_uncertainty(uncert_op, value, kwargs["data"]) + # Calculate uncertainty. + new_uncert = self._combine_uncertainty(uncert_op, value, kwargs["data"]) + if new_uncert: + # New uncertainty object must be decoupled from its original + # parent_nddata object. Set this to None here, and the parent_nddata + # will become the new cube on instantiation. + new_uncert.parent_nddata = None + uncert_unit = kwargs.get("unit", self.unit) + if uncert_unit: + # Give uncertainty object the same units as the new NDCube. + new_uncert.unit = uncert_unit + kwargs["uncertainty"] = new_uncert kwargs["mask"] = handle_mask(self.mask, value.mask) - return kwargs # return the new NDCube instance + return kwargs def __add__(self, value): kwargs = {} @@ -1083,6 +1098,9 @@ def __rmul__(self, value): return self.__mul__(value) def __truediv__(self, value): + if isinstance(value, NDData): + kwargs = self._arithmetic_operate_with_nddata("true_divide", value) + return self._new_instance(**kwargs) return self.__mul__(1/value) def __rtruediv__(self, value): diff --git a/ndcube/tests/test_ndcube_arithmetic.py b/ndcube/tests/test_ndcube_arithmetic.py index 0fd81e6e7..54b5ad071 100644 --- a/ndcube/tests/test_ndcube_arithmetic.py +++ b/ndcube/tests/test_ndcube_arithmetic.py @@ -280,6 +280,58 @@ def test_cube_arithmetic_divide(ndcube_2d_ln_lt_units, value): new_cube = ndcube_2d_ln_lt_units / value check_arithmetic_value_and_units(new_cube, cube_quantity / value) + +@pytest.mark.parametrize(("ndc", "value", "expected_kwargs"), + [( + "ndcube_2d_ln_lt_no_unit_no_unc_no_mask_2", + NDData(np.ones((2, 3)) + 1, wcs=None), + {"data": np.array([[0, 0.5, 1], [1.5, 2, 2.5]])}, + ), + ( + "ndcube_2d_ln_lt_no_unit_no_unc_no_mask_2", + NDData(np.ones((2, 3)) + 1, + wcs=None, + uncertainty=StdDevUncertainty((np.ones((2, 3)) + 1) * 0.1), + mask=np.array([[True, False, False], [False, True, False]]), + unit=u.ct), + {"data": np.array([[0, 0.5, 1], [1.5, 2, 2.5]]), + "uncertainty": astropy.nddata.StdDevUncertainty((np.ones((2, 3)) + 1) * 0.1), + "mask": np.array([[True, False, False], [False, True, False]]), + "unit": u.dimensionless_unscaled / u.ct} # ndc has no mask no uncertainty no unit, but nddata has all. + ), + ( + "ndcube_2d_ln_lt_unit_unc_mask", + NDData(np.ones((2, 3)) * 2, wcs=None), + {"data": np.array([[0, 0.5, 1], [1.5, 2, 2.5]]), + "uncertainty": astropy.nddata.StdDevUncertainty(np.array([[0, 0.05, 0.1], + [0.15, 0.2, 0.25]])), + "mask": np.array([[False, True, True], [False, True, True]])} + ), # ndc has mask, uncertainty and unit, but nddata doesn't. + ( + "ndcube_2d_ln_lt_unit_unc_mask", + NDData(np.ones((2, 3)) + 1, + wcs=None, + uncertainty=StdDevUncertainty((np.ones((2, 3)) + 1) * 0.1), + mask=np.array([[True, False, False], [False, True, False]]), + unit=u.ct), + {"unit": u.dimensionless_unscaled, + "data": np.array([[0, 0.5, 1], [1.5, 2, 2.5]]), + "uncertainty": astropy.nddata.StdDevUncertainty(np.array([[0. , 0.0559017, 0.1118034], + [0.1677051, 0.2236068, 0.2795085]])), + "mask": np.array([[True, True, True], [False, True, True]])} + ) # both of them have uncertainty and mask and unit. + ], + indirect=("ndc",)) +def test_cube_arithmetic_divide_nddata(ndc, value, expected_kwargs, wcs_2d_lt_ln): + output_cube = ndc / value # perform the division + + expected_kwargs["wcs"] = wcs_2d_lt_ln + expected_cube = NDCube(**expected_kwargs) + + # Assert output cube is same as expected cube + assert_cubes_equal(output_cube, expected_cube, check_uncertainty_values=True) + + @pytest.mark.parametrize('value', [1, 2, -1]) def test_cube_arithmetic_rdivide(ndcube_2d_ln_lt_units, value): cube_quantity = u.Quantity(ndcube_2d_ln_lt_units.data, ndcube_2d_ln_lt_units.unit) @@ -287,6 +339,7 @@ def test_cube_arithmetic_rdivide(ndcube_2d_ln_lt_units, value): new_cube = value / ndcube_2d_ln_lt_units check_arithmetic_value_and_units(new_cube, value / cube_quantity) + @pytest.mark.parametrize('value', [1, 2, -1]) def test_cube_arithmetic_rdivide_uncertainty(ndcube_4d_unit_uncertainty, value): cube_quantity = u.Quantity(ndcube_4d_unit_uncertainty.data, ndcube_4d_unit_uncertainty.unit) @@ -296,6 +349,7 @@ def test_cube_arithmetic_rdivide_uncertainty(ndcube_4d_unit_uncertainty, value): new_cube = value / ndcube_4d_unit_uncertainty check_arithmetic_value_and_units(new_cube, value / cube_quantity) + def test_cube_arithmetic_neg(ndcube_2d_ln_lt_units): check_arithmetic_value_and_units( -ndcube_2d_ln_lt_units, From 2627534391ddf3437be448b38a099015ae649989 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Fri, 15 Aug 2025 16:11:03 +0100 Subject: [PATCH 03/26] Add 880 changelog. --- changelog/880.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/880.feature.rst diff --git a/changelog/880.feature.rst b/changelog/880.feature.rst new file mode 100644 index 000000000..db5366374 --- /dev/null +++ b/changelog/880.feature.rst @@ -0,0 +1 @@ +Enable subtraction and division of `~ndcube.NDCube` by an `~astropy.nddata.NDData` instance, including uncertainty, mask and unit support. From 30e4330b41efe3ad40f3309300860e4bdb39e98c Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Thu, 2 Oct 2025 15:08:40 +0100 Subject: [PATCH 04/26] Add NDCube-NDData arithmetic tests for subtraction and division that preserve dask laziness. --- ndcube/tests/test_ndcube_arithmetic.py | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/ndcube/tests/test_ndcube_arithmetic.py b/ndcube/tests/test_ndcube_arithmetic.py index 54b5ad071..87ae341d4 100644 --- a/ndcube/tests/test_ndcube_arithmetic.py +++ b/ndcube/tests/test_ndcube_arithmetic.py @@ -171,6 +171,26 @@ def test_cube_arithmetic_subtract_nddata(ndc, value, expected_kwargs, wcs_2d_lt_ assert_cubes_equal(output_cube, expected_cube, check_uncertainty_values=True) +@pytest.mark.parametrize("value", + [ + NDData(np.ones((8, 4)), wcs=None, unit=u.J) + ]) +def test_cube_dask_arithmetic_subtract_nddata(ndcube_2d_dask, value): + ndc = ndcube_2d_dask + output_cube = ndc - value + assert type(output_cube.data) is type(ndc.data) + + +@pytest.mark.parametrize("value", + [ + NDData(np.ones((8, 4)), wcs=None, unit=u.J) + ]) +def test_cube_dask_arithmetic_subtract_nddata(ndcube_2d_dask, value): + ndc = ndcube_2d_dask + output_cube = ndc - value + assert type(output_cube.data) is type(ndc.data) + + @pytest.mark.parametrize('value', [ 10 * u.ct, u.Quantity([10], u.ct), @@ -332,6 +352,16 @@ def test_cube_arithmetic_divide_nddata(ndc, value, expected_kwargs, wcs_2d_lt_ln assert_cubes_equal(output_cube, expected_cube, check_uncertainty_values=True) +@pytest.mark.parametrize("value", + [ + NDData(np.ones((8, 4)) * 2, wcs=None) + ]) +def test_cube_dask_arithmetic_divide_nddata(ndcube_2d_dask, value): + ndc = ndcube_2d_dask + output_cube = ndc / value + assert type(output_cube.data) is type(ndc.data) + + @pytest.mark.parametrize('value', [1, 2, -1]) def test_cube_arithmetic_rdivide(ndcube_2d_ln_lt_units, value): cube_quantity = u.Quantity(ndcube_2d_ln_lt_units.data, ndcube_2d_ln_lt_units.unit) From f73e649cf58d52dcc98582be1dfd430af44e9d68 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Thu, 2 Oct 2025 15:26:35 +0100 Subject: [PATCH 05/26] Remove in-place modification in NDCube arithmetic subtraction. --- ndcube/ndcube.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ndcube/ndcube.py b/ndcube/ndcube.py index 0e533a5c9..3831a4264 100644 --- a/ndcube/ndcube.py +++ b/ndcube/ndcube.py @@ -1066,9 +1066,11 @@ def __radd__(self, value): def __sub__(self, value): if isinstance(value, NDData): - value.data[:] = -value.data - return self.__add__(value) - return self.__add__(-value) + new_value = copy.copy(value) + new_value._data = -value.data + else: + new_value = -value + return self.__add__(new_value) def __rsub__(self, value): return self.__neg__().__add__(value) From 5671bde43da90a8b2cdb840611a5867b0d17e56f Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Thu, 2 Oct 2025 15:29:11 +0100 Subject: [PATCH 06/26] Remove duplicate test. --- ndcube/tests/test_ndcube_arithmetic.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/ndcube/tests/test_ndcube_arithmetic.py b/ndcube/tests/test_ndcube_arithmetic.py index 87ae341d4..76518c46b 100644 --- a/ndcube/tests/test_ndcube_arithmetic.py +++ b/ndcube/tests/test_ndcube_arithmetic.py @@ -181,16 +181,6 @@ def test_cube_dask_arithmetic_subtract_nddata(ndcube_2d_dask, value): assert type(output_cube.data) is type(ndc.data) -@pytest.mark.parametrize("value", - [ - NDData(np.ones((8, 4)), wcs=None, unit=u.J) - ]) -def test_cube_dask_arithmetic_subtract_nddata(ndcube_2d_dask, value): - ndc = ndcube_2d_dask - output_cube = ndc - value - assert type(output_cube.data) is type(ndc.data) - - @pytest.mark.parametrize('value', [ 10 * u.ct, u.Quantity([10], u.ct), From 83c9f3d5643b0e3fc154bb632d2c44c6a7a9fb2a Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Thu, 2 Oct 2025 17:16:41 +0100 Subject: [PATCH 07/26] Fix bug whereby adding unitful NDCube and NDData did not output result with underlying arrays as dask. --- changelog/880.bugfix.rst | 1 + ndcube/ndcube.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog/880.bugfix.rst diff --git a/changelog/880.bugfix.rst b/changelog/880.bugfix.rst new file mode 100644 index 000000000..35aee69c7 --- /dev/null +++ b/changelog/880.bugfix.rst @@ -0,0 +1 @@ +Adding unitful `~ndcube.NDCube` and ``astropy.nddata.NDData`` objects backed by ``dask`` did not preserve underlying arrays as ``dask`` arrays. This is now fixed. diff --git a/ndcube/ndcube.py b/ndcube/ndcube.py index 3831a4264..3a86b2afb 100644 --- a/ndcube/ndcube.py +++ b/ndcube/ndcube.py @@ -983,7 +983,7 @@ def _arithmetic_operate_with_nddata(self, operation, value): if operation == "add": # Handle units if self.unit is not None and value.unit is not None: - value_data = (value.data * value.unit).to_value(self.unit) + value_data = value.data * (value.unit / self.unit).to(u.dimensionless_unscaled) elif self.unit is None and value.unit is None: value_data = value.data else: From 5acbd353ae0952170c565d166aea2d7dd2d84958 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Thu, 2 Oct 2025 17:17:36 +0100 Subject: [PATCH 08/26] Add more tests of dask-backed arithmetic operations. --- ndcube/conftest.py | 7 +++ ndcube/tests/test_ndcube_arithmetic.py | 75 ++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/ndcube/conftest.py b/ndcube/conftest.py index e3ee125c4..8788c7d51 100644 --- a/ndcube/conftest.py +++ b/ndcube/conftest.py @@ -832,6 +832,13 @@ def ndcube_2d_dask(wcs_2d_lt_ln): return NDCube(da, wcs=wcs_2d_lt_ln, uncertainty=da_uncert, mask=da_mask, unit=u.J) +@pytest.fixture +def nddata_2d_dask(ndcube_2d_dask): + value = astropy.nddata.NDData(ndcube_2d_dask) + value._wcs = None + return value + + @pytest.fixture def ndcube_2d(request): """ diff --git a/ndcube/tests/test_ndcube_arithmetic.py b/ndcube/tests/test_ndcube_arithmetic.py index 76518c46b..068e1593e 100644 --- a/ndcube/tests/test_ndcube_arithmetic.py +++ b/ndcube/tests/test_ndcube_arithmetic.py @@ -3,6 +3,7 @@ import astropy.units as u import astropy.wcs +import dask.array from astropy.nddata import NDData, StdDevUncertainty from ndcube import NDCube @@ -171,14 +172,22 @@ def test_cube_arithmetic_subtract_nddata(ndc, value, expected_kwargs, wcs_2d_lt_ assert_cubes_equal(output_cube, expected_cube, check_uncertainty_values=True) -@pytest.mark.parametrize("value", - [ - NDData(np.ones((8, 4)), wcs=None, unit=u.J) - ]) -def test_cube_dask_arithmetic_subtract_nddata(ndcube_2d_dask, value): +def test_cube_dask_arithmetic_subtract_nddata(ndcube_2d_dask): ndc = ndcube_2d_dask + value = NDData(np.ones(ndc.data.shape), wcs=None, unit=ndc.unit) output_cube = ndc - value assert type(output_cube.data) is type(ndc.data) + assert type(output_cube.uncertainty.array) is type(ndc.uncertainty.array) + assert type(output_cube.mask) is type(ndc.mask) + + +def test_cube_arithmetic_subtract_nddata_dask(wcs_2d_lt_ln, nddata_2d_dask): + value = nddata_2d_dask + ndc = NDCube(np.ones(value.data.shape), wcs=wcs_2d_lt_ln, unit=value.unit) + output_cube = ndc - value + assert type(output_cube.data) is type(value.data) + assert type(output_cube.uncertainty.array) is type(value.uncertainty.array) + assert type(output_cube.mask) is type(value.mask) @pytest.mark.parametrize('value', [ @@ -193,6 +202,24 @@ def test_cube_arithmetic_rsubtract(ndcube_2d_ln_lt_units, value): check_arithmetic_value_and_units(new_cube, value - cube_quantity) +def test_cube_dask_arithmetic_rsubtract_nddata(ndcube_2d_dask): + ndc = ndcube_2d_dask + value = NDData(np.ones(ndc.data.shape), wcs=None, unit=ndc.unit) + output_cube = value - ndc + assert type(output_cube.data) is type(ndc.data) + assert type(output_cube.uncertainty.array) is type(ndc.uncertainty.array) + assert type(output_cube.mask) is type(ndc.mask) + + +def test_cube_arithmetic_rsubtract_nddata_dask(wcs_2d_lt_ln, nddata_2d_dask): + value = nddata_2d_dask + ndc = NDCube(np.ones(value.data.shape), wcs=wcs_2d_lt_ln, unit=value.unit) + output_cube = value - ndc + assert type(output_cube.data) is type(value.data) + assert type(output_cube.uncertainty.array) is type(value.uncertainty.array) + assert type(output_cube.mask) is type(value.mask) + + @pytest.mark.parametrize('value', [ 10 * u.ct, u.Quantity([10], u.ct), @@ -352,6 +379,24 @@ def test_cube_dask_arithmetic_divide_nddata(ndcube_2d_dask, value): assert type(output_cube.data) is type(ndc.data) +def test_cube_dask_arithmetic_divide_nddata(ndcube_2d_dask): + ndc = ndcube_2d_dask + value = NDData(np.ones(ndc.data.shape), wcs=None, unit=ndc.unit) + output_cube = ndc / value + assert type(output_cube.data) is type(ndc.data) + assert type(output_cube.uncertainty.array) is type(ndc.uncertainty.array) + assert type(output_cube.mask) is type(ndc.mask) + + +def test_cube_arithmetic_divide_nddata_dask(wcs_2d_lt_ln, nddata_2d_dask): + value = nddata_2d_dask + ndc = NDCube(np.ones(value.data.shape), wcs=wcs_2d_lt_ln, unit=value.unit) + output_cube = ndc / value + assert type(output_cube.data) is type(value.data) + assert type(output_cube.uncertainty.array) is type(value.uncertainty.array) + assert type(output_cube.mask) is type(value.mask) + + @pytest.mark.parametrize('value', [1, 2, -1]) def test_cube_arithmetic_rdivide(ndcube_2d_ln_lt_units, value): cube_quantity = u.Quantity(ndcube_2d_ln_lt_units.data, ndcube_2d_ln_lt_units.unit) @@ -370,6 +415,26 @@ def test_cube_arithmetic_rdivide_uncertainty(ndcube_4d_unit_uncertainty, value): check_arithmetic_value_and_units(new_cube, value / cube_quantity) +def test_cube_dask_arithmetic_rdivide_nddata(ndcube_2d_dask): + ndc = ndcube_2d_dask + value = NDData(np.ones(ndc.data.shape), wcs=None, unit=ndc.unit) + match = "does not support propagation of uncertainties for power. Setting uncertainties to None." + with pytest.warns(NDCubeUserWarning, match=match): # noqa: PT031 + with np.errstate(divide='ignore'): + output_cube = value / ndc + assert type(output_cube.data) is type(ndc.data) + assert type(output_cube.mask) is type(ndc.mask) + + +def test_cube_arithmetic_rdivide_nddata_dask(wcs_2d_lt_ln, nddata_2d_dask): + value = nddata_2d_dask + ndc = NDCube(np.ones(value.data.shape), wcs=wcs_2d_lt_ln, unit=value.unit) + output_cube = value / ndc + assert type(output_cube.data) is type(value.data) + assert type(output_cube.uncertainty.array) is type(value.uncertainty.array) + assert type(output_cube.mask) is type(value.mask) + + def test_cube_arithmetic_neg(ndcube_2d_ln_lt_units): check_arithmetic_value_and_units( -ndcube_2d_ln_lt_units, From 4baa3b05b467f6e32e846a8c9013c7c49b85cbd2 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Thu, 2 Oct 2025 17:20:00 +0100 Subject: [PATCH 09/26] Remove duplicate test. --- ndcube/tests/test_ndcube_arithmetic.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/ndcube/tests/test_ndcube_arithmetic.py b/ndcube/tests/test_ndcube_arithmetic.py index 068e1593e..e50bf3606 100644 --- a/ndcube/tests/test_ndcube_arithmetic.py +++ b/ndcube/tests/test_ndcube_arithmetic.py @@ -3,7 +3,6 @@ import astropy.units as u import astropy.wcs -import dask.array from astropy.nddata import NDData, StdDevUncertainty from ndcube import NDCube @@ -369,16 +368,6 @@ def test_cube_arithmetic_divide_nddata(ndc, value, expected_kwargs, wcs_2d_lt_ln assert_cubes_equal(output_cube, expected_cube, check_uncertainty_values=True) -@pytest.mark.parametrize("value", - [ - NDData(np.ones((8, 4)) * 2, wcs=None) - ]) -def test_cube_dask_arithmetic_divide_nddata(ndcube_2d_dask, value): - ndc = ndcube_2d_dask - output_cube = ndc / value - assert type(output_cube.data) is type(ndc.data) - - def test_cube_dask_arithmetic_divide_nddata(ndcube_2d_dask): ndc = ndcube_2d_dask value = NDData(np.ones(ndc.data.shape), wcs=None, unit=ndc.unit) From 1bcd97f4bb78c59c010b7955fbe0a977d286e060 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Fri, 3 Oct 2025 00:28:33 +0100 Subject: [PATCH 10/26] First commit of docs explaining arithmetic operations. --- .../arithmetic_operations.rst | 136 ++++++++++++++++++ docs/explaining_ndcube/index.rst | 1 + 2 files changed, 137 insertions(+) create mode 100644 docs/explaining_ndcube/arithmetic_operations.rst diff --git a/docs/explaining_ndcube/arithmetic_operations.rst b/docs/explaining_ndcube/arithmetic_operations.rst new file mode 100644 index 000000000..ef64c7c15 --- /dev/null +++ b/docs/explaining_ndcube/arithmetic_operations.rst @@ -0,0 +1,136 @@ +.. _arithmetic: + +********************* +Arithmetic Operations +********************* + +Arithmetic operations are a crucial tool in n-dimensional data analysis. +Applications include subtracting a background from a 1-D timeseries or spectrum, scaling an image by a vignetting function, any many others. +To aid with such workflows, `~ndcube.NDCube` supports addition, subtraction, multiplication, and division with scalars, arrays, `~astropy.units.Quantity`. +Raising an `~ndcube.NDCube` to a power is also supported. +These operations return a new `~ndcube.NDCube` with the data array (and where appropriate, the uncertainties) altered in accordance with the arithmetic operation. +Other attributes of the `~ndcube.NDCube` remain unchanged. + +In addition, combining `~ndcube.NDCube` with coordinate-less `~astropy.nddata.NDData` subclasses via these operations is important. +Such operations can be more complicated. Hence see the :ref:`arithmetic_nddata` section below for a discussion separate, more detailed discussion. + +.. _arithmetic_standard: + +Standard Arithmetic Operations +============================== + +Addition and Subtraction with Scalars, Arrays and Quantities +------------------------------------------------------------ + +Let's demonstrate how we can add and subtract scalars, arrays and `~astropy.units.Quantity` to/from an `~ndcube.NDCube` called ``cube``. +Note that addition and subtraction only changes the data values of the `~ndcube.NDCube`. + +.. expanding-code-block:: python + :summary: Expand to see my_cube instantiated. + + >>> import astropy.units as u + >>> import astropy.wcs + >>> import numpy as np + >>> from astropy.nddata import StdDevUncertainty + + >>> from ndcube import NDCube + + >>> # Define data array. + >>> data = np.arange(2*3).reshape((2, 3)) + 10 + + >>> # Define WCS transformations in an astropy WCS object. + >>> wcs = astropy.wcs.WCS(naxis=2) + >>> wcs.wcs.ctype = 'HPLT-TAN', 'HPLN-TAN' + >>> wcs.wcs.cunit = 'deg', 'deg' + >>> wcs.wcs.cdelt = 0.5, 0.4 + >>> wcs.wcs.crpix = 2, 2 + >>> wcs.wcs.crval = 0.5, 1 + + >>> # Define mask. Initially set all elements unmasked. + >>> mask = np.zeros_like(data, dtype=bool) + >>> mask[0, :] = True # Now mask some values. + >>> # Define uncertainty, metadata and unit. + >>> uncertainty = StdDevUncertainty(np.sqrt(np.abs(data))) + >>> meta = {"Description": "This is example NDCube metadata."} + >>> unit = u.ct + + >>> # Instantiate NDCube with supporting data. + >>> cube = NDCube(data, wcs=wcs, uncertainty=uncertainty, mask=mask, meta=meta) + +.. code-block:: python + + >>> cube.data + array([[10, 11, 12], + [13, 14, 15]]) + >>> new_cube = cube + 1 + >>> new_cube.data + array([[11, 12, 13], + [14, 15, 16]]) + +Note that all the data values have been increased by 1. +We can also add an array if we want to add a different number to each data element: + +.. code-block:: python + + >>> import numpy as np + >>> arr = np.arange(cube.data.size).reshape(cube.data.shape) + >>> arr + array([[0, 1, 2], + [3, 4, 5]]) + >>> new_cube = cube + arr + >>> new_cube.data + array([[10, 12, 14], + [16, 18, 20]]) + +Subtraction works in the same way. + +.. code-block:: python + + >>> new_cube = cube - 1 + >>> new_cube.data + array([[ 9, 10, 11], + [12, 13, 14]]) + >>> new_cube = cube - arr + >>> new_cube.data + array([[10, 10, 10], + [10, 10, 10]]) + +Note that ``cube`` has no unit, which is why we are able to add and subtract scalars and arrays. +If, however, we have an `~ndcube.NDCube` with a unit assigned, + +.. code-block:: python + + >>> cube_unitful = NDCube(cube, unit=u.ct) + +then adding or subtracting an array or unitless scalar will raise an error. +In such cases, we must use a `~astropy.unit.Quantity` with a compatible unit: + +.. code-block:: python + + >>> cube.data + array([[10, 11, 12], + [13, 14, 15]]) + >>> new_cube = cube_unitful + 1 * u.ct # Adding a scalar quantity + >>> new_cube.data + array([[11, 12, 13], + [14, 15, 16]]) + >>> new_cube = cube_unitful - 1 * u.ct # Subtracting a scalar quantity + >>> new_cube.data + array([[ 9, 10, 11], + [12, 13, 14]]) + >>> new_cube = cube_unitful + arr * u.ct # Adding an array-like quantity + >>> new_cube.data + array([[10, 12, 14], + [16, 18, 20]]) + >>> new_cube = cube_unitful - arr * u.ct # Subtracting an array-like quantity + >>> new_cube.data + array([[10, 10, 10], + [10, 10, 10]]) + +Multiplying and Dividing with Scalars, Arrays and Quantities +------------------------------------------------------------ + +.. _arithmetic_nddata: + +Arithmetic Operations with Coordinate-less NDData +================================================= diff --git a/docs/explaining_ndcube/index.rst b/docs/explaining_ndcube/index.rst index a28801d00..f4176a72d 100644 --- a/docs/explaining_ndcube/index.rst +++ b/docs/explaining_ndcube/index.rst @@ -14,3 +14,4 @@ Explaining ``ndcube`` tabular_coordinates reproject visualization + arithmetic_operations From f3f07b50aaf54c1a0d9e32fb7e437cb42d11b96e Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Fri, 3 Oct 2025 01:21:31 +0100 Subject: [PATCH 11/26] Next commit on docs explaining arithmetic operations. --- .../arithmetic_operations.rst | 109 +++++++++++++----- 1 file changed, 81 insertions(+), 28 deletions(-) diff --git a/docs/explaining_ndcube/arithmetic_operations.rst b/docs/explaining_ndcube/arithmetic_operations.rst index ef64c7c15..c03ab42eb 100644 --- a/docs/explaining_ndcube/arithmetic_operations.rst +++ b/docs/explaining_ndcube/arithmetic_operations.rst @@ -6,7 +6,7 @@ Arithmetic Operations Arithmetic operations are a crucial tool in n-dimensional data analysis. Applications include subtracting a background from a 1-D timeseries or spectrum, scaling an image by a vignetting function, any many others. -To aid with such workflows, `~ndcube.NDCube` supports addition, subtraction, multiplication, and division with scalars, arrays, `~astropy.units.Quantity`. +To aid with such workflows, `~ndcube.NDCube` supports addition, subtraction, multiplication, and division with numbers, arrays, `~astropy.units.Quantity`. Raising an `~ndcube.NDCube` to a power is also supported. These operations return a new `~ndcube.NDCube` with the data array (and where appropriate, the uncertainties) altered in accordance with the arithmetic operation. Other attributes of the `~ndcube.NDCube` remain unchanged. @@ -19,11 +19,12 @@ Such operations can be more complicated. Hence see the :ref:`arithmetic_nddata` Standard Arithmetic Operations ============================== -Addition and Subtraction with Scalars, Arrays and Quantities +Addition and Subtraction with Numbers, Arrays and Quantities ------------------------------------------------------------ -Let's demonstrate how we can add and subtract scalars, arrays and `~astropy.units.Quantity` to/from an `~ndcube.NDCube` called ``cube``. +Numbers, arrays and `~astropy.units.Quantity` can be added to and subtracted from an `~ndcube.NDCube` via the ``+`` and ``-`` operators. Note that addition and subtraction only changes the data values of the `~ndcube.NDCube`. +Let's deomonstrate with an example `~ndcube.NDCube` called ``cube`` .. expanding-code-block:: python :summary: Expand to see my_cube instantiated. @@ -50,7 +51,7 @@ Note that addition and subtraction only changes the data values of the `~ndcube. >>> mask = np.zeros_like(data, dtype=bool) >>> mask[0, :] = True # Now mask some values. >>> # Define uncertainty, metadata and unit. - >>> uncertainty = StdDevUncertainty(np.sqrt(np.abs(data))) + >>> uncertainty = StdDevUncertainty(np.abs(data) * 0.1) >>> meta = {"Description": "This is example NDCube metadata."} >>> unit = u.ct @@ -60,12 +61,12 @@ Note that addition and subtraction only changes the data values of the `~ndcube. .. code-block:: python >>> cube.data - array([[10, 11, 12], - [13, 14, 15]]) + array([[10., 11., 12.], + [13., 14., 15.]]) >>> new_cube = cube + 1 >>> new_cube.data - array([[11, 12, 13], - [14, 15, 16]]) + array([[11., 12., 13.], + [14., 15., 16.]]) Note that all the data values have been increased by 1. We can also add an array if we want to add a different number to each data element: @@ -79,8 +80,8 @@ We can also add an array if we want to add a different number to each data eleme [3, 4, 5]]) >>> new_cube = cube + arr >>> new_cube.data - array([[10, 12, 14], - [16, 18, 20]]) + array([[10., 12., 14.], + [16., 18., 20.]]) Subtraction works in the same way. @@ -88,48 +89,100 @@ Subtraction works in the same way. >>> new_cube = cube - 1 >>> new_cube.data - array([[ 9, 10, 11], - [12, 13, 14]]) + array([[ 9., 10., 11.], + [12., 13., 14.]]) >>> new_cube = cube - arr >>> new_cube.data - array([[10, 10, 10], - [10, 10, 10]]) + array([[10., 10., 10.], + [10., 10., 10.]]) -Note that ``cube`` has no unit, which is why we are able to add and subtract scalars and arrays. +Note that ``cube`` has no unit, which is why we are able to add and subtract numbers and arrays. If, however, we have an `~ndcube.NDCube` with a unit assigned, .. code-block:: python >>> cube_unitful = NDCube(cube, unit=u.ct) -then adding or subtracting an array or unitless scalar will raise an error. +then adding or subtracting an array or unitless number will raise an error. In such cases, we must use a `~astropy.unit.Quantity` with a compatible unit: .. code-block:: python - >>> cube.data - array([[10, 11, 12], - [13, 14, 15]]) + >>> cube_unitful.data + array([[10., 11., 12.], + [13., 14., 15.]]) >>> new_cube = cube_unitful + 1 * u.ct # Adding a scalar quantity >>> new_cube.data - array([[11, 12, 13], - [14, 15, 16]]) + array([[11., 12., 13.], + [14., 15., 16.]]) >>> new_cube = cube_unitful - 1 * u.ct # Subtracting a scalar quantity >>> new_cube.data - array([[ 9, 10, 11], - [12, 13, 14]]) + array([[ 9., 10., 11.], + [12., 13., 14.]]) >>> new_cube = cube_unitful + arr * u.ct # Adding an array-like quantity >>> new_cube.data - array([[10, 12, 14], - [16, 18, 20]]) + array([[10., 12., 14.], + [16., 18., 20.]]) >>> new_cube = cube_unitful - arr * u.ct # Subtracting an array-like quantity >>> new_cube.data - array([[10, 10, 10], - [10, 10, 10]]) + array([[10., 10., 10.], + [10., 10., 10.]]) -Multiplying and Dividing with Scalars, Arrays and Quantities +Multiplying and Dividing with Numbers, Arrays and Quantities ------------------------------------------------------------ +An `~ndcube.NDCube` can be multiplied and divided by numbers, arrays, and `~astropy.units.Quantity` via the ``*`` and ``-`` operators. +These work similarly to addition and subtraction with a few minor differences: +- The uncertainties of the resulting `~ndcube.NDCube` are scaled by the same factor as the data. +- Classes with different units can be combined. + - e.g. an `~ndcube.NDCube` with a unit of counts divided by an `~astropy.units.Quantity` with a unit is seconds will result in an `~ndcube.NDCube` with a unit of counts per second. + - This also holds for cases were unitful and unitless classes can be combined. In such cases, the unit of the resulting `~ndcube.NDCube` will be the same as that of the unitful object. + +Below are some examples. + +.. code-block:: python + + >>> # See attributes of original cube. + >>> cube_unitful.data + array([[10., 11., 12.], + [13., 14., 15.]]) + >>> cube_unitful.unit + Unit("ct") + >>> cube_unitful.uncertainty + StdDevUncertainty([[1. , 1.1, 1.2], + [1.3, 1.4, 1.5]]) + + >>> # Multiply by a unitless array. + >>> arr = 1 + np.arange(cube_unitful.data.size).reshape(cube_unitful.data.shape) + >>> arr + array([[1, 2, 3], + [4, 5, 6]]) + >>> new_cube = cube_unitful * arr + + >>> # Inspect attributes of resultant cube. + >>> new_cube.data + array([[10., 22., 36.], + [52., 70., 90.]]) + >>> new_cube.unit + Unit("ct") + >>> new_cube.uncertainty + StdDevUncertainty([[1. , 2.2, 3.6], + [5.2, 7. , 9. ]]) + + >>> # Divide by an astropy Quantity. + >>> new_cube = cube_unitful / (2 * u.s) + + >>> # Inspect attributes of resultant cube. + >>> new_cube.data + array([[5. , 5.5, 6. ], + [6.5, 7. , 7.5]]) + >>> new_cube.unit + Unit("ct / s") + >>> new_cube.uncertainty + StdDevUncertainty([[0.5 , 0.55, 0.6 ], + [0.65, 0.7 , 0.75]]) + + .. _arithmetic_nddata: Arithmetic Operations with Coordinate-less NDData From f2536507e8d2b954c085b835386ec4f03ef58e86 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Fri, 3 Oct 2025 02:17:57 +0100 Subject: [PATCH 12/26] Rename arithmetic operations docs file and next commit in arithmetic docs. --- ...ithmetic_operations.rst => arithmetic.rst} | 86 +++++++++++++++---- docs/explaining_ndcube/index.rst | 2 +- 2 files changed, 68 insertions(+), 20 deletions(-) rename docs/explaining_ndcube/{arithmetic_operations.rst => arithmetic.rst} (61%) diff --git a/docs/explaining_ndcube/arithmetic_operations.rst b/docs/explaining_ndcube/arithmetic.rst similarity index 61% rename from docs/explaining_ndcube/arithmetic_operations.rst rename to docs/explaining_ndcube/arithmetic.rst index c03ab42eb..c4f7466d1 100644 --- a/docs/explaining_ndcube/arithmetic_operations.rst +++ b/docs/explaining_ndcube/arithmetic.rst @@ -61,12 +61,12 @@ Let's deomonstrate with an example `~ndcube.NDCube` called ``cube`` .. code-block:: python >>> cube.data - array([[10., 11., 12.], - [13., 14., 15.]]) + array([[10, 11, 12], + [13, 14, 15]]) >>> new_cube = cube + 1 >>> new_cube.data - array([[11., 12., 13.], - [14., 15., 16.]]) + array([[11, 12, 13], + [14, 15, 16]]) Note that all the data values have been increased by 1. We can also add an array if we want to add a different number to each data element: @@ -80,8 +80,8 @@ We can also add an array if we want to add a different number to each data eleme [3, 4, 5]]) >>> new_cube = cube + arr >>> new_cube.data - array([[10., 12., 14.], - [16., 18., 20.]]) + array([[10, 12, 14], + [16, 18, 20]]) Subtraction works in the same way. @@ -89,12 +89,12 @@ Subtraction works in the same way. >>> new_cube = cube - 1 >>> new_cube.data - array([[ 9., 10., 11.], - [12., 13., 14.]]) + array([[ 9, 10, 11], + [12, 13, 14]]) >>> new_cube = cube - arr >>> new_cube.data - array([[10., 10., 10.], - [10., 10., 10.]]) + array([[10, 10, 10], + [10, 10, 10]]) Note that ``cube`` has no unit, which is why we are able to add and subtract numbers and arrays. If, however, we have an `~ndcube.NDCube` with a unit assigned, @@ -109,8 +109,8 @@ In such cases, we must use a `~astropy.unit.Quantity` with a compatible unit: .. code-block:: python >>> cube_unitful.data - array([[10., 11., 12.], - [13., 14., 15.]]) + array([[10, 11, 12], + [13, 14, 15]]) >>> new_cube = cube_unitful + 1 * u.ct # Adding a scalar quantity >>> new_cube.data array([[11., 12., 13.], @@ -144,13 +144,13 @@ Below are some examples. >>> # See attributes of original cube. >>> cube_unitful.data - array([[10., 11., 12.], - [13., 14., 15.]]) + array([[10, 11, 12], + [13, 14, 15]]) >>> cube_unitful.unit Unit("ct") >>> cube_unitful.uncertainty StdDevUncertainty([[1. , 1.1, 1.2], - [1.3, 1.4, 1.5]]) + [1.3, 1.4, 1.5]]) >>> # Multiply by a unitless array. >>> arr = 1 + np.arange(cube_unitful.data.size).reshape(cube_unitful.data.shape) @@ -161,8 +161,8 @@ Below are some examples. >>> # Inspect attributes of resultant cube. >>> new_cube.data - array([[10., 22., 36.], - [52., 70., 90.]]) + array([[10, 22, 36], + [52, 70, 90]]) >>> new_cube.unit Unit("ct") >>> new_cube.uncertainty @@ -185,5 +185,53 @@ Below are some examples. .. _arithmetic_nddata: -Arithmetic Operations with Coordinate-less NDData -================================================= +Arithmetic Operations between Coordinate-less NDData +==================================================== + +Sometimes more advanced arithmetic operations are required. +For example, we may want to create a sequence of running difference images which highlight changes between frames, and propagate the uncertainties associated with each image. +Alternatively, we may want to subtract one image from another, but exclude a certain region of the image with a mask. +In such cases, numbers, arrays and `~astropy.units.Quantity` are insufficient, and we would like to subtract two `~ndcube.NDCube` objects. +This is not directly supported, but can still be achieved in practice, as we shall see below. + +Why Arithmetic Operations with Coordinate-aware NDData Instances Are Not Directly Supported, and How the Same Result Can Be Achieved +------------------------------------------------------------------------------------------------------------------------------------ + +Arithmetic operations between two `~ndcube.NDCube` instances (or equivalently, an `~ndcube.NDCube` and another coordinate-aware object) are not supported because of the possibility of supporting non-sensical operations. +For example, what does it mean to multiply a spectrum and an image in a coordinate-aware way? +Getting the difference between two images may make physical sense, but only in certain circumstances. +For example, subtracting two sequential images of the same region of the Sun is a common step in many solar image analysis workflows. +However, subtracting images of different parts of the sky, e.g. the Sun and the Crab Nebula, does not result in a physically meaningful image. +Even when subtracting two images of the Sun, drift in the telescope's pointing may result in the pixels in each image corresponding to different points in the Sun. +In this case, it is questionable whether this operation makes physical sense after all. +Moreover, in all of these cases, it is not at all clear what the resulting WCS object should be. + +In many cases, a simple solution would be to extract the data (an optionally the unit) of one of the `~ndcube.NDCube` instances and perform the operation as described in the above section on :ref:`arithmetic_standard`: + +.. expanding-code-block:: python + :summary: Expand to see definition of cube1 and cube2. + + >>> cube1 = cube_unitful + >>> cube2 = cube_unitful / 4 + +.. code-block:: python + + >>> new_cube = cube1 - cube2.data * cube2.unit + +However, this does not allow for the propagation of uncertainties or masks associated with the data in ``cube2``. +Therefore, `~ndcube.NDCube` does support arithmetic operations with instances of `~astropy.nddata.NDData` subclasses whose ``wcs`` attribute is ``None``. +This makes users explicitly aware that they are dispensing with coordinate-awareness on one of their operands. +It also leaves only one WCS involved in the operation, thus removing ambiguity regarding the WCS of the `~ndcube.NDCube` resulting from the operation. + +Users who would like to drop coordinate-awareness from an `~ndcube.NDCube` can so so simply by converting it to an `~astropy.nddata.NDData` and setting the ``wcs`` to ``None``: + +.. code-block:: python + + >>> from astropy.nddata import NDData + + >>> cube2_nocoords = NDData(cube2, wcs=None) + + +Performing Arithmetic Operations with Coordinate-less NDData +------------------------------------------------------------ + diff --git a/docs/explaining_ndcube/index.rst b/docs/explaining_ndcube/index.rst index f4176a72d..0e22d0753 100644 --- a/docs/explaining_ndcube/index.rst +++ b/docs/explaining_ndcube/index.rst @@ -14,4 +14,4 @@ Explaining ``ndcube`` tabular_coordinates reproject visualization - arithmetic_operations + arithmetic From e09b89231a04781044a5523a1d5f827fd848b7c1 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Fri, 3 Oct 2025 02:25:25 +0100 Subject: [PATCH 13/26] Fix codestyle. --- docs/explaining_ndcube/arithmetic.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/explaining_ndcube/arithmetic.rst b/docs/explaining_ndcube/arithmetic.rst index c4f7466d1..b2a0f7af9 100644 --- a/docs/explaining_ndcube/arithmetic.rst +++ b/docs/explaining_ndcube/arithmetic.rst @@ -217,21 +217,20 @@ In many cases, a simple solution would be to extract the data (an optionally the .. code-block:: python >>> new_cube = cube1 - cube2.data * cube2.unit - + However, this does not allow for the propagation of uncertainties or masks associated with the data in ``cube2``. Therefore, `~ndcube.NDCube` does support arithmetic operations with instances of `~astropy.nddata.NDData` subclasses whose ``wcs`` attribute is ``None``. This makes users explicitly aware that they are dispensing with coordinate-awareness on one of their operands. It also leaves only one WCS involved in the operation, thus removing ambiguity regarding the WCS of the `~ndcube.NDCube` resulting from the operation. -Users who would like to drop coordinate-awareness from an `~ndcube.NDCube` can so so simply by converting it to an `~astropy.nddata.NDData` and setting the ``wcs`` to ``None``: +Users who would like to drop coordinate-awareness from an `~ndcube.NDCube` can so simply by converting it to an `~astropy.nddata.NDData` and setting the ``wcs`` to ``None``: .. code-block:: python >>> from astropy.nddata import NDData - + >>> cube2_nocoords = NDData(cube2, wcs=None) - + Performing Arithmetic Operations with Coordinate-less NDData ------------------------------------------------------------ - From 8ca5c5bb744414612ad2291a69daf2bece2a8cea Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Fri, 3 Oct 2025 02:38:12 +0100 Subject: [PATCH 14/26] Fix bugs in arithmetic docs. --- docs/explaining_ndcube/arithmetic.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/explaining_ndcube/arithmetic.rst b/docs/explaining_ndcube/arithmetic.rst index b2a0f7af9..2117e81ad 100644 --- a/docs/explaining_ndcube/arithmetic.rst +++ b/docs/explaining_ndcube/arithmetic.rst @@ -104,7 +104,7 @@ If, however, we have an `~ndcube.NDCube` with a unit assigned, >>> cube_unitful = NDCube(cube, unit=u.ct) then adding or subtracting an array or unitless number will raise an error. -In such cases, we must use a `~astropy.unit.Quantity` with a compatible unit: +In such cases, we must use a `~astropy.units.Quantity` with a compatible unit: .. code-block:: python @@ -135,8 +135,8 @@ An `~ndcube.NDCube` can be multiplied and divided by numbers, arrays, and `~astr These work similarly to addition and subtraction with a few minor differences: - The uncertainties of the resulting `~ndcube.NDCube` are scaled by the same factor as the data. - Classes with different units can be combined. - - e.g. an `~ndcube.NDCube` with a unit of counts divided by an `~astropy.units.Quantity` with a unit is seconds will result in an `~ndcube.NDCube` with a unit of counts per second. - - This also holds for cases were unitful and unitless classes can be combined. In such cases, the unit of the resulting `~ndcube.NDCube` will be the same as that of the unitful object. + * e.g. an `~ndcube.NDCube` with a unit of counts divided by an `~astropy.units.Quantity` with a unit is seconds will result in an `~ndcube.NDCube` with a unit of counts per second. + * This also holds for cases were unitful and unitless classes can be combined. In such cases, the unit of the resulting `~ndcube.NDCube` will be the same as that of the unitful object. Below are some examples. From 8eff1b541ba1486563367c16f77b6a5f32923f07 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Fri, 3 Oct 2025 02:55:49 +0100 Subject: [PATCH 15/26] Next commit on arithmetic docs. --- docs/explaining_ndcube/arithmetic.rst | 71 +++++++++++++++++---------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/docs/explaining_ndcube/arithmetic.rst b/docs/explaining_ndcube/arithmetic.rst index 2117e81ad..889cbdaf9 100644 --- a/docs/explaining_ndcube/arithmetic.rst +++ b/docs/explaining_ndcube/arithmetic.rst @@ -5,14 +5,14 @@ Arithmetic Operations ********************* Arithmetic operations are a crucial tool in n-dimensional data analysis. -Applications include subtracting a background from a 1-D timeseries or spectrum, scaling an image by a vignetting function, any many others. +Applications include subtracting a background from a 1-D timeseries or spectrum, scaling an image by a vignetting function, and many others. To aid with such workflows, `~ndcube.NDCube` supports addition, subtraction, multiplication, and division with numbers, arrays, `~astropy.units.Quantity`. Raising an `~ndcube.NDCube` to a power is also supported. -These operations return a new `~ndcube.NDCube` with the data array (and where appropriate, the uncertainties) altered in accordance with the arithmetic operation. +These operations return a new `~ndcube.NDCube` with the data array (and, where appropriate, the uncertainties) altered in accordance with the arithmetic operation. Other attributes of the `~ndcube.NDCube` remain unchanged. -In addition, combining `~ndcube.NDCube` with coordinate-less `~astropy.nddata.NDData` subclasses via these operations is important. -Such operations can be more complicated. Hence see the :ref:`arithmetic_nddata` section below for a discussion separate, more detailed discussion. +In addition, combining `~ndcube.NDCube` with coordinate-less `~astropy.nddata.NDData` subclasses via these operations is also supported. +Such operations can be more complicated. See the section below on :ref:`arithmetic_nddata` for a discussion separate, more detailed discussion. .. _arithmetic_standard: @@ -22,12 +22,12 @@ Standard Arithmetic Operations Addition and Subtraction with Numbers, Arrays and Quantities ------------------------------------------------------------ -Numbers, arrays and `~astropy.units.Quantity` can be added to and subtracted from an `~ndcube.NDCube` via the ``+`` and ``-`` operators. -Note that addition and subtraction only changes the data values of the `~ndcube.NDCube`. -Let's deomonstrate with an example `~ndcube.NDCube` called ``cube`` +Numbers, arrays and `~astropy.units.Quantity` can be added to and subtracted from `~ndcube.NDCube` via the ``+`` and ``-`` operators. +Note that addition and subtraction only change the data values of the `~ndcube.NDCube`. +Let's deomonstrate with an example `~ndcube.NDCube`, ``cube`` .. expanding-code-block:: python - :summary: Expand to see my_cube instantiated. + :summary: Expand to see cube instantiated. >>> import astropy.units as u >>> import astropy.wcs @@ -63,6 +63,7 @@ Let's deomonstrate with an example `~ndcube.NDCube` called ``cube`` >>> cube.data array([[10, 11, 12], [13, 14, 15]]) + >>> new_cube = cube + 1 >>> new_cube.data array([[11, 12, 13], @@ -78,6 +79,7 @@ We can also add an array if we want to add a different number to each data eleme >>> arr array([[0, 1, 2], [3, 4, 5]]) + >>> new_cube = cube + arr >>> new_cube.data array([[10, 12, 14], @@ -91,39 +93,45 @@ Subtraction works in the same way. >>> new_cube.data array([[ 9, 10, 11], [12, 13, 14]]) + >>> new_cube = cube - arr >>> new_cube.data array([[10, 10, 10], [10, 10, 10]]) -Note that ``cube`` has no unit, which is why we are able to add and subtract numbers and arrays. +Note that int he above examples, ``cube`` has no unit. +This is why we are able to add and subtract numbers and arrays. If, however, we have an `~ndcube.NDCube` with a unit assigned, .. code-block:: python - >>> cube_unitful = NDCube(cube, unit=u.ct) + >>> cube_with_unit = NDCube(cube, unit=u.ct) then adding or subtracting an array or unitless number will raise an error. In such cases, we must use a `~astropy.units.Quantity` with a compatible unit: .. code-block:: python - >>> cube_unitful.data + >>> cube_with_unit.data array([[10, 11, 12], [13, 14, 15]]) - >>> new_cube = cube_unitful + 1 * u.ct # Adding a scalar quantity + + >>> new_cube = cube_with_unit + 1 * u.ct # Adding a scalar quantity >>> new_cube.data array([[11., 12., 13.], [14., 15., 16.]]) - >>> new_cube = cube_unitful - 1 * u.ct # Subtracting a scalar quantity + + >>> new_cube = cube_with_unit - 1 * u.ct # Subtracting a scalar quantity >>> new_cube.data array([[ 9., 10., 11.], [12., 13., 14.]]) - >>> new_cube = cube_unitful + arr * u.ct # Adding an array-like quantity + + >>> new_cube = cube_with_unit + arr * u.ct # Adding an array-like quantity >>> new_cube.data array([[10., 12., 14.], [16., 18., 20.]]) - >>> new_cube = cube_unitful - arr * u.ct # Subtracting an array-like quantity + + >>> new_cube = cube_with_unit - arr * u.ct # Subtracting an array-like quantity >>> new_cube.data array([[10., 10., 10.], [10., 10., 10.]]) @@ -133,8 +141,10 @@ Multiplying and Dividing with Numbers, Arrays and Quantities An `~ndcube.NDCube` can be multiplied and divided by numbers, arrays, and `~astropy.units.Quantity` via the ``*`` and ``-`` operators. These work similarly to addition and subtraction with a few minor differences: + - The uncertainties of the resulting `~ndcube.NDCube` are scaled by the same factor as the data. - Classes with different units can be combined. + * e.g. an `~ndcube.NDCube` with a unit of counts divided by an `~astropy.units.Quantity` with a unit is seconds will result in an `~ndcube.NDCube` with a unit of counts per second. * This also holds for cases were unitful and unitless classes can be combined. In such cases, the unit of the resulting `~ndcube.NDCube` will be the same as that of the unitful object. @@ -143,21 +153,22 @@ Below are some examples. .. code-block:: python >>> # See attributes of original cube. - >>> cube_unitful.data + >>> cube_with_unit.data array([[10, 11, 12], [13, 14, 15]]) - >>> cube_unitful.unit + >>> cube_with_unit.unit Unit("ct") - >>> cube_unitful.uncertainty + >>> cube_with_unit.uncertainty StdDevUncertainty([[1. , 1.1, 1.2], [1.3, 1.4, 1.5]]) >>> # Multiply by a unitless array. - >>> arr = 1 + np.arange(cube_unitful.data.size).reshape(cube_unitful.data.shape) + >>> arr = 1 + np.arange(cube_with_unit.data.size).reshape(cube_with_unit.data.shape) >>> arr array([[1, 2, 3], [4, 5, 6]]) - >>> new_cube = cube_unitful * arr + + >>> new_cube = cube_with_unit * arr >>> # Inspect attributes of resultant cube. >>> new_cube.data @@ -170,7 +181,7 @@ Below are some examples. [5.2, 7. , 9. ]]) >>> # Divide by an astropy Quantity. - >>> new_cube = cube_unitful / (2 * u.s) + >>> new_cube = cube_with_unit / (2 * u.s) >>> # Inspect attributes of resultant cube. >>> new_cube.data @@ -182,11 +193,17 @@ Below are some examples. StdDevUncertainty([[0.5 , 0.55, 0.6 ], [0.65, 0.7 , 0.75]]) +Note that when performing arithmetic operations with `~ndcube.NDCube` and array-like objects, their shapes only have to be broadcastable. +For example: + +Raising NDCube to a Power +------------------------- + .. _arithmetic_nddata: -Arithmetic Operations between Coordinate-less NDData -==================================================== +Arithmetic Operations with Coordinate-less NDData +================================================= Sometimes more advanced arithmetic operations are required. For example, we may want to create a sequence of running difference images which highlight changes between frames, and propagate the uncertainties associated with each image. @@ -194,8 +211,8 @@ Alternatively, we may want to subtract one image from another, but exclude a cer In such cases, numbers, arrays and `~astropy.units.Quantity` are insufficient, and we would like to subtract two `~ndcube.NDCube` objects. This is not directly supported, but can still be achieved in practice, as we shall see below. -Why Arithmetic Operations with Coordinate-aware NDData Instances Are Not Directly Supported, and How the Same Result Can Be Achieved ------------------------------------------------------------------------------------------------------------------------------------- +Why Arithmetic Operations with Coordinate-aware NDData Are Not Directly Supported, and How the Same Result Can Be Achieved +-------------------------------------------------------------------------------------------------------------------------- Arithmetic operations between two `~ndcube.NDCube` instances (or equivalently, an `~ndcube.NDCube` and another coordinate-aware object) are not supported because of the possibility of supporting non-sensical operations. For example, what does it mean to multiply a spectrum and an image in a coordinate-aware way? @@ -211,8 +228,8 @@ In many cases, a simple solution would be to extract the data (an optionally the .. expanding-code-block:: python :summary: Expand to see definition of cube1 and cube2. - >>> cube1 = cube_unitful - >>> cube2 = cube_unitful / 4 + >>> cube1 = cube_with_unit + >>> cube2 = cube_with_unit / 4 .. code-block:: python From 237c90a757c096c8d8fa2f1191cd7159607d90c6 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Fri, 3 Oct 2025 13:52:31 +0100 Subject: [PATCH 16/26] First complete draft of arithmetic docs. --- docs/explaining_ndcube/arithmetic.rst | 100 ++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 5 deletions(-) diff --git a/docs/explaining_ndcube/arithmetic.rst b/docs/explaining_ndcube/arithmetic.rst index 889cbdaf9..99b827566 100644 --- a/docs/explaining_ndcube/arithmetic.rst +++ b/docs/explaining_ndcube/arithmetic.rst @@ -199,6 +199,28 @@ For example: Raising NDCube to a Power ------------------------- +`~ndcube.NDCube` can be raised to a power. + +.. code-block:: python + + >>> cube_with_unit.data + array([[10, 11, 12], + [13, 14, 15]]) + + >>> new_cube = cube_with_unit**2 + + >>> new_cube.data + array([[100, 121, 144], + [169, 196, 225]]) + >>> new_cube.unit + Unit("ct2") + >>> (new_cube.mask == cube_with_unit.mask).all() + np.True_ + +Note that error propagation is delegated to the ``cube.uncertainty`` object. +Therefore, if this class supports error propagation by power, then ``new_cube`` will include uncertainty. +Otherwise, ``new_cube.uncertainty`` will be set to ``None``. + .. _arithmetic_nddata: @@ -211,10 +233,10 @@ Alternatively, we may want to subtract one image from another, but exclude a cer In such cases, numbers, arrays and `~astropy.units.Quantity` are insufficient, and we would like to subtract two `~ndcube.NDCube` objects. This is not directly supported, but can still be achieved in practice, as we shall see below. -Why Arithmetic Operations with Coordinate-aware NDData Are Not Directly Supported, and How the Same Result Can Be Achieved --------------------------------------------------------------------------------------------------------------------------- +Why Arithmetic Operations with Coordinate-aware NDData Are Not Directly Supported, and How This Can Be Overcome +--------------------------------------------------------------------------------------------------------------- -Arithmetic operations between two `~ndcube.NDCube` instances (or equivalently, an `~ndcube.NDCube` and another coordinate-aware object) are not supported because of the possibility of supporting non-sensical operations. +Arithmetic operations between two `~ndcube.NDCube` instances (or equivalently, an `~ndcube.NDCube` and another coordinate-aware `~astropy.nddata.NDData` subclass) are not supported because of the possibility of supporting non-sensical operations. For example, what does it mean to multiply a spectrum and an image in a coordinate-aware way? Getting the difference between two images may make physical sense, but only in certain circumstances. For example, subtracting two sequential images of the same region of the Sun is a common step in many solar image analysis workflows. @@ -223,7 +245,7 @@ Even when subtracting two images of the Sun, drift in the telescope's pointing m In this case, it is questionable whether this operation makes physical sense after all. Moreover, in all of these cases, it is not at all clear what the resulting WCS object should be. -In many cases, a simple solution would be to extract the data (an optionally the unit) of one of the `~ndcube.NDCube` instances and perform the operation as described in the above section on :ref:`arithmetic_standard`: +In many cases, a simple solution would be to extract the data (an optionally the unit) from one of the `~ndcube.NDCube` instances and perform the operation as described in the above section on :ref:`arithmetic_standard`: .. expanding-code-block:: python :summary: Expand to see definition of cube1 and cube2. @@ -235,7 +257,7 @@ In many cases, a simple solution would be to extract the data (an optionally the >>> new_cube = cube1 - cube2.data * cube2.unit -However, this does not allow for the propagation of uncertainties or masks associated with the data in ``cube2``. +However, this does not allow for the propagation of uncertainties or masks associated with ``cube2``. Therefore, `~ndcube.NDCube` does support arithmetic operations with instances of `~astropy.nddata.NDData` subclasses whose ``wcs`` attribute is ``None``. This makes users explicitly aware that they are dispensing with coordinate-awareness on one of their operands. It also leaves only one WCS involved in the operation, thus removing ambiguity regarding the WCS of the `~ndcube.NDCube` resulting from the operation. @@ -251,3 +273,71 @@ Users who would like to drop coordinate-awareness from an `~ndcube.NDCube` can s Performing Arithmetic Operations with Coordinate-less NDData ------------------------------------------------------------ + +Addition, subtraction, multiplication and division between `~ndcube.NDCube` and coordinate-less `~astropy.nddata.NDData` classes are all supported via the ``+``, ``-``, ``*``, and ``/`` operators. +With respect to the ``data`` and ``unit`` attributes, the behaviors are the same as for arrays and `~astropy.units.Quantity`. +The power of using coordinate-less `~astropy.nddata.NDData` classes is the ability to handle uncertainties and masks. + +Uncertainty Propagation +*********************** + +The uncertainty associated with the `~ndcube.NDCube` resulting from the arithmetic operation depends on the uncertainty types of the operands: + +- ``NDCube.uncertainty`` and ``NDData.uncertainty`` are both ``None`` => ``new_cube.uncertainty`` is ``None``; +- ``NDCube`` or ``NDData`` have uncertainty, but not both => the existing uncertainty is assigned to ``new_cube`` as is; +- ``NDCube`` and ``NDData`` both have uncertainty => uncertainty propagation is delegated to the ``NDCube.uncertainty.propagate`` method. + + * Note that not all uncertainty classes support error propagation, e.g. `~astropy.nddata.UnknownUncertainty`. In such cases, uncertainties are dropped altogether and ``new_cube.uncertainty`` is set to ``None``. + +If users would like to remove uncertainty from one of the operands in order to propagate the other without alteration, this can be done before the arithmetic operation via: + +.. code-block:: python + + >>> # Remove uncertainty from NDCube + >>> cube1_nouncert = NDCube(cube2, wcs=None) + >>> new_cube = cube1_nouncert + cube2_nocoords + + >>> # Remove uncertainty from coordinate-less NDData + >>> cube2_nocoords_nouncert = NDData(cube2, wcs=None, uncertainty=None) + >>> new_cube = cube1 / cube2_nocoords_nouncert + +Mask Operations +*************** + +The mask associated with the `~ndcube.NDCube` resulting from the arithmetic operation depends on the mask types of the operands: + +- ``NDCube.mask`` and ``NDData.mask`` are both ``None`` => ``new_cube.mask`` is ``None``; +- ``NDCube`` or ``NDData`` have a mask, but not both => the existing mask is assigned to ``new_cube`` as is; +- ``NDCube`` and ``NDData`` both have masks => The masks are combined via `numpy.logical_or`. + +The mask values do not affect the ``data`` values output by the operation. +However, in some cases, the mask may be used to identify regions of unreliable data that should not be included in the operation. +This can be achieved by altering the masked data values before the operation via the `ndcube.NDCube.fill_masked` method. +In the case of addition and subtraction, the ``fill_value`` should be ``0``. + +.. code-block:: python + + >>> cube_filled = cube1.fill_masked(0) + >>> new_cube = cube_filled + cube2_nocoords + +By replacing masked data values with ``0``, these pixels are effectively not included in the addition, and the data values from ``cube2_nocoords`` are passed into ``new_cube`` unchanged. +In the above example, both operands have uncertainties, which means masked uncertainties are propagated through the addition, even though the masked data values have been set to ``0``. +Propagation of masked uncertainties can also be suppressed by setting the optional kwarg, ``fill_uncertainty_value=0``. +By default, the mask of ``cube_filled`` is not changed, and therefore is incorporated into the mask of the output cube. +However, mask propagation can also be suppressed by setting the optional kwarg, ``unmask=True``, which sets ``cube_filled0.mask`` to ``False``. + +In the case of multiplication and division, and ``fill_value`` of ``1`` will prevent masked values being including in the operations: + +.. code-block:: python + + >>> cube_filled = cube1.fill_masked(1, fill_uncertainty_value=0, unmask=True) + >>> new_cube = cube_filled * cube2_nocoords + +By default, `ndcube.NDCube.fill_masked` returns a new `~ndcube.NDCube` instance. +However, in some case it may be preferable to fill the masked values in-place, e.g. because the data within the `~ndcube.NDCube` is very large and users want to control the number of copies in RAM. +In this case, the ``fill_in_place`` can be used. + +.. code-block:: python + + >>> cube1.fill_masked(0, fill_in_place=True) + >>> new_cube = cube1 + cube2_nocoords From 9f9805f683ddf14f08622ae3a28510791c0e7ea6 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Fri, 3 Oct 2025 14:41:26 +0100 Subject: [PATCH 17/26] Updates to arithmetic docs. --- docs/explaining_ndcube/arithmetic.rst | 90 ++++++++++++++++----------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/docs/explaining_ndcube/arithmetic.rst b/docs/explaining_ndcube/arithmetic.rst index 99b827566..39c8fb450 100644 --- a/docs/explaining_ndcube/arithmetic.rst +++ b/docs/explaining_ndcube/arithmetic.rst @@ -6,25 +6,25 @@ Arithmetic Operations Arithmetic operations are a crucial tool in n-dimensional data analysis. Applications include subtracting a background from a 1-D timeseries or spectrum, scaling an image by a vignetting function, and many others. -To aid with such workflows, `~ndcube.NDCube` supports addition, subtraction, multiplication, and division with numbers, arrays, `~astropy.units.Quantity`. +To aid with such workflows, `~ndcube.NDCube` supports addition, subtraction, multiplication, and division with numbers, arrays, and `~astropy.units.Quantity`. Raising an `~ndcube.NDCube` to a power is also supported. These operations return a new `~ndcube.NDCube` with the data array (and, where appropriate, the uncertainties) altered in accordance with the arithmetic operation. Other attributes of the `~ndcube.NDCube` remain unchanged. -In addition, combining `~ndcube.NDCube` with coordinate-less `~astropy.nddata.NDData` subclasses via these operations is also supported. -Such operations can be more complicated. See the section below on :ref:`arithmetic_nddata` for a discussion separate, more detailed discussion. +In addition, arithmetic operations between `~ndcube.NDCube` and coordinate-less `~astropy.nddata.NDData` subclasses is supported. +See the section below on :ref:`arithmetic_nddata` for a separate, more detailed discussion. .. _arithmetic_standard: -Standard Arithmetic Operations -============================== +Arithmetic Operations with Numbers, Arrays and Quantities +========================================================= -Addition and Subtraction with Numbers, Arrays and Quantities ------------------------------------------------------------- +Addition and Subtraction +------------------------ Numbers, arrays and `~astropy.units.Quantity` can be added to and subtracted from `~ndcube.NDCube` via the ``+`` and ``-`` operators. -Note that addition and subtraction only change the data values of the `~ndcube.NDCube`. -Let's deomonstrate with an example `~ndcube.NDCube`, ``cube`` +Note that these only change the data values of the `~ndcube.NDCube` and units must be consistent with that of the `~ndcube.NDCube`. +Let's demonstrate with an example `~ndcube.NDCube`, ``cube`` .. expanding-code-block:: python :summary: Expand to see cube instantiated. @@ -70,7 +70,7 @@ Let's deomonstrate with an example `~ndcube.NDCube`, ``cube`` [14, 15, 16]]) Note that all the data values have been increased by 1. -We can also add an array if we want to add a different number to each data element: +We can also use an array if we want to add a different number to each data element: .. code-block:: python @@ -99,7 +99,7 @@ Subtraction works in the same way. array([[10, 10, 10], [10, 10, 10]]) -Note that int he above examples, ``cube`` has no unit. +Note that in the above examples, ``cube`` has no unit. This is why we are able to add and subtract numbers and arrays. If, however, we have an `~ndcube.NDCube` with a unit assigned, @@ -136,16 +136,16 @@ In such cases, we must use a `~astropy.units.Quantity` with a compatible unit: array([[10., 10., 10.], [10., 10., 10.]]) -Multiplying and Dividing with Numbers, Arrays and Quantities ------------------------------------------------------------- +Multiplication and Division +--------------------------- An `~ndcube.NDCube` can be multiplied and divided by numbers, arrays, and `~astropy.units.Quantity` via the ``*`` and ``-`` operators. These work similarly to addition and subtraction with a few minor differences: - The uncertainties of the resulting `~ndcube.NDCube` are scaled by the same factor as the data. -- Classes with different units can be combined. +- Classes with different non-equivalent units can be combined. - * e.g. an `~ndcube.NDCube` with a unit of counts divided by an `~astropy.units.Quantity` with a unit is seconds will result in an `~ndcube.NDCube` with a unit of counts per second. + * e.g. an `~ndcube.NDCube` with a unit of ``ct`` divided by an `~astropy.units.Quantity` with a unit of ``s`` will result in an `~ndcube.NDCube` with a unit of ``ct / s``. * This also holds for cases were unitful and unitless classes can be combined. In such cases, the unit of the resulting `~ndcube.NDCube` will be the same as that of the unitful object. Below are some examples. @@ -180,7 +180,7 @@ Below are some examples. StdDevUncertainty([[1. , 2.2, 3.6], [5.2, 7. , 9. ]]) - >>> # Divide by an astropy Quantity. + >>> # Divide by a scalar astropy Quantity. >>> new_cube = cube_with_unit / (2 * u.s) >>> # Inspect attributes of resultant cube. @@ -193,21 +193,29 @@ Below are some examples. StdDevUncertainty([[0.5 , 0.55, 0.6 ], [0.65, 0.7 , 0.75]]) -Note that when performing arithmetic operations with `~ndcube.NDCube` and array-like objects, their shapes only have to be broadcastable. +Note that when performing arithmetic operations with `~ndcube.NDCube` and array-like objects, their shapes only have to be broadcastable, not necessarily the same. For example: +.. code-block:: python + + >>> arr[0] + array([1, 2, 3]) + + >>> new_cube = cube + arr[0] + >>> new_cube.data + array([[11, 13, 15], + [14, 16, 18]]) + Raising NDCube to a Power ------------------------- -`~ndcube.NDCube` can be raised to a power. - .. code-block:: python >>> cube_with_unit.data array([[10, 11, 12], [13, 14, 15]]) - >>> new_cube = cube_with_unit**2 + >>> new_cube = cube_with_unit**2 # noqa >>> new_cube.data array([[100, 121, 144], @@ -228,24 +236,33 @@ Arithmetic Operations with Coordinate-less NDData ================================================= Sometimes more advanced arithmetic operations are required. -For example, we may want to create a sequence of running difference images which highlight changes between frames, and propagate the uncertainties associated with each image. +For example, we may want to create a sequence of running difference images which highlight changes between frames, and propagate the uncertainties associated with the image subtraction. Alternatively, we may want to subtract one image from another, but exclude a certain region of the image with a mask. -In such cases, numbers, arrays and `~astropy.units.Quantity` are insufficient, and we would like to subtract two `~ndcube.NDCube` objects. -This is not directly supported, but can still be achieved in practice, as we shall see below. +In such cases, numbers, arrays and `~astropy.units.Quantity` are insufficient. +Instead it would be better to subtract two `~ndcube.NDCube` objects. +This is not directly supported, for reasons we will see below. +The the effect of the operation can still be achieved in practice, as well shall also see. Why Arithmetic Operations with Coordinate-aware NDData Are Not Directly Supported, and How This Can Be Overcome --------------------------------------------------------------------------------------------------------------- -Arithmetic operations between two `~ndcube.NDCube` instances (or equivalently, an `~ndcube.NDCube` and another coordinate-aware `~astropy.nddata.NDData` subclass) are not supported because of the possibility of supporting non-sensical operations. -For example, what does it mean to multiply a spectrum and an image in a coordinate-aware way? +The remit of the `ndcube` package is support N-dimensional coordinate-aware data astronomical data analysis. +Arithmetic operations between two `~ndcube.NDCube` instances (or equivalently, an `~ndcube.NDCube` and another coordinate-aware `~astropy.nddata.NDData` subclass) are therefore not directly supported because of the possibility of supporting non-sensical operations. +(Although they are supported indirectly, as we shall see below.) +For example, what does it mean to multiply a spectrum and an image? Getting the difference between two images may make physical sense, but only in certain circumstances. For example, subtracting two sequential images of the same region of the Sun is a common step in many solar image analysis workflows. -However, subtracting images of different parts of the sky, e.g. the Sun and the Crab Nebula, does not result in a physically meaningful image. +But subtracting images of different parts of the sky, e.g. the Sun and the Crab Nebula, does not produce a physically meaningful result. Even when subtracting two images of the Sun, drift in the telescope's pointing may result in the pixels in each image corresponding to different points in the Sun. -In this case, it is questionable whether this operation makes physical sense after all. +In this case, it is questionable whether even this operation makes physical sense. Moreover, in all of these cases, it is not at all clear what the resulting WCS object should be. -In many cases, a simple solution would be to extract the data (an optionally the unit) from one of the `~ndcube.NDCube` instances and perform the operation as described in the above section on :ref:`arithmetic_standard`: +One way to ensure physically meaningful, coordinate-aware arithmetic operations between `~ndcube-NDCube` instances would be to compare their WCS objects are the same within a certain tolerance. +Alternatively, the arithmetic operation could attempt to reproject on `~ndcube-NDCube` to the other's WCS. +However, these operations are potentially prohibitively slow and operationally expensive. + +Despite this, arithmetic operations between two `~ndcube.NDCube` instance is supported, provided the coordinate-awareness of one is dropped. +A simple solution that satisfies many use-cases is to extract the data (an optionally the unit) from one of the `~ndcube.NDCube` instances and perform the operation as described in the above section on :ref:`arithmetic_standard`: .. expanding-code-block:: python :summary: Expand to see definition of cube1 and cube2. @@ -258,8 +275,8 @@ In many cases, a simple solution would be to extract the data (an optionally the >>> new_cube = cube1 - cube2.data * cube2.unit However, this does not allow for the propagation of uncertainties or masks associated with ``cube2``. -Therefore, `~ndcube.NDCube` does support arithmetic operations with instances of `~astropy.nddata.NDData` subclasses whose ``wcs`` attribute is ``None``. -This makes users explicitly aware that they are dispensing with coordinate-awareness on one of their operands. +Therefore, `~ndcube.NDCube` also support arithmetic operations with instances of `~astropy.nddata.NDData` subclasses whose ``wcs`` attribute is ``None``. +Requiring users to remove coordinates in this way makes them explicitly aware that they are dispensing with coordinate-awareness on one of their operands. It also leaves only one WCS involved in the operation, thus removing ambiguity regarding the WCS of the `~ndcube.NDCube` resulting from the operation. Users who would like to drop coordinate-awareness from an `~ndcube.NDCube` can so simply by converting it to an `~astropy.nddata.NDData` and setting the ``wcs`` to ``None``: @@ -281,7 +298,7 @@ The power of using coordinate-less `~astropy.nddata.NDData` classes is the abili Uncertainty Propagation *********************** -The uncertainty associated with the `~ndcube.NDCube` resulting from the arithmetic operation depends on the uncertainty types of the operands: +The uncertainty resulting from the arithmetic operation depends on the uncertainty types of the operands: - ``NDCube.uncertainty`` and ``NDData.uncertainty`` are both ``None`` => ``new_cube.uncertainty`` is ``None``; - ``NDCube`` or ``NDData`` have uncertainty, but not both => the existing uncertainty is assigned to ``new_cube`` as is; @@ -294,7 +311,7 @@ If users would like to remove uncertainty from one of the operands in order to p .. code-block:: python >>> # Remove uncertainty from NDCube - >>> cube1_nouncert = NDCube(cube2, wcs=None) + >>> cube1_nouncert = NDCube(cube1, wcs=None) >>> new_cube = cube1_nouncert + cube2_nocoords >>> # Remove uncertainty from coordinate-less NDData @@ -320,13 +337,14 @@ In the case of addition and subtraction, the ``fill_value`` should be ``0``. >>> cube_filled = cube1.fill_masked(0) >>> new_cube = cube_filled + cube2_nocoords -By replacing masked data values with ``0``, these pixels are effectively not included in the addition, and the data values from ``cube2_nocoords`` are passed into ``new_cube`` unchanged. +By replacing masked data values with ``0``, these values are effectively not included in the addition, and the data values from ``cube2_nocoords`` are passed into ``new_cube`` unchanged. In the above example, both operands have uncertainties, which means masked uncertainties are propagated through the addition, even though the masked data values have been set to ``0``. Propagation of masked uncertainties can also be suppressed by setting the optional kwarg, ``fill_uncertainty_value=0``. + By default, the mask of ``cube_filled`` is not changed, and therefore is incorporated into the mask of the output cube. However, mask propagation can also be suppressed by setting the optional kwarg, ``unmask=True``, which sets ``cube_filled0.mask`` to ``False``. -In the case of multiplication and division, and ``fill_value`` of ``1`` will prevent masked values being including in the operations: +In the case of multiplication and division, a ``fill_value`` of ``1`` will prevent masked values being including in the operations. (Also see the optional use of the ``fill_uncertainty_value`` and ``unmask`` kwargs.) .. code-block:: python @@ -334,8 +352,8 @@ In the case of multiplication and division, and ``fill_value`` of ``1`` will pre >>> new_cube = cube_filled * cube2_nocoords By default, `ndcube.NDCube.fill_masked` returns a new `~ndcube.NDCube` instance. -However, in some case it may be preferable to fill the masked values in-place, e.g. because the data within the `~ndcube.NDCube` is very large and users want to control the number of copies in RAM. -In this case, the ``fill_in_place`` can be used. +However, in some case it may be preferable to fill the masked values in-place, for example because the data within the `~ndcube.NDCube` is very large and users want to control the number of copies in RAM. +In this case, the ``fill_in_place`` kwarg can be used. .. code-block:: python From a58a23194a7c3de83e71160bf22b403ad78ba4dd Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Fri, 3 Oct 2025 17:24:15 +0100 Subject: [PATCH 18/26] Updates to arithmetic docs. --- docs/explaining_ndcube/arithmetic.rst | 61 +++++++++++++++------------ 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/docs/explaining_ndcube/arithmetic.rst b/docs/explaining_ndcube/arithmetic.rst index 39c8fb450..9e3679d19 100644 --- a/docs/explaining_ndcube/arithmetic.rst +++ b/docs/explaining_ndcube/arithmetic.rst @@ -5,14 +5,14 @@ Arithmetic Operations ********************* Arithmetic operations are a crucial tool in n-dimensional data analysis. -Applications include subtracting a background from a 1-D timeseries or spectrum, scaling an image by a vignetting function, and many others. -To aid with such workflows, `~ndcube.NDCube` supports addition, subtraction, multiplication, and division with numbers, arrays, and `~astropy.units.Quantity`. +Applications include subtracting a background from a 1-D timeseries or spectrum, scaling an image by a vignetting function, and many more. +To this end, `~ndcube.NDCube` supports addition, subtraction, multiplication, and division with numbers, arrays, and `~astropy.units.Quantity`. Raising an `~ndcube.NDCube` to a power is also supported. -These operations return a new `~ndcube.NDCube` with the data array (and, where appropriate, the uncertainties) altered in accordance with the arithmetic operation. +These operations return a new `~ndcube.NDCube` with the data array (and, where appropriate, the uncertainties and unit) altered in accordance with the arithmetic operation. Other attributes of the `~ndcube.NDCube` remain unchanged. -In addition, arithmetic operations between `~ndcube.NDCube` and coordinate-less `~astropy.nddata.NDData` subclasses is supported. -See the section below on :ref:`arithmetic_nddata` for a separate, more detailed discussion. +Arithmetic operations between `~ndcube.NDCube` and coordinate-less `~astropy.nddata.NDData` subclasses are also supported. +See the section below on :ref:`arithmetic_nddata` for further details. .. _arithmetic_standard: @@ -24,7 +24,7 @@ Addition and Subtraction Numbers, arrays and `~astropy.units.Quantity` can be added to and subtracted from `~ndcube.NDCube` via the ``+`` and ``-`` operators. Note that these only change the data values of the `~ndcube.NDCube` and units must be consistent with that of the `~ndcube.NDCube`. -Let's demonstrate with an example `~ndcube.NDCube`, ``cube`` +Let's demonstrate with an example `~ndcube.NDCube`, ``cube``: .. expanding-code-block:: python :summary: Expand to see cube instantiated. @@ -143,7 +143,7 @@ An `~ndcube.NDCube` can be multiplied and divided by numbers, arrays, and `~astr These work similarly to addition and subtraction with a few minor differences: - The uncertainties of the resulting `~ndcube.NDCube` are scaled by the same factor as the data. -- Classes with different non-equivalent units can be combined. +- Classes with non-equivalent units can be combined. * e.g. an `~ndcube.NDCube` with a unit of ``ct`` divided by an `~astropy.units.Quantity` with a unit of ``s`` will result in an `~ndcube.NDCube` with a unit of ``ct / s``. * This also holds for cases were unitful and unitless classes can be combined. In such cases, the unit of the resulting `~ndcube.NDCube` will be the same as that of the unitful object. @@ -198,6 +198,9 @@ For example: .. code-block:: python + >>> cube.data + array([[10, 11, 12], + [13, 14, 15]]) >>> arr[0] array([1, 2, 3]) @@ -215,7 +218,10 @@ Raising NDCube to a Power array([[10, 11, 12], [13, 14, 15]]) - >>> new_cube = cube_with_unit**2 # noqa + >>> import warnings + >>> with warnings.catch_warnings(): + ... warnings.simplefilter("ignore") # Catching warnings not needed but keeps docs cleaner. + ... new_cube = cube_with_unit**2 >>> new_cube.data array([[100, 121, 144], @@ -236,32 +242,31 @@ Arithmetic Operations with Coordinate-less NDData ================================================= Sometimes more advanced arithmetic operations are required. -For example, we may want to create a sequence of running difference images which highlight changes between frames, and propagate the uncertainties associated with the image subtraction. -Alternatively, we may want to subtract one image from another, but exclude a certain region of the image with a mask. +For example, we may want to create a sequence of running difference images which highlight changes between frames while propagating the uncertainties associated with the image subtraction. +Alternatively, we may want exclude a certain regions of one or other image from the operation with a mask. In such cases, numbers, arrays and `~astropy.units.Quantity` are insufficient. Instead it would be better to subtract two `~ndcube.NDCube` objects. -This is not directly supported, for reasons we will see below. -The the effect of the operation can still be achieved in practice, as well shall also see. +While this is not directly supported, for reasons we will see below, the operation can still be achieved indirectly, as well shall also see. Why Arithmetic Operations with Coordinate-aware NDData Are Not Directly Supported, and How This Can Be Overcome --------------------------------------------------------------------------------------------------------------- -The remit of the `ndcube` package is support N-dimensional coordinate-aware data astronomical data analysis. +The purpose of the `ndcube` package is to support N-dimensional coordinate-aware data astronomical data analysis. Arithmetic operations between two `~ndcube.NDCube` instances (or equivalently, an `~ndcube.NDCube` and another coordinate-aware `~astropy.nddata.NDData` subclass) are therefore not directly supported because of the possibility of supporting non-sensical operations. -(Although they are supported indirectly, as we shall see below.) +(Although, as stated above, they are indirectly supported.) For example, what does it mean to multiply a spectrum and an image? Getting the difference between two images may make physical sense, but only in certain circumstances. -For example, subtracting two sequential images of the same region of the Sun is a common step in many solar image analysis workflows. +For example, subtracting two sequential images of the same region of the Sun is a common step in many solar image analyses. But subtracting images of different parts of the sky, e.g. the Sun and the Crab Nebula, does not produce a physically meaningful result. -Even when subtracting two images of the Sun, drift in the telescope's pointing may result in the pixels in each image corresponding to different points in the Sun. +Even when subtracting two images of the Sun, drift in the telescope's pointing may result in the pixels in each image corresponding to different points on the Sun. In this case, it is questionable whether even this operation makes physical sense. Moreover, in all of these cases, it is not at all clear what the resulting WCS object should be. -One way to ensure physically meaningful, coordinate-aware arithmetic operations between `~ndcube-NDCube` instances would be to compare their WCS objects are the same within a certain tolerance. -Alternatively, the arithmetic operation could attempt to reproject on `~ndcube-NDCube` to the other's WCS. -However, these operations are potentially prohibitively slow and operationally expensive. +One way to ensure physically meaningful, coordinate-aware arithmetic operations between `~ndcube.NDCube` instances would be to compare their WCS objects are the same within a certain tolerance. +Alternatively, the arithmetic operation could attempt to reproject on `~ndcube.NDCube` to the other's WCS. +However, these operations can be prohibitively slow and resource-hungry. -Despite this, arithmetic operations between two `~ndcube.NDCube` instance is supported, provided the coordinate-awareness of one is dropped. +Despite this, arithmetic operations between two `~ndcube.NDCube` instances is supported, provided the coordinate-awareness of one is dropped. A simple solution that satisfies many use-cases is to extract the data (an optionally the unit) from one of the `~ndcube.NDCube` instances and perform the operation as described in the above section on :ref:`arithmetic_standard`: .. expanding-code-block:: python @@ -276,8 +281,8 @@ A simple solution that satisfies many use-cases is to extract the data (an optio However, this does not allow for the propagation of uncertainties or masks associated with ``cube2``. Therefore, `~ndcube.NDCube` also support arithmetic operations with instances of `~astropy.nddata.NDData` subclasses whose ``wcs`` attribute is ``None``. -Requiring users to remove coordinates in this way makes them explicitly aware that they are dispensing with coordinate-awareness on one of their operands. -It also leaves only one WCS involved in the operation, thus removing ambiguity regarding the WCS of the `~ndcube.NDCube` resulting from the operation. +Requiring users to remove coordinates in this way makes them explicitly aware that they are dispensing with coordinate-awareness on one of their operands, and gives them the power to select the one for which this is done. +It also leaves only one WCS involved in the operation, thus removing ambiguity regarding the WCS of the resulting `~ndcube.NDCube`. Users who would like to drop coordinate-awareness from an `~ndcube.NDCube` can so simply by converting it to an `~astropy.nddata.NDData` and setting the ``wcs`` to ``None``: @@ -318,8 +323,8 @@ If users would like to remove uncertainty from one of the operands in order to p >>> cube2_nocoords_nouncert = NDData(cube2, wcs=None, uncertainty=None) >>> new_cube = cube1 / cube2_nocoords_nouncert -Mask Operations -*************** +Mask Operations and the NDCube.fill_masked Method +************************************************* The mask associated with the `~ndcube.NDCube` resulting from the arithmetic operation depends on the mask types of the operands: @@ -341,10 +346,10 @@ By replacing masked data values with ``0``, these values are effectively not inc In the above example, both operands have uncertainties, which means masked uncertainties are propagated through the addition, even though the masked data values have been set to ``0``. Propagation of masked uncertainties can also be suppressed by setting the optional kwarg, ``fill_uncertainty_value=0``. -By default, the mask of ``cube_filled`` is not changed, and therefore is incorporated into the mask of the output cube. -However, mask propagation can also be suppressed by setting the optional kwarg, ``unmask=True``, which sets ``cube_filled0.mask`` to ``False``. +By default, the mask of ``cube_filled`` is not changed, and therefore is incorporated into the mask of ``new_cube``. +However, mask propagation can also be suppressed by setting the optional kwarg, ``unmask=True``, which sets ``cube_filled.mask`` to ``False``. -In the case of multiplication and division, a ``fill_value`` of ``1`` will prevent masked values being including in the operations. (Also see the optional use of the ``fill_uncertainty_value`` and ``unmask`` kwargs.) +In the case of multiplication and division, a ``fill_value`` of ``1`` will prevent masked values being including in the operations. (Also see, below, the optional use of the ``fill_uncertainty_value`` and ``unmask`` kwargs.) .. code-block:: python @@ -352,7 +357,7 @@ In the case of multiplication and division, a ``fill_value`` of ``1`` will preve >>> new_cube = cube_filled * cube2_nocoords By default, `ndcube.NDCube.fill_masked` returns a new `~ndcube.NDCube` instance. -However, in some case it may be preferable to fill the masked values in-place, for example because the data within the `~ndcube.NDCube` is very large and users want to control the number of copies in RAM. +However, in some cases it may be preferable to fill the masked values in-place, for example, because the data within the `~ndcube.NDCube` is very large and users want to control the number of copies in RAM. In this case, the ``fill_in_place`` kwarg can be used. .. code-block:: python From 212d01363c035abcb3aa49cd7027cb23e15d20d4 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Sun, 5 Oct 2025 18:41:50 +0100 Subject: [PATCH 19/26] Fix bug raising error when two coordinate aware objects are combined via arithmetic operations. --- changelog/{880.bugfix.rst => 880.bugfix.1.rst} | 0 changelog/880.bugfix.2.rst | 1 + ndcube/ndcube.py | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) rename changelog/{880.bugfix.rst => 880.bugfix.1.rst} (100%) create mode 100644 changelog/880.bugfix.2.rst diff --git a/changelog/880.bugfix.rst b/changelog/880.bugfix.1.rst similarity index 100% rename from changelog/880.bugfix.rst rename to changelog/880.bugfix.1.rst diff --git a/changelog/880.bugfix.2.rst b/changelog/880.bugfix.2.rst new file mode 100644 index 000000000..d6fb8efe2 --- /dev/null +++ b/changelog/880.bugfix.2.rst @@ -0,0 +1 @@ +Fix bug where error was returned rather than raised with trying to perform arithmetic operation between `~ndcube.NDCube` and an object whose WCS attribute is not ``None``. diff --git a/ndcube/ndcube.py b/ndcube/ndcube.py index b0f98bb83..79d0bbe06 100644 --- a/ndcube/ndcube.py +++ b/ndcube/ndcube.py @@ -978,7 +978,7 @@ def _arithmetic_handle_mask(self, self_mask, value_mask): def _arithmetic_operate_with_nddata(self, operation, value): handle_mask = self._arithmetic_handle_mask if value.wcs is not None: - return TypeError("Cannot add coordinate-aware NDCubes together.") + raise TypeError("Cannot add coordinate-aware objects to NDCubes.") kwargs = {} if operation == "add": # Handle units From 23a589b9f72908d7a51b85b26cc02da796fa457f Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Sun, 5 Oct 2025 18:46:45 +0100 Subject: [PATCH 20/26] Updates arithmetic docs. --- docs/explaining_ndcube/arithmetic.rst | 29 +++++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/explaining_ndcube/arithmetic.rst b/docs/explaining_ndcube/arithmetic.rst index 9e3679d19..a9d27a169 100644 --- a/docs/explaining_ndcube/arithmetic.rst +++ b/docs/explaining_ndcube/arithmetic.rst @@ -221,6 +221,7 @@ Raising NDCube to a Power >>> import warnings >>> with warnings.catch_warnings(): ... warnings.simplefilter("ignore") # Catching warnings not needed but keeps docs cleaner. + ... ... new_cube = cube_with_unit**2 >>> new_cube.data @@ -241,15 +242,21 @@ Otherwise, ``new_cube.uncertainty`` will be set to ``None``. Arithmetic Operations with Coordinate-less NDData ================================================= -Sometimes more advanced arithmetic operations are required. -For example, we may want to create a sequence of running difference images which highlight changes between frames while propagating the uncertainties associated with the image subtraction. -Alternatively, we may want exclude a certain regions of one or other image from the operation with a mask. -In such cases, numbers, arrays and `~astropy.units.Quantity` are insufficient. -Instead it would be better to subtract two `~ndcube.NDCube` objects. -While this is not directly supported, for reasons we will see below, the operation can still be achieved indirectly, as well shall also see. +Sometimes more advanced arithmetic operations are required, for which numbers, arrays and `~astropy.units.Quantity` are insufficient. +These include operations where both operands have: -Why Arithmetic Operations with Coordinate-aware NDData Are Not Directly Supported, and How This Can Be Overcome ---------------------------------------------------------------------------------------------------------------- +- uncertainties which need to be propagated; +- masks that need to be combined: +- units and non-`numpy` data arrays unsuitable for representation as an `~astropy.units.Quantity`, e.g. `dask.array`. + +To achieve these operations, users may want to perform arithmetic operations between `~ndcube.NDCube` instances. +While this is not supported, due to ambiguity in combining potentially different coordinate systems, the above use-cases can be achieved via arithmetic operations between `~ndcube.NDCube` and coordinate-less `astropy.nddata.NDData` or subclasses of the same. + +In the rest of this section, we will elaborate on why arithmetic operations are not directly supported between `~ndcube.NDCube` instances and show how users can convert `~ndcube.NDCube` to a coordinate-less `~astropy.nddata.NDData` instance. +We will then discuss how uncertainties and masks are propagated in such operations, and how to control how masked values are treated. + +Why Arithmetic Operations between NDCubes Are Not Directly Supported, and Alternatives for Achieving the Same Result +-------------------------------------------------------------------------------------------------------------------- The purpose of the `ndcube` package is to support N-dimensional coordinate-aware data astronomical data analysis. Arithmetic operations between two `~ndcube.NDCube` instances (or equivalently, an `~ndcube.NDCube` and another coordinate-aware `~astropy.nddata.NDData` subclass) are therefore not directly supported because of the possibility of supporting non-sensical operations. @@ -344,16 +351,16 @@ In the case of addition and subtraction, the ``fill_value`` should be ``0``. By replacing masked data values with ``0``, these values are effectively not included in the addition, and the data values from ``cube2_nocoords`` are passed into ``new_cube`` unchanged. In the above example, both operands have uncertainties, which means masked uncertainties are propagated through the addition, even though the masked data values have been set to ``0``. -Propagation of masked uncertainties can also be suppressed by setting the optional kwarg, ``fill_uncertainty_value=0``. +Propagation of masked uncertainties can also be suppressed by setting the optional kwarg, ``uncertainty_fill_value=0``. By default, the mask of ``cube_filled`` is not changed, and therefore is incorporated into the mask of ``new_cube``. However, mask propagation can also be suppressed by setting the optional kwarg, ``unmask=True``, which sets ``cube_filled.mask`` to ``False``. -In the case of multiplication and division, a ``fill_value`` of ``1`` will prevent masked values being including in the operations. (Also see, below, the optional use of the ``fill_uncertainty_value`` and ``unmask`` kwargs.) +In the case of multiplication and division, a ``fill_value`` of ``1`` will prevent masked values being including in the operations. (Also see, below, the optional use of the ``uncertainty_fill_value`` and ``unmask`` kwargs.) .. code-block:: python - >>> cube_filled = cube1.fill_masked(1, fill_uncertainty_value=0, unmask=True) + >>> cube_filled = cube1.fill_masked(1, uncertainty_fill_value=0, unmask=True) >>> new_cube = cube_filled * cube2_nocoords By default, `ndcube.NDCube.fill_masked` returns a new `~ndcube.NDCube` instance. From 12faeac1002eb7dcfa93e5e669369644ef30aedf Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Tue, 7 Oct 2025 18:09:53 +0100 Subject: [PATCH 21/26] Update arithmetic docs. --- docs/explaining_ndcube/arithmetic.rst | 168 ++++++++++++++++---------- 1 file changed, 106 insertions(+), 62 deletions(-) diff --git a/docs/explaining_ndcube/arithmetic.rst b/docs/explaining_ndcube/arithmetic.rst index a9d27a169..74bfa9711 100644 --- a/docs/explaining_ndcube/arithmetic.rst +++ b/docs/explaining_ndcube/arithmetic.rst @@ -239,42 +239,37 @@ Otherwise, ``new_cube.uncertainty`` will be set to ``None``. .. _arithmetic_nddata: -Arithmetic Operations with Coordinate-less NDData -================================================= +Arithmetic Operations Between NDCubes +===================================== -Sometimes more advanced arithmetic operations are required, for which numbers, arrays and `~astropy.units.Quantity` are insufficient. -These include operations where both operands have: +Why Arithmetic Operations between NDCubes Are Not Supported Directly (but Are Indirectly) +----------------------------------------------------------------------------------------- -- uncertainties which need to be propagated; -- masks that need to be combined: -- units and non-`numpy` data arrays unsuitable for representation as an `~astropy.units.Quantity`, e.g. `dask.array`. - -To achieve these operations, users may want to perform arithmetic operations between `~ndcube.NDCube` instances. -While this is not supported, due to ambiguity in combining potentially different coordinate systems, the above use-cases can be achieved via arithmetic operations between `~ndcube.NDCube` and coordinate-less `astropy.nddata.NDData` or subclasses of the same. - -In the rest of this section, we will elaborate on why arithmetic operations are not directly supported between `~ndcube.NDCube` instances and show how users can convert `~ndcube.NDCube` to a coordinate-less `~astropy.nddata.NDData` instance. -We will then discuss how uncertainties and masks are propagated in such operations, and how to control how masked values are treated. - -Why Arithmetic Operations between NDCubes Are Not Directly Supported, and Alternatives for Achieving the Same Result --------------------------------------------------------------------------------------------------------------------- - -The purpose of the `ndcube` package is to support N-dimensional coordinate-aware data astronomical data analysis. -Arithmetic operations between two `~ndcube.NDCube` instances (or equivalently, an `~ndcube.NDCube` and another coordinate-aware `~astropy.nddata.NDData` subclass) are therefore not directly supported because of the possibility of supporting non-sensical operations. -(Although, as stated above, they are indirectly supported.) +Arithmetic operations between two `~ndcube.NDCube` instances are not supported directly. +(However, as we shall see, they are supported indirectly.) +This is because of the wide scope for enabling non-sensical coordinate-aware operations. For example, what does it mean to multiply a spectrum and an image? -Getting the difference between two images may make physical sense, but only in certain circumstances. +Getting the difference between two images may make physical sense in certain circumstances. For example, subtracting two sequential images of the same region of the Sun is a common step in many solar image analyses. -But subtracting images of different parts of the sky, e.g. the Sun and the Crab Nebula, does not produce a physically meaningful result. -Even when subtracting two images of the Sun, drift in the telescope's pointing may result in the pixels in each image corresponding to different points on the Sun. +But subtracting images of different parts of the sky, e.g. the Crab and Horseshoe Nebulae, does not produce a physically meaningful result. +Even when subtracting two images of the Sun, drift in the telescope's pointing may result in corresponding pixels representing different points on the Sun. In this case, it is questionable whether even this operation makes physical sense. -Moreover, in all of these cases, it is not at all clear what the resulting WCS object should be. +Moreover, in all of these cases, it is not clear what the resulting WCS object should be. One way to ensure physically meaningful, coordinate-aware arithmetic operations between `~ndcube.NDCube` instances would be to compare their WCS objects are the same within a certain tolerance. -Alternatively, the arithmetic operation could attempt to reproject on `~ndcube.NDCube` to the other's WCS. +Alternatively, the arithmetic operation could attempt to reproject one `~ndcube.NDCube` to the other's WCS. However, these operations can be prohibitively slow and resource-hungry. - Despite this, arithmetic operations between two `~ndcube.NDCube` instances is supported, provided the coordinate-awareness of one is dropped. -A simple solution that satisfies many use-cases is to extract the data (an optionally the unit) from one of the `~ndcube.NDCube` instances and perform the operation as described in the above section on :ref:`arithmetic_standard`: +Below we shall outline two ways of doing this. + +Performing Arithmetic Operations between NDCubes Indirectly +----------------------------------------------------------- + +Extracting One NDCube's Data and Unit +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The simplest way to perform arithmetic operations between `~ndcube.NDCube` instances is to directly combine one with the data (an optionally the unit) of the other. +Thus, the operation can be performed as already described in the above section on :ref:`arithmetic_standard`: .. expanding-code-block:: python :summary: Expand to see definition of cube1 and cube2. @@ -286,31 +281,78 @@ A simple solution that satisfies many use-cases is to extract the data (an optio >>> new_cube = cube1 - cube2.data * cube2.unit -However, this does not allow for the propagation of uncertainties or masks associated with ``cube2``. -Therefore, `~ndcube.NDCube` also support arithmetic operations with instances of `~astropy.nddata.NDData` subclasses whose ``wcs`` attribute is ``None``. -Requiring users to remove coordinates in this way makes them explicitly aware that they are dispensing with coordinate-awareness on one of their operands, and gives them the power to select the one for which this is done. -It also leaves only one WCS involved in the operation, thus removing ambiguity regarding the WCS of the resulting `~ndcube.NDCube`. +Enabling Arithmetic Operations between NDCubes with NDCube.to_nddata +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sometimes, however, more advanced arithmetic operations are required for which numbers, arrays and `~astropy.units.Quantity` are insufficient. +These include cases where both operands have: + +- uncertainties which need to be propagated, and/or; +- masks that need to be combined, and/or; +- units and non-`numpy` data arrays unsuitable for representation as an `~astropy.units.Quantity`, e.g. `dask.array`. + +To achieve these operations, it would be preferable to perform arithmetic operations directly between the `~ndcube.NDCube` instances. +While this is not supported, as already outlined, the same result can be achieved by first dropping the coordinate-awareness of one `~ndcube.NDCube` via the `ndcube.NDCube.to_nddata` method. +The two datasets can then be combined using the standard arithmetic operators. +`ndcube.NDCube.to_nddata` enables the conversion of the `~ndcube.NDCube` instance to any `~astropy.nddata.NDData` subclass, while also enabling the values of specific attributes to be altered during the conversion. +Therefore, arithmetic operations between `~ndcube.NDCube` instances via: + +.. code-block:: python + + >>> new_cube = cube1 + cube2.to_nddata(wcs=None) + +where addition, subtraction, multiplication and division are all enabled by the ``+``, ``-``, ``*``, and ``/`` operators, respectively. + +Note that `~ndcube.NDCube` attributes not supported by the constructor of the output type employed by `ndcube.NDCube.to_nddata` are dropped by the conversion. +Therefore, since `ndcube.NDCube.to_nddata` converts to `~astropy.nddata.NDData` by default, there was no need in the above example to explicitly set `~ndcube.NDCube.extra_coords` and `~ndcube.NDCube.global_coords` to ``None``. +Note that the output type of `ndcube.NDCube.to_nddata` can be controlled via the ``nddata_type`` kwarg. +For example: + + >>> from astropy.nddata import NDDataRef + >>> nddataref2 = cube2.to_nddata(wcs=None, nddata_type=NDDataRef) + >>> print(type(nd) is NDDataRef) + True + +Requiring users to explicitly remove coordinate-awareness makes it clear that coordinates are not combined as part of arithmetic operations. +It also makes it unambiguous which operand's coordinates are maintained through the operation. -Users who would like to drop coordinate-awareness from an `~ndcube.NDCube` can so simply by converting it to an `~astropy.nddata.NDData` and setting the ``wcs`` to ``None``: +`ndcube.NDCube.to_nddata` is not limited to changing/removing the WCS. +The value of any input supported by the ``nddata_type``'s constructor can be altered by setting a kwarg for that input, e.g.: .. code-block:: python - >>> from astropy.nddata import NDData + >>> nddata_ones = cube2.to_nddata(data=np.ones(cube2.data.shape)) + >>> nddata_ones.data + array([[1., 1., 1.], + [1., 1., 1.]]) - >>> cube2_nocoords = NDData(cube2, wcs=None) +Handling of Data, Units and Meta +"""""""""""""""""""""""""""""""" +The treatment of the ``data`` and ``unit`` attributes in operations between `~ndcube.NDCube` and coordinate-less `~astropy.nddata.NDData` subclasses are the same as for arrays and `~astropy.units.Quantity`. +However, only the metadata from the `~ndcube.NDCube` is retained. +This can be updated after the operation, if desired. +For example: + +.. code-block:: python + >>> cube1.meta + {'Description': 'This is example NDCube metadata.'} -Performing Arithmetic Operations with Coordinate-less NDData ------------------------------------------------------------- + >>> cube2.meta["more"] = True + >>> cube2.meta + {'Description': 'This is example NDCube metadata.', 'More': True} -Addition, subtraction, multiplication and division between `~ndcube.NDCube` and coordinate-less `~astropy.nddata.NDData` classes are all supported via the ``+``, ``-``, ``*``, and ``/`` operators. -With respect to the ``data`` and ``unit`` attributes, the behaviors are the same as for arrays and `~astropy.units.Quantity`. -The power of using coordinate-less `~astropy.nddata.NDData` classes is the ability to handle uncertainties and masks. + >>> new_cube = cube1 + cube2.to_nddata(wcs=None) + >>> new_cube.meta + {'Description': 'This is example NDCube metadata.'} -Uncertainty Propagation -*********************** + >>> new_cube.meta.update(cube2.meta) + >>> new_cube.meta + {'Description': 'This is example NDCube metadata.', 'More': True} -The uncertainty resulting from the arithmetic operation depends on the uncertainty types of the operands: +Handling of Uncertainties +""""""""""""""""""""""""" +How uncertainties are handled depends on the uncertainty types of the operands: - ``NDCube.uncertainty`` and ``NDData.uncertainty`` are both ``None`` => ``new_cube.uncertainty`` is ``None``; - ``NDCube`` or ``NDData`` have uncertainty, but not both => the existing uncertainty is assigned to ``new_cube`` as is; @@ -318,22 +360,17 @@ The uncertainty resulting from the arithmetic operation depends on the uncertain * Note that not all uncertainty classes support error propagation, e.g. `~astropy.nddata.UnknownUncertainty`. In such cases, uncertainties are dropped altogether and ``new_cube.uncertainty`` is set to ``None``. -If users would like to remove uncertainty from one of the operands in order to propagate the other without alteration, this can be done before the arithmetic operation via: +If users would like to remove uncertainty from one of the operands in order to propagate the other without alteration, this can be done by casting the `~ndcube.NDCube` to a new instance with the uncertainty set to ``None`` via the `ndcube.NDCube.to_nddata` method before the operation: .. code-block:: python >>> # Remove uncertainty from NDCube - >>> cube1_nouncert = NDCube(cube1, wcs=None) - >>> new_cube = cube1_nouncert + cube2_nocoords - - >>> # Remove uncertainty from coordinate-less NDData - >>> cube2_nocoords_nouncert = NDData(cube2, wcs=None, uncertainty=None) - >>> new_cube = cube1 / cube2_nocoords_nouncert + >>> new_cube = cube1.to_nddata(uncertainty=None, nddata_type=NDCube) + cube2.to_nddata(wcs=None) -Mask Operations and the NDCube.fill_masked Method -************************************************* +Handling of Masks and NDCube.fill_masked Method +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The mask associated with the `~ndcube.NDCube` resulting from the arithmetic operation depends on the mask types of the operands: +The mask resulting from an arithmetic operation between an `~ndcube.NDCube` and coordinate-less `~astropy.nddata.NDData` subclass depends on the mask types of the operands: - ``NDCube.mask`` and ``NDData.mask`` are both ``None`` => ``new_cube.mask`` is ``None``; - ``NDCube`` or ``NDData`` have a mask, but not both => the existing mask is assigned to ``new_cube`` as is; @@ -342,29 +379,36 @@ The mask associated with the `~ndcube.NDCube` resulting from the arithmetic oper The mask values do not affect the ``data`` values output by the operation. However, in some cases, the mask may be used to identify regions of unreliable data that should not be included in the operation. This can be achieved by altering the masked data values before the operation via the `ndcube.NDCube.fill_masked` method. -In the case of addition and subtraction, the ``fill_value`` should be ``0``. + +The NDCube.fill_masked Method +""""""""""""""""""""""""""""" + +The `ndcube.NDCube.fill_masked` method returns a new `~ndcube.NDCube` instance with masked data elements (and optionally uncertainty elements) replaced with a user-defined ``fill_value``. +This can be used to effectively exclude masked values from an arithmetic operation by replacing masked values with the identity value for that operation. +For example, in the case of addition and subtraction, the identity ``fill_value`` is ``0``. .. code-block:: python - >>> cube_filled = cube1.fill_masked(0) - >>> new_cube = cube_filled + cube2_nocoords + >>> new_cube = cube1.fill_masked(0) + cube2_nocoords -By replacing masked data values with ``0``, these values are effectively not included in the addition, and the data values from ``cube2_nocoords`` are passed into ``new_cube`` unchanged. -In the above example, both operands have uncertainties, which means masked uncertainties are propagated through the addition, even though the masked data values have been set to ``0``. -Propagation of masked uncertainties can also be suppressed by setting the optional kwarg, ``uncertainty_fill_value=0``. +In this example, both operands have uncertainties, which means masked uncertainties are propagated through the operation, even though the masked data values have been set to ``0``. +Propagation of masked uncertainties can also be suppressed by setting the optional kwarg, ``uncertainty_fill_value`` to ``0``. -By default, the mask of ``cube_filled`` is not changed, and therefore is incorporated into the mask of ``new_cube``. -However, mask propagation can also be suppressed by setting the optional kwarg, ``unmask=True``, which sets ``cube_filled.mask`` to ``False``. +By default, the mask of the filled `~ndcube.NDCube` cube is not changed, and therefore is incorporated into the mask of ``new_cube``. +However, mask propagation can also be suppressed by unmasking the filled `~ndcube.NDCube`. +This can be done by setting the optional kwarg, ``unmask=True``, in `ndcube.NDCube.fill_masked`, which sets the mask of the filled `~ndcube.NDCube` to ``False``. -In the case of multiplication and division, a ``fill_value`` of ``1`` will prevent masked values being including in the operations. (Also see, below, the optional use of the ``uncertainty_fill_value`` and ``unmask`` kwargs.) +In the case of multiplication and division, the identity ``fill_value`` is ``1``. (Note that in the below example we show the optional use of the ``uncertainty_fill_value`` and ``unmask`` kwargs.) .. code-block:: python >>> cube_filled = cube1.fill_masked(1, uncertainty_fill_value=0, unmask=True) >>> new_cube = cube_filled * cube2_nocoords +Note that irrespective of the arithmetic operation, the ``uncertainty_fill_value`` should always be set to ``0`` to avoid propagating masked uncertainties. + By default, `ndcube.NDCube.fill_masked` returns a new `~ndcube.NDCube` instance. -However, in some cases it may be preferable to fill the masked values in-place, for example, because the data within the `~ndcube.NDCube` is very large and users want to control the number of copies in RAM. +However, in some cases it may be preferable to fill the masked values in-place, for example, because the data are very large and users want to control the number of copies in RAM. In this case, the ``fill_in_place`` kwarg can be used. .. code-block:: python From 8f1534feb22d64f235396bb63932cffca5a1b471 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Tue, 7 Oct 2025 18:36:32 +0100 Subject: [PATCH 22/26] Fix undefined variable in arithmetic docs. --- docs/explaining_ndcube/arithmetic.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/explaining_ndcube/arithmetic.rst b/docs/explaining_ndcube/arithmetic.rst index 74bfa9711..7f1e77262 100644 --- a/docs/explaining_ndcube/arithmetic.rst +++ b/docs/explaining_ndcube/arithmetic.rst @@ -389,7 +389,7 @@ For example, in the case of addition and subtraction, the identity ``fill_value` .. code-block:: python - >>> new_cube = cube1.fill_masked(0) + cube2_nocoords + >>> new_cube = cube1.fill_masked(0) + cube2.to_nddata(wcs=None) In this example, both operands have uncertainties, which means masked uncertainties are propagated through the operation, even though the masked data values have been set to ``0``. Propagation of masked uncertainties can also be suppressed by setting the optional kwarg, ``uncertainty_fill_value`` to ``0``. @@ -403,7 +403,7 @@ In the case of multiplication and division, the identity ``fill_value`` is ``1`` .. code-block:: python >>> cube_filled = cube1.fill_masked(1, uncertainty_fill_value=0, unmask=True) - >>> new_cube = cube_filled * cube2_nocoords + >>> new_cube = cube_filled * cube2.to_nddata(wcs=None) Note that irrespective of the arithmetic operation, the ``uncertainty_fill_value`` should always be set to ``0`` to avoid propagating masked uncertainties. @@ -414,4 +414,4 @@ In this case, the ``fill_in_place`` kwarg can be used. .. code-block:: python >>> cube1.fill_masked(0, fill_in_place=True) - >>> new_cube = cube1 + cube2_nocoords + >>> new_cube = cube1 + cube2.to_nddata(wcs=None) From d7a595df6d05478b57c7233cc2e51fc9bec946ed Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Tue, 7 Oct 2025 18:46:11 +0100 Subject: [PATCH 23/26] More fixes to arithmetic docs. --- docs/explaining_ndcube/arithmetic.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/explaining_ndcube/arithmetic.rst b/docs/explaining_ndcube/arithmetic.rst index 7f1e77262..4766f45f0 100644 --- a/docs/explaining_ndcube/arithmetic.rst +++ b/docs/explaining_ndcube/arithmetic.rst @@ -310,7 +310,7 @@ For example: >>> from astropy.nddata import NDDataRef >>> nddataref2 = cube2.to_nddata(wcs=None, nddata_type=NDDataRef) - >>> print(type(nd) is NDDataRef) + >>> print(type(nddataref2) is NDDataRef) True Requiring users to explicitly remove coordinate-awareness makes it clear that coordinates are not combined as part of arithmetic operations. @@ -340,7 +340,7 @@ For example: >>> cube2.meta["more"] = True >>> cube2.meta - {'Description': 'This is example NDCube metadata.', 'More': True} + {'Description': 'This is example NDCube metadata.', 'more': True} >>> new_cube = cube1 + cube2.to_nddata(wcs=None) >>> new_cube.meta @@ -348,7 +348,7 @@ For example: >>> new_cube.meta.update(cube2.meta) >>> new_cube.meta - {'Description': 'This is example NDCube metadata.', 'More': True} + {'Description': 'This is example NDCube metadata.', 'more': True} Handling of Uncertainties """"""""""""""""""""""""" From f943acac578f074ba491179e4240076e727795d3 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Tue, 7 Oct 2025 19:55:21 +0100 Subject: [PATCH 24/26] doc formatting fix. --- docs/explaining_ndcube/arithmetic.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/explaining_ndcube/arithmetic.rst b/docs/explaining_ndcube/arithmetic.rst index 4766f45f0..5fe8012c6 100644 --- a/docs/explaining_ndcube/arithmetic.rst +++ b/docs/explaining_ndcube/arithmetic.rst @@ -289,7 +289,7 @@ These include cases where both operands have: - uncertainties which need to be propagated, and/or; - masks that need to be combined, and/or; -- units and non-`numpy` data arrays unsuitable for representation as an `~astropy.units.Quantity`, e.g. `dask.array`. +- units and non-`numpy` data arrays unsuitable for representation as an `~astropy.units.Quantity`, e.g. ``dask.array``. To achieve these operations, it would be preferable to perform arithmetic operations directly between the `~ndcube.NDCube` instances. While this is not supported, as already outlined, the same result can be achieved by first dropping the coordinate-awareness of one `~ndcube.NDCube` via the `ndcube.NDCube.to_nddata` method. From cb3c15d71d88b03826b811e73fdfba04dadb6ed4 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Wed, 8 Oct 2025 09:29:23 +0100 Subject: [PATCH 25/26] Fix test of to_nddata to ndcube. --- ndcube/ndcube.py | 2 +- ndcube/tests/test_ndcube.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ndcube/ndcube.py b/ndcube/ndcube.py index 3c55c80dc..737a61395 100644 --- a/ndcube/ndcube.py +++ b/ndcube/ndcube.py @@ -1525,7 +1525,7 @@ def to_nddata(self, *, nddata_type=NDData, **kwargs): new_kwargs = {key.strip("_"): value for key, value in self.__dict__.items()} new_kwargs.update(kwargs) extra_coords, global_coords = None, None - if isinstance(nddata_type, NDCube): + if nddata_type is NDCube: extra_coords = new_kwargs.pop("extra_coords") global_coords = new_kwargs.pop("global_coords") # Inspect call signature of new_nddata class and diff --git a/ndcube/tests/test_ndcube.py b/ndcube/tests/test_ndcube.py index 2f911fb06..0e1e74eda 100644 --- a/ndcube/tests/test_ndcube.py +++ b/ndcube/tests/test_ndcube.py @@ -250,8 +250,9 @@ def test_to_nddata(ndcube_2d_ln_lt): assert (output.data == new_data).all() -def test_to_nddata_type_ndcube(ndcube_2d_ln_lt): - ndc = ndcube_2d_ln_lt +def test_to_nddata_type_ndcube(ndcube_2d_ln_lt_uncert_ec): + ndc = ndcube_2d_ln_lt_uncert_ec + ndc.global_coords.add("wavelength", "em.wl", 100*u.nm) new_data = ndc.data * 2 output = ndc.to_nddata(data=new_data, nddata_type=NDCube) assert type(output) is NDCube From 881168cfd1431b1d1ad74fbab99c0c987e0ca784 Mon Sep 17 00:00:00 2001 From: DanRyanIrish Date: Thu, 9 Oct 2025 10:54:58 +0100 Subject: [PATCH 26/26] Mention motivating use case of arithmetic operations in NDCube.to_nddata docstring. --- ndcube/ndcube.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ndcube/ndcube.py b/ndcube/ndcube.py index 778210d9e..cb2624b2e 100644 --- a/ndcube/ndcube.py +++ b/ndcube/ndcube.py @@ -1506,6 +1506,10 @@ def to_nddata(self, Attribute values can be altered on the output object by setting a kwarg with the new value, e.g. ``data=new_data``. Any attributes not supported by the new class (``nddata_type``), will be discarded. + A motivating use case for this method is in enabling arithmetic operations between + `~ndcube.NDCube` instances by removing coordinate-awareness. See the section of the + ndcube documentation on + 'Enabling Arithmetic Operations between NDCubes with NDCube.to_nddata'. Parameters ----------