From fbf5fe1c05ae4346d68b2c0465a6c9acbf66758b Mon Sep 17 00:00:00 2001 From: Alasdair Wilson Date: Thu, 30 Oct 2025 12:54:00 +0000 Subject: [PATCH 01/10] docs: add example using reproject to upscale resolution --- .../upscaling_resolution_via_reproject.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 examples/upscaling_resolution_via_reproject.py diff --git a/examples/upscaling_resolution_via_reproject.py b/examples/upscaling_resolution_via_reproject.py new file mode 100644 index 000000000..800490351 --- /dev/null +++ b/examples/upscaling_resolution_via_reproject.py @@ -0,0 +1,58 @@ +""" +===================================== +Upscaling the resolution of an NDCube +===================================== + +This example shows how to increase the resolution of an NDCube by reprojecting to a finer grid. +""" + +import matplotlib.pyplot as plt + +from astropy.io import fits +from astropy.wcs import WCS + +from sunpy.data.sample import AIA_171_IMAGE +from sunpy.visualization.colormaps import cm + +from ndcube import NDCube + +############################################################################## +# We start by creating an NDCube from sample solar data provided by SunPy. +# Here we use an AIA 171 image, but the same approach can be applied to other datasets, including those with non celestial axes. + +hdul = fits.open(AIA_171_IMAGE) +cube = NDCube(hdul[1].data, WCS(hdul[1].header)) + +########################################################################### +# Next, we define a new WCS with a finer pixel scale, note that while it is obvious that the CDELT values are changed to reflect the finer scale, +# the CRPIX values also need to be adjusted as the reference pixel position changes with the new scale. +# You can use any value for the scale factor, including non-integer values, greater or less than 1. +# You can also scale each axis independently. + +scale_factor = 1.5 +new_wcs = cube.wcs.deepcopy() +new_wcs.wcs.cdelt /= scale_factor +new_wcs.wcs.crpix *= scale_factor + +########################################################################### +# Now we can reproject the original cube to the new WCS with higher resolution. +new_shape = tuple(int(s * scale_factor) for s in cube.data.shape) +reprojected_cube = cube.reproject_to(new_wcs, shape_out=new_shape) + +########################################################################### +# Our new NDCube now has a higher resolution. +# We can compare the shapes of the original and reprojected cubes with new pixel axes. +print(cube.data.shape, reprojected_cube.data.shape) + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 8)) + +ax1.imshow(cube.data, origin='lower', cmap=cm.sdoaia171, vmin=10, vmax=10000) +ax1.set_title('Original') +ax1.set_xlabel('X [pixel]') +ax1.set_ylabel('Y [pixel]') +ax2.imshow(reprojected_cube.data, origin='lower', cmap=cm.sdoaia171, vmin=10, vmax=10000) +ax2.set_title('Reprojected (Upscaled)') +ax2.set_xlabel('X [pixel]') +ax2.set_ylabel('Y [pixel]') +plt.tight_layout() +plt.show() From 3c2e1424b04513e4a4cc364d13df04394dc355ea Mon Sep 17 00:00:00 2001 From: Alasdair Wilson Date: Thu, 30 Oct 2025 12:54:45 +0000 Subject: [PATCH 02/10] tests: added clearer error message for attempting to rebin with scale factors greater than 1 --- ndcube/ndcube.py | 4 +++- ndcube/tests/test_ndcube_reproject_and_rebin.py | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ndcube/ndcube.py b/ndcube/ndcube.py index fb0ffc79a..a9bcbb0ed 100644 --- a/ndcube/ndcube.py +++ b/ndcube/ndcube.py @@ -1290,9 +1290,11 @@ def my_propagate(uncertainty, data, mask, **kwargs): bin_shape[bin_shape == -1] = np.array(data_shape)[bin_shape == -1] if (bin_shape < 0).any(): raise ValueError("bin_shape should not be less than -1.") + if np.any(bin_shape > data_shape): + raise ValueError("bin_shape cannot be larger than data shape in any dimension.") if (np.mod(data_shape, bin_shape) != 0).any(): raise ValueError( - "bin shape must be an integer fraction of the data shape in each dimension. " + "bin_shape must be an integer fraction of the data shape in each dimension. " f"data shape: {data_shape}; bin shape: {bin_shape}" ) diff --git a/ndcube/tests/test_ndcube_reproject_and_rebin.py b/ndcube/tests/test_ndcube_reproject_and_rebin.py index 9e0c90ecb..b91779e51 100644 --- a/ndcube/tests/test_ndcube_reproject_and_rebin.py +++ b/ndcube/tests/test_ndcube_reproject_and_rebin.py @@ -258,13 +258,20 @@ def test_rebin_some_masked_uncerts_exclude_masked_values(ndcube_2d_ln_lt_mask_un def test_rebin_errors(ndcube_3d_l_ln_lt_ectime): cube = ndcube_3d_l_ln_lt_ectime # Wrong number of axes in bin_shape) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="bin_shape must have an entry for each"): cube.rebin((2,)) + # bin shape shouldn't have any negatives + with pytest.raises(ValueError, match="bin_shape should not be less than -1"): + cube.rebin((2, -2, 1)) # bin_shape not integer multiple of data shape. - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="bin_shape must be an integer fraction"): cube.rebin((9, 2, 1)) + # bin_shape larger than data shape. + with pytest.raises(ValueError, match="bin_shape cannot be larger than data shape"): + cube.rebin((20, 2, 1)) + def test_rebin_no_propagate(ndcube_2d_ln_lt_mask_uncert): # Execute rebin. From 5bffd76adcd813ecfb71497e78a2df41c276de7c Mon Sep 17 00:00:00 2001 From: Alasdair Wilson Date: Thu, 30 Oct 2025 13:14:15 +0000 Subject: [PATCH 03/10] changelog --- changelog/893.doc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/893.doc.rst diff --git a/changelog/893.doc.rst b/changelog/893.doc.rst new file mode 100644 index 000000000..52382ce03 --- /dev/null +++ b/changelog/893.doc.rst @@ -0,0 +1 @@ +Added an example (:ref:`sphx_glr_generated_gallery_upscaling_resolution_via_reproject.py`) showing how to change resolution of an NDCube using `~ndcube.NDCube.reproject`. From c7b078139140f47fdc5ecdfa1d5307c988ec3d3f Mon Sep 17 00:00:00 2001 From: Stuart Mumford Date: Thu, 30 Oct 2025 13:22:45 +0000 Subject: [PATCH 04/10] Update changelog/893.doc.rst --- changelog/893.doc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/893.doc.rst b/changelog/893.doc.rst index 52382ce03..2dfa20d73 100644 --- a/changelog/893.doc.rst +++ b/changelog/893.doc.rst @@ -1 +1 @@ -Added an example (:ref:`sphx_glr_generated_gallery_upscaling_resolution_via_reproject.py`) showing how to change resolution of an NDCube using `~ndcube.NDCube.reproject`. +Added an example (:ref:`sphx_glr_generated_gallery_upscaling_resolution_via_reproject.py`) showing how to change resolution of an NDCube using `~ndcube.NDCube.reproject_to`. From 7e6deaadc5e38623428c78b8ee0bb65ff30386ef Mon Sep 17 00:00:00 2001 From: Alasdair Wilson Date: Fri, 31 Oct 2025 14:13:33 +0000 Subject: [PATCH 05/10] slight touchups to language --- .../upscaling_resolution_via_reproject.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/examples/upscaling_resolution_via_reproject.py b/examples/upscaling_resolution_via_reproject.py index 800490351..05878bb3d 100644 --- a/examples/upscaling_resolution_via_reproject.py +++ b/examples/upscaling_resolution_via_reproject.py @@ -16,8 +16,8 @@ from ndcube import NDCube -############################################################################## -# We start by creating an NDCube from sample solar data provided by SunPy. +############################################################################ +# We start by creating an NDCube from sample solar data provided by `sunpy``. # Here we use an AIA 171 image, but the same approach can be applied to other datasets, including those with non celestial axes. hdul = fits.open(AIA_171_IMAGE) @@ -25,31 +25,36 @@ ########################################################################### # Next, we define a new WCS with a finer pixel scale, note that while it is obvious that the CDELT values are changed to reflect the finer scale, -# the CRPIX values also need to be adjusted as the reference pixel position changes with the new scale. -# You can use any value for the scale factor, including non-integer values, greater or less than 1. +# the CRPIX values also need to be adjusted as the reference pixel position is different on the new pixel scale. +# You can scale the axes by any amount, including non-integer values, greater or less than 1. # You can also scale each axis independently. +# Here we scale both spatial axes by a factor of 1.5. scale_factor = 1.5 + new_wcs = cube.wcs.deepcopy() new_wcs.wcs.cdelt /= scale_factor new_wcs.wcs.crpix *= scale_factor +new_shape = tuple(int(s * scale_factor) for s in cube.data.shape) ########################################################################### -# Now we can reproject the original cube to the new WCS with higher resolution. -new_shape = tuple(int(s * scale_factor) for s in cube.data.shape) +# We reproject the original cube to the new WCS. reprojected_cube = cube.reproject_to(new_wcs, shape_out=new_shape) ########################################################################### # Our new NDCube now has a higher resolution. -# We can compare the shapes of the original and reprojected cubes with new pixel axes. print(cube.data.shape, reprojected_cube.data.shape) +########################################################################### +# We can plot the original and reprojected cubes side by side. + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 8)) ax1.imshow(cube.data, origin='lower', cmap=cm.sdoaia171, vmin=10, vmax=10000) ax1.set_title('Original') ax1.set_xlabel('X [pixel]') ax1.set_ylabel('Y [pixel]') + ax2.imshow(reprojected_cube.data, origin='lower', cmap=cm.sdoaia171, vmin=10, vmax=10000) ax2.set_title('Reprojected (Upscaled)') ax2.set_xlabel('X [pixel]') From d23357b83a1435a23f53d37c972676cebb55196f Mon Sep 17 00:00:00 2001 From: Alasdair Wilson Date: Mon, 3 Nov 2025 09:54:27 +0000 Subject: [PATCH 06/10] oops --- examples/upscaling_resolution_via_reproject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/upscaling_resolution_via_reproject.py b/examples/upscaling_resolution_via_reproject.py index 05878bb3d..b675b9168 100644 --- a/examples/upscaling_resolution_via_reproject.py +++ b/examples/upscaling_resolution_via_reproject.py @@ -17,7 +17,7 @@ from ndcube import NDCube ############################################################################ -# We start by creating an NDCube from sample solar data provided by `sunpy``. +# We start by creating an NDCube from sample solar data provided by SunPy. # Here we use an AIA 171 image, but the same approach can be applied to other datasets, including those with non celestial axes. hdul = fits.open(AIA_171_IMAGE) From ec2757c0b74920a469a2441181589a580962220f Mon Sep 17 00:00:00 2001 From: Alasdair Wilson Date: Tue, 4 Nov 2025 20:03:25 +0000 Subject: [PATCH 07/10] changed from upsacling to simply changing res --- ...reproject.py => changing_resolution_via_reproject.py} | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) rename examples/{upscaling_resolution_via_reproject.py => changing_resolution_via_reproject.py} (89%) diff --git a/examples/upscaling_resolution_via_reproject.py b/examples/changing_resolution_via_reproject.py similarity index 89% rename from examples/upscaling_resolution_via_reproject.py rename to examples/changing_resolution_via_reproject.py index b675b9168..2d9f7274c 100644 --- a/examples/upscaling_resolution_via_reproject.py +++ b/examples/changing_resolution_via_reproject.py @@ -1,9 +1,9 @@ """ ===================================== -Upscaling the resolution of an NDCube +Changing the resolution of an NDCube ===================================== -This example shows how to increase the resolution of an NDCube by reprojecting to a finer grid. +This example shows how to change the resolution of an NDCube by reprojecting to a finer grid. """ import matplotlib.pyplot as plt @@ -20,8 +20,9 @@ # We start by creating an NDCube from sample solar data provided by SunPy. # Here we use an AIA 171 image, but the same approach can be applied to other datasets, including those with non celestial axes. -hdul = fits.open(AIA_171_IMAGE) -cube = NDCube(hdul[1].data, WCS(hdul[1].header)) +image_data = fits.getdata(AIA_171_IMAGE) +image_header = fits.getheader(AIA_171_IMAGE) +cube = NDCube(image_data, WCS(image_header)) ########################################################################### # Next, we define a new WCS with a finer pixel scale, note that while it is obvious that the CDELT values are changed to reflect the finer scale, From 012f324eeded71be680d3b11931c65260aacace5 Mon Sep 17 00:00:00 2001 From: Alasdair Wilson Date: Tue, 4 Nov 2025 20:11:02 +0000 Subject: [PATCH 08/10] updated c hangelog ref to new file name --- changelog/893.doc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/893.doc.rst b/changelog/893.doc.rst index 2dfa20d73..181b0184e 100644 --- a/changelog/893.doc.rst +++ b/changelog/893.doc.rst @@ -1 +1 @@ -Added an example (:ref:`sphx_glr_generated_gallery_upscaling_resolution_via_reproject.py`) showing how to change resolution of an NDCube using `~ndcube.NDCube.reproject_to`. +Added an example (:ref:`sphx_glr_generated_gallery_changing_resolution_via_reproject.py`) showing how to change resolution of an NDCube using `~ndcube.NDCube.reproject_to`. From 6fa2a7dec5ca4890e779622ae76e5c291a991616 Mon Sep 17 00:00:00 2001 From: Alasdair Wilson <60351846+alasdairwilson@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:15:30 +0100 Subject: [PATCH 09/10] Apply suggestion from @Cadair Co-authored-by: Stuart Mumford --- examples/changing_resolution_via_reproject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/changing_resolution_via_reproject.py b/examples/changing_resolution_via_reproject.py index 2d9f7274c..3d87599e3 100644 --- a/examples/changing_resolution_via_reproject.py +++ b/examples/changing_resolution_via_reproject.py @@ -20,8 +20,8 @@ # We start by creating an NDCube from sample solar data provided by SunPy. # Here we use an AIA 171 image, but the same approach can be applied to other datasets, including those with non celestial axes. -image_data = fits.getdata(AIA_171_IMAGE) -image_header = fits.getheader(AIA_171_IMAGE) +image_data = fits.getdata(AIA_171_IMAGE, ext=1) +image_header = fits.getheader(AIA_171_IMAGE, ext=1) cube = NDCube(image_data, WCS(image_header)) ########################################################################### From 524ece7b9c9e9e4f9b0cdfcf78086d689ae48b78 Mon Sep 17 00:00:00 2001 From: Alasdair Wilson Date: Tue, 2 Jun 2026 11:38:57 +0100 Subject: [PATCH 10/10] add a warning to point users to rebin also changed prose surrounding example being a different resolution rather than upscaled --- examples/changing_resolution_via_reproject.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/changing_resolution_via_reproject.py b/examples/changing_resolution_via_reproject.py index 3d87599e3..afa80ade9 100644 --- a/examples/changing_resolution_via_reproject.py +++ b/examples/changing_resolution_via_reproject.py @@ -3,7 +3,15 @@ Changing the resolution of an NDCube ===================================== -This example shows how to change the resolution of an NDCube by reprojecting to a finer grid. +This example shows how to change the sampling of an NDCube by reprojecting to a different grid. + +Reprojecting is useful when you need a new WCS or a non-integer change in pixel scale. + +.. warning:: + + Reprojecting resamples the data, but it does not make the underlying data intrinsically higher resolution. + If you only need to combine aligned pixels by an integer factor to reduce resolution, + `~ndcube.NDCube.rebin` is usually a better choice because it operates directly on contiguous input pixels. """ import matplotlib.pyplot as plt @@ -18,15 +26,15 @@ ############################################################################ # We start by creating an NDCube from sample solar data provided by SunPy. -# Here we use an AIA 171 image, but the same approach can be applied to other datasets, including those with non celestial axes. +# Here we use an AIA 171 image, but the same approach can be applied to other datasets, including those with non-celestial axes. image_data = fits.getdata(AIA_171_IMAGE, ext=1) image_header = fits.getheader(AIA_171_IMAGE, ext=1) cube = NDCube(image_data, WCS(image_header)) ########################################################################### -# Next, we define a new WCS with a finer pixel scale, note that while it is obvious that the CDELT values are changed to reflect the finer scale, -# the CRPIX values also need to be adjusted as the reference pixel position is different on the new pixel scale. +# Next, we define a new WCS with a finer pixel scale. This changes the sampling of the output array, but it does not make the underlying data intrinsically higher resolution. +# Note that while it is obvious that the CDELT values are changed to reflect the finer scale, the CRPIX values also need to be adjusted as the reference pixel position is different on the new pixel scale. # You can scale the axes by any amount, including non-integer values, greater or less than 1. # You can also scale each axis independently. # Here we scale both spatial axes by a factor of 1.5. @@ -43,7 +51,7 @@ reprojected_cube = cube.reproject_to(new_wcs, shape_out=new_shape) ########################################################################### -# Our new NDCube now has a higher resolution. +# Our new NDCube now has finer sampling. print(cube.data.shape, reprojected_cube.data.shape) ########################################################################### @@ -57,7 +65,7 @@ ax1.set_ylabel('Y [pixel]') ax2.imshow(reprojected_cube.data, origin='lower', cmap=cm.sdoaia171, vmin=10, vmax=10000) -ax2.set_title('Reprojected (Upscaled)') +ax2.set_title('Reprojected to New Resolution') ax2.set_xlabel('X [pixel]') ax2.set_ylabel('Y [pixel]') plt.tight_layout()