Skip to content

Commit 33cd6ff

Browse files
SpacialTreekeflaviche-koch
authored
Add cube mosaicking (#829)
* Add cube mosaicking Created functions to mosaic cubes together. * Made Requested Changes to Pull Request * Delete .gitignore * Revert "Delete .gitignore" This reverts commit 04ccb30. * Update .gitignore * Fix for Colorbar already exists error In the case where the FITSFigure has already has a colorbar, will not attempt to add the colorbar again. * Catch all Exceptions for Quicklook Should print out all exceptions when encountering them in quicklook, and display an image still instead of crashing. * Change to Combine Headers Rename `reproject_together` and change function to use headers instead of cubes. * mosaic_cubes now takes a list Rework of mosaic_cubes to take a list of cubes and mosaic them all together into the same field. * Update cube_utils.py Change function name to combine_headers where it was called * Update combine_headers Fixed problems with find_optimal_celestial_wcs line being passed 3D WCS and wrong order of WCS & shape * Update cube_utils.py Fixed masking out additional cubes * Update test_regrid.py Added test for mosaic_cubes * Update spectral_cube/cube_utils.py Co-authored-by: Adam Ginsburg <[email protected]> * Update spectral_cube/cube_utils.py Co-authored-by: Adam Ginsburg <[email protected]> * Update spectral_cube/cube_utils.py Co-authored-by: Adam Ginsburg <[email protected]> * Update spectral_cube/cube_utils.py Co-authored-by: Adam Ginsburg <[email protected]> * Update cube_utils.py * Update cube_utils.py += was causing an error * Update spectral_cube/tests/test_regrid.py Co-authored-by: Adam Ginsburg <[email protected]> * Update spectral_cube/tests/test_regrid.py Co-authored-by: Adam Ginsburg <[email protected]> * Update spectral_cube/tests/test_regrid.py Co-authored-by: Adam Ginsburg <[email protected]> * Update lower_dimensional_structures.py * Update test_regrid.py add import statement * Update test_regrid.py more specific import statement * Update spectral_cube/tests/test_regrid.py Co-authored-by: Adam Ginsburg <[email protected]> * Update spectral_cube/cube_utils.py block size argument error Co-authored-by: Adam Ginsburg <[email protected]> * Update spectral_cube/tests/test_regrid.py Co-authored-by: Adam Ginsburg <[email protected]> * pass kwargs to reproject * add documentation for reading lower-dimensional objects (#834) * add documentation for reading lower-dimensional objects * Update docs/creating_reading.rst Co-authored-by: Eric Koch <[email protected]> * Update docs/creating_reading.rst Co-authored-by: Eric Koch <[email protected]> * Update docs/creating_reading.rst Co-authored-by: Eric Koch <[email protected]> * Update docs/creating_reading.rst Co-authored-by: Eric Koch <[email protected]> * Update spectral_cube/lower_dimensional_structures.py Co-authored-by: Eric Koch <[email protected]> * Update spectral_cube/lower_dimensional_structures.py * Update spectral_cube/lower_dimensional_structures.py Co-authored-by: Eric Koch <[email protected]> * Update spectral_cube/lower_dimensional_structures.py Co-authored-by: Eric Koch <[email protected]> * fix some simple issues * fix up first part of test (testing WCS) * remove comments * minor whitespace * Fix whitespace * Fix for missing edges * Add fix to other reproject * pass kwargs to mosaic_cubes * fix tests and add some helpful error messages * fix bug uncovered with new test * Update spectral_cube/tests/test_regrid.py Co-authored-by: Eric Koch <[email protected]> * Update spectral_cube/cube_utils.py Co-authored-by: Eric Koch <[email protected]> * resolve comment from e-koch * minor flake8 fixes Co-authored-by: Adam Ginsburg <[email protected]> Co-authored-by: Eric Koch <[email protected]>
1 parent a37fbaa commit 33cd6ff

File tree

4 files changed

+157
-1
lines changed

4 files changed

+157
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,4 @@ docs/api
6060
*/cython_version.py
6161

6262
.tmp
63+
.ipynb_checkpoints

spectral_cube/cube_utils.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import numpy as np
1515
from astropy.wcs.utils import proj_plane_pixel_area
1616
from astropy.wcs import (WCSSUB_SPECTRAL, WCSSUB_LONGITUDE, WCSSUB_LATITUDE)
17+
from astropy.wcs import WCS
1718
from . import wcs_utils
1819
from .utils import FITSWarning, AstropyUserWarning, WCSCelestialError
1920
from astropy import log
@@ -750,3 +751,102 @@ def bunit_converters(obj, unit, equivalencies=(), freq=None):
750751
else:
751752
# Slice along first axis to return a 1D array.
752753
return factors[0]
754+
755+
def combine_headers(header1, header2):
756+
'''
757+
Given two Header objects, this function returns a fits Header of the optimal wcs.
758+
759+
Parameters
760+
----------
761+
header1 : astropy.io.fits.Header
762+
A Header.
763+
header2 : astropy.io.fits.Header
764+
A Header.
765+
766+
Returns
767+
-------
768+
header : astropy.io.fits.Header
769+
A header object of a field containing both initial headers.
770+
771+
'''
772+
773+
from reproject.mosaicking import find_optimal_celestial_wcs
774+
775+
# Get wcs and shape of both headers
776+
w1 = WCS(header1).celestial
777+
s1 = w1.array_shape
778+
w2 = WCS(header2).celestial
779+
s2 = w2.array_shape
780+
781+
# Get the optimal wcs and shape for both fields together
782+
wcs_opt, shape_opt = find_optimal_celestial_wcs([(s1, w1), (s2, w2)], auto_rotate=False)
783+
784+
# Make a new header using the optimal wcs and information from cubes
785+
header = header1.copy()
786+
header['NAXIS'] = 3
787+
header['NAXIS1'] = shape_opt[1]
788+
header['NAXIS2'] = shape_opt[0]
789+
header['NAXIS3'] = header1['NAXIS3']
790+
header.update(wcs_opt.to_header())
791+
header['WCSAXES'] = 3
792+
return header
793+
794+
def mosaic_cubes(cubes, spectral_block_size=100, **kwargs):
795+
'''
796+
This function reprojects cubes onto a common grid and combines them to a single field.
797+
798+
Parameters
799+
----------
800+
cubes : iterable
801+
Iterable list of SpectralCube objects to reproject and add together.
802+
spectral_block_size : int
803+
Block size so that reproject does not run out of memory.
804+
805+
Outputs
806+
-------
807+
cube : SpectralCube
808+
A spectral cube with the list of cubes mosaicked together.
809+
'''
810+
811+
cube1 = cubes[0]
812+
header = cube1.header
813+
814+
# Create a header for a field containing all cubes
815+
for cu in cubes[1:]:
816+
header = combine_headers(header, cu.header)
817+
818+
# Prepare an array and mask for the final cube
819+
shape_opt = (header['NAXIS3'], header['NAXIS2'], header['NAXIS1'])
820+
final_array = np.zeros(shape_opt)
821+
mask_opt = np.zeros(shape_opt[1:])
822+
823+
for cube in cubes:
824+
# Reproject cubes to the header
825+
try:
826+
if spectral_block_size is not None:
827+
cube_repr = cube.reproject(header, block_size=[spectral_block_size, cube.shape[1], cube.shape[2]], **kwargs)
828+
else:
829+
cube_repr = cube.reproject(header, **kwargs)
830+
except TypeError:
831+
warnings.warn("The block_size argument is not accepted by `reproject`. A more recent version may be needed.")
832+
cube_repr = cube.reproject(header, **kwargs)
833+
834+
# Create weighting mask (2D)
835+
mask = (cube_repr[0:1].get_mask_array()[0])
836+
mask_opt += mask.astype(float)
837+
838+
# Go through each slice of the cube, add it to the final array
839+
for ii in range(final_array.shape[0]):
840+
slice1 = np.nan_to_num(cube_repr.unitless_filled_data[ii])
841+
final_array[ii] = final_array[ii] + slice1
842+
843+
# Dividing by the mask throws errors where it is zero
844+
with np.errstate(divide='ignore'):
845+
846+
# Use weighting mask to average where cubes overlap
847+
for ss in range(final_array.shape[0]):
848+
final_array[ss] /= mask_opt
849+
850+
# Create Cube
851+
cube = cube1.__class__(data=final_array * cube1.unit, wcs=WCS(header))
852+
return cube

spectral_cube/spectral_cube.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2701,6 +2701,13 @@ def reproject(self, header, order='bilinear', use_memmap=False,
27012701
shape_out=shape_out,
27022702
order=order,
27032703
**reproj_kwargs)
2704+
if np.all(np.isnan(newcube)):
2705+
raise ValueError("All values in reprojected cube are nan. This can be caused"
2706+
" by an error in which coordinates do not 'round-trip'. Try "
2707+
"setting ``roundtrip_coords=False``. You might also check "
2708+
"whether the WCS transformation produces valid pixel->world "
2709+
"and world->pixel coordinates in each axis."
2710+
)
27042711

27052712
return self._new_cube_with(data=newcube,
27062713
wcs=newwcs,

spectral_cube/tests/test_regrid.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import numpy as np
55
import os
66

7-
from astropy import units as u
7+
from astropy import constants, units as u
88
from astropy import convolution
99
from astropy.wcs import WCS
1010
from astropy import wcs
@@ -25,7 +25,9 @@
2525
from radio_beam import beam, Beam
2626

2727
from .. import SpectralCube
28+
from ..masks import BooleanArrayMask
2829
from ..utils import WCSCelestialError
30+
from ..cube_utils import mosaic_cubes, combine_headers
2931
from .test_spectral_cube import cube_and_raw
3032
from .test_projection import load_projection
3133
from . import path, utilities
@@ -113,6 +115,11 @@ def test_reproject(use_memmap, data_adv, use_dask):
113115
wcs_out.wcs.crval = [134.37608, -31.939241, wcs_in.wcs.crval[2]]
114116
wcs_out.wcs.crpix = [2., 2., wcs_in.wcs.crpix[2]]
115117

118+
# cube is doppler-optical by default, which uses the rest wavelength,
119+
# which isn't auto-computed, resulting in nan pixels in the WCS transform
120+
wcs_out.wcs.restwav = 0.21106114549833
121+
cube._wcs.wcs.restwav = 0.21106114549833
122+
116123
header_out = cube.header
117124
header_out['NAXIS1'] = 4
118125
header_out['NAXIS2'] = 5
@@ -585,3 +592,44 @@ def test_reproject_3D_memory():
585592

586593
assert result.wcs.wcs.crval[0] == 0.001
587594
assert result.wcs.wcs.crpix[0] == 2.
595+
596+
@pytest.mark.parametrize('spectral_block_size,use_memmap', ((None, False),
597+
(100, False),
598+
(None, True),
599+
(100, False),
600+
(1, True),
601+
(1, False),
602+
))
603+
def test_mosaic_cubes(use_memmap, data_adv, use_dask, spectral_block_size):
604+
605+
pytest.importorskip('reproject')
606+
607+
# Read in data to use
608+
cube, data = cube_and_raw(data_adv, use_dask=use_dask)
609+
610+
# cube is doppler-optical by default, which uses the rest wavelength,
611+
# which isn't auto-computed, resulting in nan pixels in the WCS transform
612+
cube._wcs.wcs.restwav = constants.c.to(u.m/u.s).value / cube.wcs.wcs.restfrq
613+
614+
expected_wcs = WCS(combine_headers(cube.header, cube.header)).celestial
615+
616+
# Make two overlapping cubes of the data
617+
part1 = cube[:, :round(cube.shape[1]*2./3.), :]
618+
part2 = cube[:, round(cube.shape[1]/3.):, :]
619+
620+
assert part1.wcs.wcs.restwav != 0
621+
assert part2.wcs.wcs.restwav != 0
622+
623+
result = mosaic_cubes([part1, part2], order='nearest-neighbor',
624+
roundtrip_coords=False,
625+
spectral_block_size=spectral_block_size)
626+
627+
# Check that the shapes are the same
628+
assert result.shape == cube.shape
629+
630+
# Check WCS in reprojected matches wcs_out
631+
# (comparing WCS failed for no reason we could discern)
632+
assert repr(expected_wcs) == repr(result.wcs.celestial)
633+
# Check that values of original and result are comparable
634+
np.testing.assert_almost_equal(result.filled_data[:].value, cube.filled_data[:].value, decimal=3)
635+
# only good to 3 decimal places is not amazing...

0 commit comments

Comments
 (0)