From f35f7bc23968822b13b889bf5336355c8cc58488 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Fri, 26 Jan 2024 14:11:27 -0500 Subject: [PATCH 01/56] Add conical aperture support in the spectral extraction plugin --- .../spectral_extraction.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 26ee9b83ae..c8c35d547d 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -11,6 +11,7 @@ ) from traitlets import Any, Bool, Dict, Float, List, Unicode, observe + from jdaviz.core.custom_traitlets import FloatHandleEmpty from jdaviz.core.events import SnackbarMessage, SliceWavelengthUpdatedMessage from jdaviz.core.registries import tray_registry @@ -77,6 +78,12 @@ class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin, extracted_spec_available = Bool(False).tag(sync=True) overwrite_warn = Bool(False).tag(sync=True) + aperture_method_items = List(['exact', 'subpixel', 'center']).tag(sync=True) + aperture_method_selected = Unicode('exact').tag(sync=True) + cone_aperture_slope = Float().tag(sync=True) + cone_aperture_intercept = Float().tag(sync=True) + cone_aperture_center = Any().tag(sync=True) + # export_enabled controls whether saving to a file is enabled via the UI. This # is a temporary measure to allow server-installations to disable saving server-side until # saving client-side is supported @@ -284,6 +291,45 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): return collapsed_spec + @with_spinner() + def cone_aperture(self): + # Temporarily here until we decide where this code will go permanently + from specutils import Spectrum1D + from photutils.aperture import CircularAperture + spectral_cube = self._app._jdaviz_helper._loaded_flux_cube + nddata = spectral_cube.get_object(cls=NDDataArray) + + # Hardcode to get the DQ array + mask_cube = self.app.data_collection[2].get_object(cls=Spectrum1D, statistic=None) + + masks_exact_values = np.zeros_like(mask_cube.flux.value) + masks_boolean_values = np.zeros_like(mask_cube.flux.value) + + cone_height = len(mask_cube.spectral_axis) + unit = nddata.unit + # This will be set in the .vue file once its location in the plugin is set + self.cone_aperture_slope = .001 + self.cone_aperture_intercept = 1 + self.cone_aperture_center = (25, 25) + + for wavelength in range(1, cone_height): + radius = ((self.cone_aperture_slope * u.pix / unit) * (wavelength - 1) * unit + + (self.cone_aperture_intercept * u.pix)) + aperture = CircularAperture(self.cone_aperture_center, r=radius.value) + slice_mask = aperture.to_mask(method=self.aperture_method_selected).to_image( + (len(mask_cube.flux), len(mask_cube.flux[0]))) + # Calculates the mask array based on what function is selected. This array + # is then added to the larger array that tracks mask values for the entire + # cube + if self.function_selected == 'Min': + masks_exact_values[:, :, wavelength] = slice_mask + masks_boolean_values[:, :, wavelength] = ~(slice_mask < 1) + else: + masks_exact_values[:, :, wavelength] = slice_mask + masks_boolean_values[:, :, wavelength] = ~(slice_mask == 0) + all_masks = NDDataArray(data=masks_exact_values, mask=masks_boolean_values) + return all_masks + def vue_spectral_extraction(self, *args, **kwargs): self.collapse_to_spectrum(add_data=True) From 775a9cadd3a949dab3b2c252d74ef1fda6c06529 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 1 Feb 2024 14:40:35 -0500 Subject: [PATCH 02/56] Rebase and remove commented out code --- jdaviz/configs/cubeviz/helper.py | 1 + jdaviz/configs/cubeviz/plugins/parsers.py | 8 +- .../spectral_extraction.py | 84 +++++++++---------- .../spectral_extraction.vue | 15 ++-- 4 files changed, 57 insertions(+), 51 deletions(-) diff --git a/jdaviz/configs/cubeviz/helper.py b/jdaviz/configs/cubeviz/helper.py index f059bb87e6..cffb78069a 100644 --- a/jdaviz/configs/cubeviz/helper.py +++ b/jdaviz/configs/cubeviz/helper.py @@ -24,6 +24,7 @@ class Cubeviz(ImageConfigHelper, LineListMixin): _loaded_flux_cube = None _loaded_uncert_cube = None + _loaded_mask_cube = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/jdaviz/configs/cubeviz/plugins/parsers.py b/jdaviz/configs/cubeviz/plugins/parsers.py index 8d14a575ac..7903dc0b6d 100644 --- a/jdaviz/configs/cubeviz/plugins/parsers.py +++ b/jdaviz/configs/cubeviz/plugins/parsers.py @@ -319,6 +319,8 @@ def _parse_jwst_s3d(app, hdulist, data_label, ext='SCI', app._jdaviz_helper._loaded_flux_cube = app.data_collection[data_label] elif data_type == 'uncert': app._jdaviz_helper._loaded_uncert_cube = app.data_collection[data_label] + elif data_type == 'mask': + app._jdaviz_helper._loaded_mask_cube = app.data_collection[data_label] def _parse_esa_s3d(app, hdulist, data_label, ext='DATA', flux_viewer_reference_name=None, @@ -368,8 +370,10 @@ def _parse_esa_s3d(app, hdulist, data_label, ext='DATA', flux_viewer_reference_n if data_type == 'flux': app._jdaviz_helper._loaded_flux_cube = app.data_collection[data_label] - if data_type == 'uncert': + elif data_type == 'uncert': app._jdaviz_helper._loaded_uncert_cube = app.data_collection[data_label] + elif data_type == 'mask': + app._jdaviz_helper._loaded_mask_cube = app.data_collection[data_label] def _parse_spectrum1d_3d(app, file_obj, data_label=None, @@ -470,6 +474,8 @@ def _parse_ndarray(app, file_obj, data_label=None, data_type=None, elif data_type == 'uncert': app.add_data_to_viewer(uncert_viewer_reference_name, data_label) app._jdaviz_helper._loaded_uncert_cube = app.data_collection[data_label] + if data_type == 'mask': + app._jdaviz_helper._loaded_mask_cube = app.data_collection[data_label] def _parse_gif(app, file_obj, data_label=None, flux_viewer_reference_name=None, diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index c8c35d547d..103df7fd9d 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -10,7 +10,8 @@ NDDataArray, StdDevUncertainty, NDUncertainty ) from traitlets import Any, Bool, Dict, Float, List, Unicode, observe - +from photutils.aperture import CircularAperture +from specutils import Spectrum1D from jdaviz.core.custom_traitlets import FloatHandleEmpty from jdaviz.core.events import SnackbarMessage, SliceWavelengthUpdatedMessage @@ -54,9 +55,9 @@ class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin, uses_active_status = Bool(True).tag(sync=True) # feature flag for cone support - dev_cone_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring - dev_bg_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring - dev_subpixel_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring + dev_cone_support = Bool(True).tag(sync=True) # when enabling: add entries to docstring + dev_bg_support = Bool(True).tag(sync=True) # when enabling: add entries to docstring + dev_subpixel_support = Bool(True).tag(sync=True) # when enabling: add entries to docstring active_step = Unicode().tag(sync=True) @@ -70,7 +71,9 @@ class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin, bg_scale_factor = Float(1).tag(sync=True) bg_wavelength_dependent = Bool(False).tag(sync=True) - subpixel = Bool(False).tag(sync=True) + # subpixel = Bool(False).tag(sync=True) + # aperture_masking_methods = List(['exact', 'subpixel', 'center']).tag(sync=True) + # aperture_masking_method_selected = Unicode('exact').tag(sync=True) function_items = List().tag(sync=True) function_selected = Unicode('Sum').tag(sync=True) @@ -80,9 +83,9 @@ class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin, aperture_method_items = List(['exact', 'subpixel', 'center']).tag(sync=True) aperture_method_selected = Unicode('exact').tag(sync=True) - cone_aperture_slope = Float().tag(sync=True) - cone_aperture_intercept = Float().tag(sync=True) - cone_aperture_center = Any().tag(sync=True) + # cone_aperture_slope = Float().tag(sync=True) + # cone_aperture_intercept = Float().tag(sync=True) + # cone_aperture_center = Any().tag(sync=True) # export_enabled controls whether saving to a file is enabled via the UI. This # is a temporary measure to allow server-installations to disable saving server-side until @@ -148,7 +151,7 @@ def user_api(self): if self.dev_bg_support: expose += ['background', 'bg_wavelength_dependent'] if self.dev_subpixel_support: - expose += ['subpixel'] + expose += ['aperture_method_items', 'aperture_method_selected'] return PluginUserApi(self, expose=expose) @@ -217,17 +220,27 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): # defaults to ``No Subset``). Since the Cubeviz parser puts the fluxes # and uncertainties in different glue Data objects, we translate the spectral # cube and its uncertainties into separate NDDataArrays, then combine them: - if self.aperture.selected != self.aperture.default_text: + if self.aperture.selected != self.aperture.default_text and self.wavelength_dependent: nddata = spectral_cube.get_subset_object( subset_id=self.aperture.selected, cls=NDDataArray ) uncertainties = uncert_cube.get_subset_object( subset_id=self.aperture.selected, cls=StdDevUncertainty ) + mask = self.cone_aperture() + + elif self.aperture.selected != self.aperture.default_text: + nddata = spectral_cube.get_subset_object( + subset_id=self.aperture.selected, cls=NDDataArray + ) + uncertainties = uncert_cube.get_subset_object( + subset_id=self.aperture.selected, cls=StdDevUncertainty + ) + mask = nddata.mask else: nddata = spectral_cube.get_object(cls=NDDataArray) uncertainties = uncert_cube.get_object(cls=StdDevUncertainty) - + mask = nddata.mask # Use the spectral coordinate from the WCS: if '_orig_spec' in spectral_cube.meta: wcs = spectral_cube.meta['_orig_spec'].wcs.spectral @@ -235,7 +248,6 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): wcs = spectral_cube.coords.spectral flux = nddata.data << nddata.unit - mask = nddata.mask nddata_reshaped = NDDataArray( flux, mask=mask, uncertainty=uncertainties, wcs=wcs, meta=nddata.meta @@ -293,42 +305,26 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): @with_spinner() def cone_aperture(self): - # Temporarily here until we decide where this code will go permanently - from specutils import Spectrum1D - from photutils.aperture import CircularAperture - spectral_cube = self._app._jdaviz_helper._loaded_flux_cube - nddata = spectral_cube.get_object(cls=NDDataArray) - - # Hardcode to get the DQ array - mask_cube = self.app.data_collection[2].get_object(cls=Spectrum1D, statistic=None) - - masks_exact_values = np.zeros_like(mask_cube.flux.value) + # Retrieve mask cube and create array to represent the cone mask + mask_cube = self._app._jdaviz_helper._loaded_mask_cube.get_object(cls=Spectrum1D, + statistic=None) masks_boolean_values = np.zeros_like(mask_cube.flux.value) - cone_height = len(mask_cube.spectral_axis) - unit = nddata.unit - # This will be set in the .vue file once its location in the plugin is set - self.cone_aperture_slope = .001 - self.cone_aperture_intercept = 1 - self.cone_aperture_center = (25, 25) - - for wavelength in range(1, cone_height): - radius = ((self.cone_aperture_slope * u.pix / unit) * (wavelength - 1) * unit - + (self.cone_aperture_intercept * u.pix)) - aperture = CircularAperture(self.cone_aperture_center, r=radius.value) + # Center is reverse coordinates + center = (self.aperture.selected_spatial_region.center.y, self.aperture.selected_spatial_region.center.x) + # Replace with code for retrieving display_unit in cubeviz when it is available + display_unit = u.um + + # Loop through cube and create cone aperture at each wavelength. Then convert that to a + # mask using the selected aperture method and add that to a mask cube. + for index, wavelength in enumerate(mask_cube.spectral_axis): + radius = ((wavelength.to(display_unit).value / self.reference_wavelength) * + self.aperture.selected_spatial_region.radius) + aperture = CircularAperture(center, r=radius) slice_mask = aperture.to_mask(method=self.aperture_method_selected).to_image( (len(mask_cube.flux), len(mask_cube.flux[0]))) - # Calculates the mask array based on what function is selected. This array - # is then added to the larger array that tracks mask values for the entire - # cube - if self.function_selected == 'Min': - masks_exact_values[:, :, wavelength] = slice_mask - masks_boolean_values[:, :, wavelength] = ~(slice_mask < 1) - else: - masks_exact_values[:, :, wavelength] = slice_mask - masks_boolean_values[:, :, wavelength] = ~(slice_mask == 0) - all_masks = NDDataArray(data=masks_exact_values, mask=masks_boolean_values) - return all_masks + masks_boolean_values[:, :, index] = ~(slice_mask > 0) + return masks_boolean_values def vue_spectral_extraction(self, *args, **kwargs): self.collapse_to_spectrum(add_data=True) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue index d807663f3d..8c5de6bee4 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue @@ -134,12 +134,15 @@ && (bg_selected === 'None' || bg_selected_validity.is_aperture) && dev_subpixel_support"> - - + From a1efbc51abe2a3d399044cd886c72d7a249dc449 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 1 Feb 2024 14:52:40 -0500 Subject: [PATCH 03/56] Fix style --- .../cubeviz/plugins/spectral_extraction/spectral_extraction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 103df7fd9d..fecf1bffd3 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -311,7 +311,8 @@ def cone_aperture(self): masks_boolean_values = np.zeros_like(mask_cube.flux.value) # Center is reverse coordinates - center = (self.aperture.selected_spatial_region.center.y, self.aperture.selected_spatial_region.center.x) + center = (self.aperture.selected_spatial_region.center.y, + self.aperture.selected_spatial_region.center.x) # Replace with code for retrieving display_unit in cubeviz when it is available display_unit = u.um From 2e523b353e44e2aa3292f4c0a749b132fc3997bd Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 6 Feb 2024 09:04:33 -0500 Subject: [PATCH 04/56] Add test --- jdaviz/configs/cubeviz/plugins/parsers.py | 6 ++- .../spectral_extraction.py | 13 ++----- .../tests/test_spectral_extraction.py | 38 +++++++++++++++++++ 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/parsers.py b/jdaviz/configs/cubeviz/plugins/parsers.py index 7903dc0b6d..708144488a 100644 --- a/jdaviz/configs/cubeviz/plugins/parsers.py +++ b/jdaviz/configs/cubeviz/plugins/parsers.py @@ -254,7 +254,7 @@ def _parse_hdulist(app, hdulist, file_name=None, if data_type == 'mask': # We no longer auto-populate the mask cube into a viewer - pass + app._jdaviz_helper._loaded_mask_cube = app.data_collection[data_label] elif data_type == 'uncert': app.add_data_to_viewer(uncert_viewer_reference_name, data_label) @@ -426,7 +426,9 @@ def _parse_spectrum1d_3d(app, file_obj, data_label=None, elif attr == 'uncertainty': app.add_data_to_viewer(uncert_viewer_reference_name, cur_data_label) app._jdaviz_helper._loaded_uncert_cube = app.data_collection[cur_data_label] - # We no longer auto-populate the mask cube into a viewer + elif attr == 'mask': + # We no longer auto-populate the mask cube into a viewer + app._jdaviz_helper._loaded_mask_cube = app.data_collection[cur_data_label] def _parse_spectrum1d(app, file_obj, data_label=None, spectrum_viewer_reference_name=None): diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index fecf1bffd3..1022db7b56 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -71,10 +71,6 @@ class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin, bg_scale_factor = Float(1).tag(sync=True) bg_wavelength_dependent = Bool(False).tag(sync=True) - # subpixel = Bool(False).tag(sync=True) - # aperture_masking_methods = List(['exact', 'subpixel', 'center']).tag(sync=True) - # aperture_masking_method_selected = Unicode('exact').tag(sync=True) - function_items = List().tag(sync=True) function_selected = Unicode('Sum').tag(sync=True) filename = Unicode().tag(sync=True) @@ -83,9 +79,6 @@ class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin, aperture_method_items = List(['exact', 'subpixel', 'center']).tag(sync=True) aperture_method_selected = Unicode('exact').tag(sync=True) - # cone_aperture_slope = Float().tag(sync=True) - # cone_aperture_intercept = Float().tag(sync=True) - # cone_aperture_center = Any().tag(sync=True) # export_enabled controls whether saving to a file is enabled via the UI. This # is a temporary measure to allow server-installations to disable saving server-side until @@ -308,13 +301,15 @@ def cone_aperture(self): # Retrieve mask cube and create array to represent the cone mask mask_cube = self._app._jdaviz_helper._loaded_mask_cube.get_object(cls=Spectrum1D, statistic=None) + flux_cube = self._app._jdaviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, + statistic=None) masks_boolean_values = np.zeros_like(mask_cube.flux.value) # Center is reverse coordinates center = (self.aperture.selected_spatial_region.center.y, self.aperture.selected_spatial_region.center.x) - # Replace with code for retrieving display_unit in cubeviz when it is available - display_unit = u.um + # TODO: Replace with code for retrieving display_unit in cubeviz when it is available + display_unit = flux_cube.spectral_axis.unit # Loop through cube and create cone aperture at each wavelength. Then convert that to a # mask using the selected aperture method and add that to a mask cube. diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 2f6f35b444..99e1ad9ca9 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -231,3 +231,41 @@ def test_aperture_markers(cubeviz_helper, spectrum1d_cube): extract_plg._obj.vue_adopt_slice_as_reference() extract_plg._obj.vue_goto_reference_wavelength() assert slice_plg.slice == 1 + + +def test_cone_aperture(cubeviz_helper, spectrum1d_cube_larger): + from specutils import Spectrum1D + cubeviz_helper.load_data(spectrum1d_cube_larger) + cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(1, 1), radius=0.7)]) + + mask_cube = Spectrum1D(flux=np.ones_like(spectrum1d_cube_larger.flux), + spectral_axis=spectrum1d_cube_larger.spectral_axis, + mask=np.ones_like(spectrum1d_cube_larger.flux)) + cubeviz_helper.load_data(mask_cube, override_cube_limit=True) + cubeviz_helper._loaded_mask_cube = cubeviz_helper.app.data_collection[-1] + + extract_plg = cubeviz_helper.plugins['Spectral Extraction'] + slice_plg = cubeviz_helper.plugins['Slice'] + + extract_plg.aperture = 'Subset 1' + extract_plg.wavelength_dependent = True + assert cubeviz_helper._loaded_mask_cube.get_object(cls=Spectrum1D, statistic=None) + + slice_plg.slice = 1 + extract_plg._obj.vue_adopt_slice_as_reference() + cone_aperture = extract_plg._obj.cone_aperture() + ref_wavelength_1 = extract_plg._obj.reference_wavelength + print(cone_aperture) + assert cone_aperture.shape == cubeviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, statistic=None).shape + + print(slice_plg.slice) + slice_plg.slice = 8 + extract_plg._obj.vue_adopt_slice_as_reference() + ref_wavelength_2 = extract_plg._obj.reference_wavelength + # Potentially not enough slices for this to make sense + cone_aperture_2 = extract_plg._obj.cone_aperture() + print(cone_aperture) + print(cone_aperture_2) + # Cone apertures are the same for some reason + # assert (cone_aperture != cone_aperture_2).any() + assert ref_wavelength_1 != ref_wavelength_2 From 1a3800b35b8d56366156049e71e78923222b2744 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 6 Feb 2024 09:26:41 -0500 Subject: [PATCH 05/56] Update test, address review comments, remove debug statements --- jdaviz/configs/cubeviz/plugins/parsers.py | 2 +- .../spectral_extraction.py | 13 +++++++--- .../tests/test_spectral_extraction.py | 25 +++++++++++++------ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/parsers.py b/jdaviz/configs/cubeviz/plugins/parsers.py index 708144488a..82d9d587e4 100644 --- a/jdaviz/configs/cubeviz/plugins/parsers.py +++ b/jdaviz/configs/cubeviz/plugins/parsers.py @@ -476,7 +476,7 @@ def _parse_ndarray(app, file_obj, data_label=None, data_type=None, elif data_type == 'uncert': app.add_data_to_viewer(uncert_viewer_reference_name, data_label) app._jdaviz_helper._loaded_uncert_cube = app.data_collection[data_label] - if data_type == 'mask': + elif data_type == 'mask': app._jdaviz_helper._loaded_mask_cube = app.data_collection[data_label] diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 1022db7b56..c8af0e9d6a 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -50,13 +50,15 @@ class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin, Subset to use for the spectral extraction, or ``Entire Cube``. * ``add_results`` (:class:`~jdaviz.core.template_mixin.AddResults`) * :meth:`collapse` + * ``aperture_method`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`): + Method used to create a cone aperture in spectral extraction. """ template_file = __file__, "spectral_extraction.vue" uses_active_status = Bool(True).tag(sync=True) # feature flag for cone support dev_cone_support = Bool(True).tag(sync=True) # when enabling: add entries to docstring - dev_bg_support = Bool(True).tag(sync=True) # when enabling: add entries to docstring + dev_bg_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring dev_subpixel_support = Bool(True).tag(sync=True) # when enabling: add entries to docstring active_step = Unicode().tag(sync=True) @@ -116,6 +118,12 @@ def __init__(self, *args, **kwargs): selected='function_selected', manual_options=['Mean', 'Min', 'Max', 'Sum'] ) + self.aperture_method = SelectPluginComponent( + self, + items='aperture_method_items', + selected='aperture_method_selected', + manual_options=['exact', 'subpixel', 'center'] + ) self._set_default_results_label() self.add_results.viewer.filters = ['is_spectrum_viewer'] @@ -144,7 +152,7 @@ def user_api(self): if self.dev_bg_support: expose += ['background', 'bg_wavelength_dependent'] if self.dev_subpixel_support: - expose += ['aperture_method_items', 'aperture_method_selected'] + expose += ['aperture_method'] return PluginUserApi(self, expose=expose) @@ -296,7 +304,6 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): return collapsed_spec - @with_spinner() def cone_aperture(self): # Retrieve mask cube and create array to represent the cone mask mask_cube = self._app._jdaviz_helper._loaded_mask_cube.get_object(cls=Spectrum1D, diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 99e1ad9ca9..453e8ed93d 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -236,7 +236,7 @@ def test_aperture_markers(cubeviz_helper, spectrum1d_cube): def test_cone_aperture(cubeviz_helper, spectrum1d_cube_larger): from specutils import Spectrum1D cubeviz_helper.load_data(spectrum1d_cube_larger) - cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(1, 1), radius=0.7)]) + cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(1, 1), radius=0.5)]) mask_cube = Spectrum1D(flux=np.ones_like(spectrum1d_cube_larger.flux), spectral_axis=spectrum1d_cube_larger.spectral_axis, @@ -255,17 +255,26 @@ def test_cone_aperture(cubeviz_helper, spectrum1d_cube_larger): extract_plg._obj.vue_adopt_slice_as_reference() cone_aperture = extract_plg._obj.cone_aperture() ref_wavelength_1 = extract_plg._obj.reference_wavelength - print(cone_aperture) assert cone_aperture.shape == cubeviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, statistic=None).shape - print(slice_plg.slice) slice_plg.slice = 8 extract_plg._obj.vue_adopt_slice_as_reference() ref_wavelength_2 = extract_plg._obj.reference_wavelength - # Potentially not enough slices for this to make sense + # Not enough slices for the cone to develop cone_aperture_2 = extract_plg._obj.cone_aperture() - print(cone_aperture) - print(cone_aperture_2) - # Cone apertures are the same for some reason - # assert (cone_aperture != cone_aperture_2).any() + + cone_result = [[[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], + [[1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]] + + # Cone apertures are the same because the radius and wavelength values do not grow + # fast enough to create different sized apertures. This functionality is more apparent + # data cubes with longer spectral axes. + assert (cone_aperture == cone_result).any() + assert (cone_aperture_2 == cone_result).any() assert ref_wavelength_1 != ref_wavelength_2 From 6ae4b1580609a5ab3592845fd6bd1ff8d123ed9e Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 6 Feb 2024 09:37:48 -0500 Subject: [PATCH 06/56] Fix style --- .../spectral_extraction/tests/test_spectral_extraction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 453e8ed93d..036ef60722 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -255,7 +255,8 @@ def test_cone_aperture(cubeviz_helper, spectrum1d_cube_larger): extract_plg._obj.vue_adopt_slice_as_reference() cone_aperture = extract_plg._obj.cone_aperture() ref_wavelength_1 = extract_plg._obj.reference_wavelength - assert cone_aperture.shape == cubeviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, statistic=None).shape + assert (cone_aperture.shape == + cubeviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, statistic=None).shape) slice_plg.slice = 8 extract_plg._obj.vue_adopt_slice_as_reference() From ef5e97a9979bc2f07ffa50a33ee230e73f6ae2f8 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 8 Feb 2024 10:31:15 -0500 Subject: [PATCH 07/56] Address review comments --- .../spectral_extraction.py | 43 ++++++++++++------- .../spectral_extraction.vue | 18 ++++---- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index c8af0e9d6a..be64d050fc 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -50,16 +50,18 @@ class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin, Subset to use for the spectral extraction, or ``Entire Cube``. * ``add_results`` (:class:`~jdaviz.core.template_mixin.AddResults`) * :meth:`collapse` + * ``wavelength_dependent``: + When true, the cone_aperture method will be used to determine the mask. + * ``reference_wavelength``: + The wavelength that will be used to calculate the radius of the cone through the cube. * ``aperture_method`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`): - Method used to create a cone aperture in spectral extraction. + Extract spectrum using an aperture masking method in place of the subset mask. """ template_file = __file__, "spectral_extraction.vue" uses_active_status = Bool(True).tag(sync=True) - # feature flag for cone support - dev_cone_support = Bool(True).tag(sync=True) # when enabling: add entries to docstring + # feature flag for background cone support dev_bg_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring - dev_subpixel_support = Bool(True).tag(sync=True) # when enabling: add entries to docstring active_step = Unicode().tag(sync=True) @@ -146,13 +148,11 @@ def __init__(self, *args, **kwargs): @property def user_api(self): expose = ['function', 'spatial_subset', 'aperture', - 'add_results', 'collapse_to_spectrum'] - if self.dev_cone_support: - expose += ['wavelength_dependent', 'reference_wavelength'] + 'add_results', 'collapse_to_spectrum', + 'wavelength_dependent', 'reference_wavelength', + 'aperture_method'] if self.dev_bg_support: expose += ['background', 'bg_wavelength_dependent'] - if self.dev_subpixel_support: - expose += ['aperture_method'] return PluginUserApi(self, expose=expose) @@ -216,6 +216,7 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): """ spectral_cube = self._app._jdaviz_helper._loaded_flux_cube uncert_cube = self._app._jdaviz_helper._loaded_uncert_cube + uncertainties = None # This plugin collapses over the *spatial axes* (optionally over a spatial subset, # defaults to ``No Subset``). Since the Cubeviz parser puts the fluxes @@ -225,22 +226,25 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): nddata = spectral_cube.get_subset_object( subset_id=self.aperture.selected, cls=NDDataArray ) - uncertainties = uncert_cube.get_subset_object( - subset_id=self.aperture.selected, cls=StdDevUncertainty - ) + if uncert_cube: + uncertainties = uncert_cube.get_subset_object( + subset_id=self.aperture.selected, cls=StdDevUncertainty + ) mask = self.cone_aperture() elif self.aperture.selected != self.aperture.default_text: nddata = spectral_cube.get_subset_object( subset_id=self.aperture.selected, cls=NDDataArray ) - uncertainties = uncert_cube.get_subset_object( - subset_id=self.aperture.selected, cls=StdDevUncertainty - ) + if uncert_cube: + uncertainties = uncert_cube.get_subset_object( + subset_id=self.aperture.selected, cls=StdDevUncertainty + ) mask = nddata.mask else: nddata = spectral_cube.get_object(cls=NDDataArray) - uncertainties = uncert_cube.get_object(cls=StdDevUncertainty) + if uncert_cube: + uncertainties = uncert_cube.get_object(cls=StdDevUncertainty) mask = nddata.mask # Use the spectral coordinate from the WCS: if '_orig_spec' in spectral_cube.meta: @@ -305,6 +309,13 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): return collapsed_spec def cone_aperture(self): + if not self._app._jdaviz_helper._loaded_mask_cube: + snackbar_message = SnackbarMessage( + "Cannot create cone aperture without valid mask cube loaded.", + color="error", + sender=self) + self.hub.broadcast(snackbar_message) + return # Retrieve mask cube and create array to represent the cone mask mask_cube = self._app._jdaviz_helper._loaded_mask_cube.get_object(cls=Spectrum1D, statistic=None) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue index 8c5de6bee4..15c52f2277 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue @@ -19,13 +19,13 @@ hint="Select a spatial region to extract its spectrum." /> - + {{aperture_selected}} does not support wavelength dependence (cone support): {{aperture_selected_validity.aperture_message}}. -
+
- + {{bg_selected}} does not support wavelength dependence (cone support): {{bg_selected_validity.aperture_message}}. @@ -88,8 +88,7 @@
+ && wavelength_dependent"> Extract - + Aperture: {{aperture_selected}} does not support subpixel: {{aperture_selected_validity.aperture_message}}. - + Background: {{bg_selected}} does not support subpixel: {{bg_selected_validity.aperture_message}}. @@ -131,13 +130,12 @@
+ && (bg_selected === 'None' || bg_selected_validity.is_aperture)"> Date: Thu, 8 Feb 2024 11:30:34 -0500 Subject: [PATCH 08/56] Update test --- .../tests/test_spectral_extraction.py | 33 +++++-------------- jdaviz/conftest.py | 7 +++- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 036ef60722..49dd8ac904 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -233,14 +233,12 @@ def test_aperture_markers(cubeviz_helper, spectrum1d_cube): assert slice_plg.slice == 1 -def test_cone_aperture(cubeviz_helper, spectrum1d_cube_larger): - from specutils import Spectrum1D - cubeviz_helper.load_data(spectrum1d_cube_larger) +def test_cone_aperture(cubeviz_helper, spectrum1d_cube_largest): + cubeviz_helper.load_data(spectrum1d_cube_largest) cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(1, 1), radius=0.5)]) - mask_cube = Spectrum1D(flux=np.ones_like(spectrum1d_cube_larger.flux), - spectral_axis=spectrum1d_cube_larger.spectral_axis, - mask=np.ones_like(spectrum1d_cube_larger.flux)) + mask_cube = Spectrum1D(flux=np.ones_like(spectrum1d_cube_largest.flux), + spectral_axis=spectrum1d_cube_largest.spectral_axis) cubeviz_helper.load_data(mask_cube, override_cube_limit=True) cubeviz_helper._loaded_mask_cube = cubeviz_helper.app.data_collection[-1] @@ -254,28 +252,13 @@ def test_cone_aperture(cubeviz_helper, spectrum1d_cube_larger): slice_plg.slice = 1 extract_plg._obj.vue_adopt_slice_as_reference() cone_aperture = extract_plg._obj.cone_aperture() - ref_wavelength_1 = extract_plg._obj.reference_wavelength assert (cone_aperture.shape == cubeviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, statistic=None).shape) - slice_plg.slice = 8 + # Make sure that the cone created when the reference slice is 988 is different + # to the cone made at reference slice 1. + slice_plg.slice = 988 extract_plg._obj.vue_adopt_slice_as_reference() - ref_wavelength_2 = extract_plg._obj.reference_wavelength - # Not enough slices for the cone to develop cone_aperture_2 = extract_plg._obj.cone_aperture() - cone_result = [[[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], - [[1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]] - - # Cone apertures are the same because the radius and wavelength values do not grow - # fast enough to create different sized apertures. This functionality is more apparent - # data cubes with longer spectral axes. - assert (cone_aperture == cone_result).any() - assert (cone_aperture_2 == cone_result).any() - assert ref_wavelength_1 != ref_wavelength_2 + assert not (cone_aperture == cone_aperture_2).all() diff --git a/jdaviz/conftest.py b/jdaviz/conftest.py index fab7c8aea5..b853848a98 100644 --- a/jdaviz/conftest.py +++ b/jdaviz/conftest.py @@ -21,7 +21,7 @@ if not NUMPY_LT_2_0: np.set_printoptions(legacy="1.25") -SPECTRUM_SIZE = 10 # length of spectrum +SPECTRUM_SIZE = 1000 # length of spectrum @pytest.fixture @@ -238,6 +238,11 @@ def spectrum1d_cube_larger(): return _create_spectrum1d_cube_with_fluxunit(fluxunit=u.Jy, shape=(SPECTRUM_SIZE, 2, 4)) +@pytest.fixture +def spectrum1d_cube_largest(): + return _create_spectrum1d_cube_with_fluxunit(fluxunit=u.Jy, shape=(1000, 2, 4)) + + @pytest.fixture def spectrum1d_cube_custom_fluxunit(): return _create_spectrum1d_cube_with_fluxunit From defe3b6826b93304c9139ce78cb132dccf3a85c2 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Fri, 9 Feb 2024 16:14:12 -0500 Subject: [PATCH 09/56] Remove change to SPECTRUM_SIZE in conftest --- jdaviz/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/conftest.py b/jdaviz/conftest.py index b853848a98..cb1f293d0d 100644 --- a/jdaviz/conftest.py +++ b/jdaviz/conftest.py @@ -21,7 +21,7 @@ if not NUMPY_LT_2_0: np.set_printoptions(legacy="1.25") -SPECTRUM_SIZE = 1000 # length of spectrum +SPECTRUM_SIZE = 10 # length of spectrum @pytest.fixture From a2b7a35ca3b882bf525dc3e97ac6b882fe7ce0d5 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Mon, 19 Feb 2024 10:27:36 -0500 Subject: [PATCH 10/56] Add docs --- docs/cubeviz/plugins.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/cubeviz/plugins.rst b/docs/cubeviz/plugins.rst index 57f2f13db5..82229c1bbe 100644 --- a/docs/cubeviz/plugins.rst +++ b/docs/cubeviz/plugins.rst @@ -291,6 +291,15 @@ Click :guilabel:`EXTRACT` to produce a new 1D spectrum dataset from the spectral cube, which has uncertainties propagated by `astropy.nddata `_. +If using a simple subset for the spatial aperture, an option to +make the aperture wavelength dependent will appear. If checked, this will +create a cone aperture that increases linearly with wavelength. +The reference wavelength for the cone can be changed using the +:guilabel:`Adopt Current Slice` button. + +The method of aperture masking can also be changed using the +:guilabel:`Aperture masking method` dropdown. + .. _cubeviz-aper-phot: Aperture Photometry From 0a6468f0a4e535bc7e2a425688085f21dc86e8c9 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Mon, 19 Feb 2024 13:45:51 -0500 Subject: [PATCH 11/56] Address review comments --- docs/cubeviz/plugins.rst | 10 +++++++++- .../tests/test_spectral_extraction.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/cubeviz/plugins.rst b/docs/cubeviz/plugins.rst index 82229c1bbe..182f78d612 100644 --- a/docs/cubeviz/plugins.rst +++ b/docs/cubeviz/plugins.rst @@ -294,11 +294,19 @@ from the spectral cube, which has uncertainties propagated by If using a simple subset for the spatial aperture, an option to make the aperture wavelength dependent will appear. If checked, this will create a cone aperture that increases linearly with wavelength. +The formula for that is:: + + radius = ((slice_wavelength / reference_wavelength) * + aperture.selected_spatial_region.radius) + The reference wavelength for the cone can be changed using the :guilabel:`Adopt Current Slice` button. The method of aperture masking can also be changed using the -:guilabel:`Aperture masking method` dropdown. +:guilabel:`Aperture masking method` dropdown. To see a description +for each of these options, please see +`Aperture and Pixel Overlap `_. + .. _cubeviz-aper-phot: diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 49dd8ac904..0d0f29b78e 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -261,4 +261,4 @@ def test_cone_aperture(cubeviz_helper, spectrum1d_cube_largest): extract_plg._obj.vue_adopt_slice_as_reference() cone_aperture_2 = extract_plg._obj.cone_aperture() - assert not (cone_aperture == cone_aperture_2).all() + assert np.testing.assert_array_equal(cone_aperture, cone_aperture_2) From 7eabb16895190608a42c7a89de36a0da94cb7dbe Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Mon, 19 Feb 2024 13:59:26 -0500 Subject: [PATCH 12/56] Fix test --- .../spectral_extraction/tests/test_spectral_extraction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 0d0f29b78e..b01b2e926c 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -261,4 +261,4 @@ def test_cone_aperture(cubeviz_helper, spectrum1d_cube_largest): extract_plg._obj.vue_adopt_slice_as_reference() cone_aperture_2 = extract_plg._obj.cone_aperture() - assert np.testing.assert_array_equal(cone_aperture, cone_aperture_2) + assert np.testing.assert_array_equal(cone_aperture, cone_aperture_2) is False From 59a3acc2d8450329592a51e6a925f3cc84916539 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Mon, 19 Feb 2024 14:50:16 -0500 Subject: [PATCH 13/56] Update test and change log --- CHANGES.rst | 2 ++ .../spectral_extraction/tests/test_spectral_extraction.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d6c556e400..4c60f254b3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,8 @@ New Features - Live-preview of aperture selection in plugins. [#2664, #2684] +- Add conical aperture support to cubeviz in the spectral extraction plugin [#2679] + Cubeviz ^^^^^^^ diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index b01b2e926c..80ca53f606 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -261,4 +261,5 @@ def test_cone_aperture(cubeviz_helper, spectrum1d_cube_largest): extract_plg._obj.vue_adopt_slice_as_reference() cone_aperture_2 = extract_plg._obj.cone_aperture() - assert np.testing.assert_array_equal(cone_aperture, cone_aperture_2) is False + with pytest.raises(AssertionError, match="Arrays are not equal"): + assert np.testing.assert_array_equal(cone_aperture, cone_aperture_2) From f3a01180702cb2a8df48b5ce024ab9796f30cae4 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 20 Feb 2024 09:31:07 -0500 Subject: [PATCH 14/56] Update docs/cubeviz/plugins.rst Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> --- docs/cubeviz/plugins.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cubeviz/plugins.rst b/docs/cubeviz/plugins.rst index 182f78d612..668d816a68 100644 --- a/docs/cubeviz/plugins.rst +++ b/docs/cubeviz/plugins.rst @@ -305,7 +305,7 @@ The reference wavelength for the cone can be changed using the The method of aperture masking can also be changed using the :guilabel:`Aperture masking method` dropdown. To see a description for each of these options, please see -`Aperture and Pixel Overlap `_. +:ref:`photutils:photutils-aperture-overlap`. .. _cubeviz-aper-phot: From 6ab130c0ad898d70a79b0759ef3350bcab60514f Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 20 Feb 2024 10:31:16 -0500 Subject: [PATCH 15/56] Change button to Slice to Wavelength --- .../cubeviz/plugins/spectral_extraction/spectral_extraction.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue index 15c52f2277..08ed7b8783 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue @@ -56,7 +56,7 @@ - Slice to Reference Wavelength + Slice to Wavelength From 42df57738bb384150c48122a4d9a612a7219dc43 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 20 Feb 2024 11:51:57 -0500 Subject: [PATCH 16/56] Add link to photutils docs, use blank mask cube --- .../spectral_extraction/spectral_extraction.py | 18 ++++++++---------- .../spectral_extraction.vue | 5 +++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index be64d050fc..28dac3f617 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -81,8 +81,8 @@ class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin, extracted_spec_available = Bool(False).tag(sync=True) overwrite_warn = Bool(False).tag(sync=True) - aperture_method_items = List(['exact', 'subpixel', 'center']).tag(sync=True) - aperture_method_selected = Unicode('exact').tag(sync=True) + aperture_method_items = List().tag(sync=True) + aperture_method_selected = Unicode('Exact').tag(sync=True) # export_enabled controls whether saving to a file is enabled via the UI. This # is a temporary measure to allow server-installations to disable saving server-side until @@ -124,7 +124,7 @@ def __init__(self, *args, **kwargs): self, items='aperture_method_items', selected='aperture_method_selected', - manual_options=['exact', 'subpixel', 'center'] + manual_options=['Exact', 'Subpixel', 'Center'] ) self._set_default_results_label() self.add_results.viewer.filters = ['is_spectrum_viewer'] @@ -316,12 +316,10 @@ def cone_aperture(self): sender=self) self.hub.broadcast(snackbar_message) return - # Retrieve mask cube and create array to represent the cone mask - mask_cube = self._app._jdaviz_helper._loaded_mask_cube.get_object(cls=Spectrum1D, - statistic=None) + # Retrieve flux cube and create an array to represent the cone mask flux_cube = self._app._jdaviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, statistic=None) - masks_boolean_values = np.zeros_like(mask_cube.flux.value) + masks_boolean_values = np.zeros_like(flux_cube.flux.value) # Center is reverse coordinates center = (self.aperture.selected_spatial_region.center.y, @@ -331,12 +329,12 @@ def cone_aperture(self): # Loop through cube and create cone aperture at each wavelength. Then convert that to a # mask using the selected aperture method and add that to a mask cube. - for index, wavelength in enumerate(mask_cube.spectral_axis): + for index, wavelength in enumerate(flux_cube.spectral_axis): radius = ((wavelength.to(display_unit).value / self.reference_wavelength) * self.aperture.selected_spatial_region.radius) aperture = CircularAperture(center, r=radius) - slice_mask = aperture.to_mask(method=self.aperture_method_selected).to_image( - (len(mask_cube.flux), len(mask_cube.flux[0]))) + slice_mask = aperture.to_mask(method=self.aperture_method_selected.lower()).to_image( + (len(flux_cube.flux), len(flux_cube.flux[0]))) masks_boolean_values[:, :, index] = ~(slice_mask > 0) return masks_boolean_values diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue index 08ed7b8783..3757218cf8 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue @@ -141,6 +141,11 @@ hint="Extract spectrum using an aperture masking method in place of the subset mask." persistent-hint > + + See the + for more details on aperture masking methods. +
From 1428cb6d1c68b1419376e0300bc531c730c676b0 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 20 Feb 2024 11:54:07 -0500 Subject: [PATCH 17/56] Use center as default aperture masking method --- .../cubeviz/plugins/spectral_extraction/spectral_extraction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 28dac3f617..78ab3c210e 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -82,7 +82,7 @@ class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin, overwrite_warn = Bool(False).tag(sync=True) aperture_method_items = List().tag(sync=True) - aperture_method_selected = Unicode('Exact').tag(sync=True) + aperture_method_selected = Unicode('Center').tag(sync=True) # export_enabled controls whether saving to a file is enabled via the UI. This # is a temporary measure to allow server-installations to disable saving server-side until From 24ee0ae07cbcd568b01b6589fa57967d3231b7c5 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 20 Feb 2024 12:36:18 -0500 Subject: [PATCH 18/56] Switch test to use exact --- .../spectral_extraction/tests/test_spectral_extraction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 80ca53f606..83a9674d32 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -246,6 +246,7 @@ def test_cone_aperture(cubeviz_helper, spectrum1d_cube_largest): slice_plg = cubeviz_helper.plugins['Slice'] extract_plg.aperture = 'Subset 1' + extract_plg.aperture_method.selected = "Exact" extract_plg.wavelength_dependent = True assert cubeviz_helper._loaded_mask_cube.get_object(cls=Spectrum1D, statistic=None) From fb54e88e470613b4216162d4fa8936db611e4d5a Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 20 Feb 2024 16:19:02 -0500 Subject: [PATCH 19/56] Fix test and change dtype --- .../cubeviz/plugins/spectral_extraction/spectral_extraction.py | 2 +- .../spectral_extraction/tests/test_spectral_extraction.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 78ab3c210e..d278500f9c 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -319,7 +319,7 @@ def cone_aperture(self): # Retrieve flux cube and create an array to represent the cone mask flux_cube = self._app._jdaviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, statistic=None) - masks_boolean_values = np.zeros_like(flux_cube.flux.value) + masks_boolean_values = np.zeros_like(flux_cube.flux.value, dtype=bool) # Center is reverse coordinates center = (self.aperture.selected_spatial_region.center.y, diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 83a9674d32..b8990bc848 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -253,8 +253,7 @@ def test_cone_aperture(cubeviz_helper, spectrum1d_cube_largest): slice_plg.slice = 1 extract_plg._obj.vue_adopt_slice_as_reference() cone_aperture = extract_plg._obj.cone_aperture() - assert (cone_aperture.shape == - cubeviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, statistic=None).shape) + assert cone_aperture.shape == spectrum1d_cube_largest.shape # Make sure that the cone created when the reference slice is 988 is different # to the cone made at reference slice 1. From c9c6c47693aff5630792a0c81bb8810ef1593b44 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 20 Feb 2024 16:20:36 -0500 Subject: [PATCH 20/56] Fix typo --- .../cubeviz/plugins/spectral_extraction/spectral_extraction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index d278500f9c..87489b3f40 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -319,7 +319,7 @@ def cone_aperture(self): # Retrieve flux cube and create an array to represent the cone mask flux_cube = self._app._jdaviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, statistic=None) - masks_boolean_values = np.zeros_like(flux_cube.flux.value, dtype=bool) + masks_boolean_values = np.zeros_like(flux_cube.flux.value, dtype=bool) # Center is reverse coordinates center = (self.aperture.selected_spatial_region.center.y, From 9f854d370dbb04c90811ac0db327228ae4040626 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 20 Feb 2024 16:38:39 -0500 Subject: [PATCH 21/56] Change test --- .../spectral_extraction/tests/test_spectral_extraction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index b8990bc848..d9ce05e343 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -253,7 +253,7 @@ def test_cone_aperture(cubeviz_helper, spectrum1d_cube_largest): slice_plg.slice = 1 extract_plg._obj.vue_adopt_slice_as_reference() cone_aperture = extract_plg._obj.cone_aperture() - assert cone_aperture.shape == spectrum1d_cube_largest.shape + assert cone_aperture.shape == (2, 4, 1000) # Make sure that the cone created when the reference slice is 988 is different # to the cone made at reference slice 1. From 305890a91d91d706a72a66769ec09ca737c91e22 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 20 Feb 2024 16:49:38 -0500 Subject: [PATCH 22/56] Throw exception if spectral axis is not in wavelength --- .../plugins/spectral_extraction/spectral_extraction.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 87489b3f40..929f291dbe 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -326,6 +326,12 @@ def cone_aperture(self): self.aperture.selected_spatial_region.center.x) # TODO: Replace with code for retrieving display_unit in cubeviz when it is available display_unit = flux_cube.spectral_axis.unit + if display_unit.physical_type != 'length': + error_msg = (f'Spectral axis unit physical type is ' + f'{display_unit.physical_type}, must be length') + snackbar_message = SnackbarMessage(error_msg, color="error", sender=self) + self.hub.broadcast(snackbar_message) + raise AttributeError(error_msg) # Loop through cube and create cone aperture at each wavelength. Then convert that to a # mask using the selected aperture method and add that to a mask cube. From 20e4bf2a2597dff97c9b598f2ab66a0400cba1e5 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 20 Feb 2024 17:18:54 -0500 Subject: [PATCH 23/56] Drastic change --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4c60f254b3..77e8604364 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,7 +13,7 @@ New Features - Live-preview of aperture selection in plugins. [#2664, #2684] -- Add conical aperture support to cubeviz in the spectral extraction plugin [#2679] +- Add conical aperture support to cubeviz in the spectral extraction plugin. [#2679] Cubeviz ^^^^^^^ From 7be10fc52bddd66fbe3d288563d9e05bd18d301f Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 20 Feb 2024 20:31:56 -0500 Subject: [PATCH 24/56] Change code --- .../plugins/spectral_extraction/spectral_extraction.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 929f291dbe..8d50c647e3 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -309,13 +309,6 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): return collapsed_spec def cone_aperture(self): - if not self._app._jdaviz_helper._loaded_mask_cube: - snackbar_message = SnackbarMessage( - "Cannot create cone aperture without valid mask cube loaded.", - color="error", - sender=self) - self.hub.broadcast(snackbar_message) - return # Retrieve flux cube and create an array to represent the cone mask flux_cube = self._app._jdaviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, statistic=None) @@ -331,7 +324,7 @@ def cone_aperture(self): f'{display_unit.physical_type}, must be length') snackbar_message = SnackbarMessage(error_msg, color="error", sender=self) self.hub.broadcast(snackbar_message) - raise AttributeError(error_msg) + return # Loop through cube and create cone aperture at each wavelength. Then convert that to a # mask using the selected aperture method and add that to a mask cube. From 4727f7d3012878785c8d81e2e6c55abe8f1a21f2 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Wed, 21 Feb 2024 11:08:01 -0500 Subject: [PATCH 25/56] Undo boolean conversion --- .../plugins/spectral_extraction/spectral_extraction.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 8d50c647e3..c465b8b4e8 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -312,7 +312,7 @@ def cone_aperture(self): # Retrieve flux cube and create an array to represent the cone mask flux_cube = self._app._jdaviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, statistic=None) - masks_boolean_values = np.zeros_like(flux_cube.flux.value, dtype=bool) + masks_float_values = np.zeros_like(flux_cube.flux.value, dtype=np.float32) # Center is reverse coordinates center = (self.aperture.selected_spatial_region.center.y, @@ -334,8 +334,8 @@ def cone_aperture(self): aperture = CircularAperture(center, r=radius) slice_mask = aperture.to_mask(method=self.aperture_method_selected.lower()).to_image( (len(flux_cube.flux), len(flux_cube.flux[0]))) - masks_boolean_values[:, :, index] = ~(slice_mask > 0) - return masks_boolean_values + masks_float_values[:, :, index] = ~(slice_mask > 0) + return masks_float_values def vue_spectral_extraction(self, *args, **kwargs): self.collapse_to_spectrum(add_data=True) From 239d7f4a42105f0856c3eec222139820c418cbd1 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Wed, 21 Feb 2024 11:16:26 -0500 Subject: [PATCH 26/56] Remove loaded_mask_cube --- jdaviz/configs/cubeviz/helper.py | 11 ++--------- jdaviz/configs/cubeviz/plugins/parsers.py | 14 +++----------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/jdaviz/configs/cubeviz/helper.py b/jdaviz/configs/cubeviz/helper.py index cffb78069a..f5a5ad2e10 100644 --- a/jdaviz/configs/cubeviz/helper.py +++ b/jdaviz/configs/cubeviz/helper.py @@ -24,7 +24,6 @@ class Cubeviz(ImageConfigHelper, LineListMixin): _loaded_flux_cube = None _loaded_uncert_cube = None - _loaded_mask_cube = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -105,14 +104,8 @@ def select_wavelength(self, wavelength): """ if not isinstance(wavelength, (int, float)): raise TypeError("wavelength must be a float or int") - # Retrieve the x slices from the spectrum viewer's marks - sv = self.app.get_viewer(self._default_spectrum_viewer_reference_name) - x_all = sv.native_marks[0].x - if sv.state.layers[0].as_steps: - # then the marks have been doubled in length (each point duplicated) - x_all = x_all[::2] - index = np.argmin(abs(x_all - wavelength)) - return self.select_slice(int(index)) + msg = SliceSelectSliceMessage(value=wavelength, sender=self) + self.app.hub.broadcast(msg) @property def specviz(self): diff --git a/jdaviz/configs/cubeviz/plugins/parsers.py b/jdaviz/configs/cubeviz/plugins/parsers.py index 82d9d587e4..8d14a575ac 100644 --- a/jdaviz/configs/cubeviz/plugins/parsers.py +++ b/jdaviz/configs/cubeviz/plugins/parsers.py @@ -254,7 +254,7 @@ def _parse_hdulist(app, hdulist, file_name=None, if data_type == 'mask': # We no longer auto-populate the mask cube into a viewer - app._jdaviz_helper._loaded_mask_cube = app.data_collection[data_label] + pass elif data_type == 'uncert': app.add_data_to_viewer(uncert_viewer_reference_name, data_label) @@ -319,8 +319,6 @@ def _parse_jwst_s3d(app, hdulist, data_label, ext='SCI', app._jdaviz_helper._loaded_flux_cube = app.data_collection[data_label] elif data_type == 'uncert': app._jdaviz_helper._loaded_uncert_cube = app.data_collection[data_label] - elif data_type == 'mask': - app._jdaviz_helper._loaded_mask_cube = app.data_collection[data_label] def _parse_esa_s3d(app, hdulist, data_label, ext='DATA', flux_viewer_reference_name=None, @@ -370,10 +368,8 @@ def _parse_esa_s3d(app, hdulist, data_label, ext='DATA', flux_viewer_reference_n if data_type == 'flux': app._jdaviz_helper._loaded_flux_cube = app.data_collection[data_label] - elif data_type == 'uncert': + if data_type == 'uncert': app._jdaviz_helper._loaded_uncert_cube = app.data_collection[data_label] - elif data_type == 'mask': - app._jdaviz_helper._loaded_mask_cube = app.data_collection[data_label] def _parse_spectrum1d_3d(app, file_obj, data_label=None, @@ -426,9 +422,7 @@ def _parse_spectrum1d_3d(app, file_obj, data_label=None, elif attr == 'uncertainty': app.add_data_to_viewer(uncert_viewer_reference_name, cur_data_label) app._jdaviz_helper._loaded_uncert_cube = app.data_collection[cur_data_label] - elif attr == 'mask': - # We no longer auto-populate the mask cube into a viewer - app._jdaviz_helper._loaded_mask_cube = app.data_collection[cur_data_label] + # We no longer auto-populate the mask cube into a viewer def _parse_spectrum1d(app, file_obj, data_label=None, spectrum_viewer_reference_name=None): @@ -476,8 +470,6 @@ def _parse_ndarray(app, file_obj, data_label=None, data_type=None, elif data_type == 'uncert': app.add_data_to_viewer(uncert_viewer_reference_name, data_label) app._jdaviz_helper._loaded_uncert_cube = app.data_collection[data_label] - elif data_type == 'mask': - app._jdaviz_helper._loaded_mask_cube = app.data_collection[data_label] def _parse_gif(app, file_obj, data_label=None, flux_viewer_reference_name=None, From 8bf7b55f0ae4e25ccce8b68c1102f7e41656c9ec Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Wed, 21 Feb 2024 13:20:27 -0500 Subject: [PATCH 27/56] jdaviz/configs/cubeviz/helper.py --- jdaviz/configs/cubeviz/helper.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/jdaviz/configs/cubeviz/helper.py b/jdaviz/configs/cubeviz/helper.py index f5a5ad2e10..f059bb87e6 100644 --- a/jdaviz/configs/cubeviz/helper.py +++ b/jdaviz/configs/cubeviz/helper.py @@ -104,8 +104,14 @@ def select_wavelength(self, wavelength): """ if not isinstance(wavelength, (int, float)): raise TypeError("wavelength must be a float or int") - msg = SliceSelectSliceMessage(value=wavelength, sender=self) - self.app.hub.broadcast(msg) + # Retrieve the x slices from the spectrum viewer's marks + sv = self.app.get_viewer(self._default_spectrum_viewer_reference_name) + x_all = sv.native_marks[0].x + if sv.state.layers[0].as_steps: + # then the marks have been doubled in length (each point duplicated) + x_all = x_all[::2] + index = np.argmin(abs(x_all - wavelength)) + return self.select_slice(int(index)) @property def specviz(self): From 2621fa6603694569da7a1a7f1ecc6538a8acd213 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:37:08 -0500 Subject: [PATCH 28/56] Move physical_type check to top [ci skip] --- .../spectral_extraction/spectral_extraction.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index c465b8b4e8..9f595f829c 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -312,19 +312,19 @@ def cone_aperture(self): # Retrieve flux cube and create an array to represent the cone mask flux_cube = self._app._jdaviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, statistic=None) + # TODO: Replace with code for retrieving display_unit in cubeviz when it is available + display_unit = flux_cube.spectral_axis.unit + if display_unit.physical_type != 'length': + self.hub.broadcast(SnackbarMessage('Spectral axis unit physical type is ' + f'{display_unit.physical_type}, must be length', + color="error", sender=self)) + return + masks_float_values = np.zeros_like(flux_cube.flux.value, dtype=np.float32) # Center is reverse coordinates center = (self.aperture.selected_spatial_region.center.y, self.aperture.selected_spatial_region.center.x) - # TODO: Replace with code for retrieving display_unit in cubeviz when it is available - display_unit = flux_cube.spectral_axis.unit - if display_unit.physical_type != 'length': - error_msg = (f'Spectral axis unit physical type is ' - f'{display_unit.physical_type}, must be length') - snackbar_message = SnackbarMessage(error_msg, color="error", sender=self) - self.hub.broadcast(snackbar_message) - return # Loop through cube and create cone aperture at each wavelength. Then convert that to a # mask using the selected aperture method and add that to a mask cube. From 7cb532897e519d4920641d04b20da93ac08c323e Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:41:23 -0500 Subject: [PATCH 29/56] Clean up test --- .../spectral_extraction/tests/test_spectral_extraction.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index d9ce05e343..7c13e318cc 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -237,18 +237,12 @@ def test_cone_aperture(cubeviz_helper, spectrum1d_cube_largest): cubeviz_helper.load_data(spectrum1d_cube_largest) cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(1, 1), radius=0.5)]) - mask_cube = Spectrum1D(flux=np.ones_like(spectrum1d_cube_largest.flux), - spectral_axis=spectrum1d_cube_largest.spectral_axis) - cubeviz_helper.load_data(mask_cube, override_cube_limit=True) - cubeviz_helper._loaded_mask_cube = cubeviz_helper.app.data_collection[-1] - extract_plg = cubeviz_helper.plugins['Spectral Extraction'] slice_plg = cubeviz_helper.plugins['Slice'] extract_plg.aperture = 'Subset 1' extract_plg.aperture_method.selected = "Exact" extract_plg.wavelength_dependent = True - assert cubeviz_helper._loaded_mask_cube.get_object(cls=Spectrum1D, statistic=None) slice_plg.slice = 1 extract_plg._obj.vue_adopt_slice_as_reference() @@ -262,4 +256,4 @@ def test_cone_aperture(cubeviz_helper, spectrum1d_cube_largest): cone_aperture_2 = extract_plg._obj.cone_aperture() with pytest.raises(AssertionError, match="Arrays are not equal"): - assert np.testing.assert_array_equal(cone_aperture, cone_aperture_2) + np.testing.assert_array_equal(cone_aperture, cone_aperture_2) From ba90b05d472b487308233da363940a5e09221881 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:38:18 -0500 Subject: [PATCH 30/56] Simplify cone_aperture algorithm by not repeating unnecessary calculations --- .../spectral_extraction/spectral_extraction.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 9f595f829c..ec2e13b2f2 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -328,12 +328,14 @@ def cone_aperture(self): # Loop through cube and create cone aperture at each wavelength. Then convert that to a # mask using the selected aperture method and add that to a mask cube. - for index, wavelength in enumerate(flux_cube.spectral_axis): - radius = ((wavelength.to(display_unit).value / self.reference_wavelength) * - self.aperture.selected_spatial_region.radius) + # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit conversion. + radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * + self.aperture.selected_spatial_region.radius) + im_shape = (flux_cube.shape[1], flux_cube.shape[0]) # Reversed like center + aper_method = self.aperture_method_selected.lower() + for index, radius in enumerate(radii): aperture = CircularAperture(center, r=radius) - slice_mask = aperture.to_mask(method=self.aperture_method_selected.lower()).to_image( - (len(flux_cube.flux), len(flux_cube.flux[0]))) + slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) masks_float_values[:, :, index] = ~(slice_mask > 0) return masks_float_values From 0bb158a7106290656675d99db32a2965990a9aca Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 21 Feb 2024 17:06:15 -0500 Subject: [PATCH 31/56] Have to flip shape so test would pass --- .../plugins/spectral_extraction/spectral_extraction.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index ec2e13b2f2..e82a64a356 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -331,11 +331,14 @@ def cone_aperture(self): # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit conversion. radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * self.aperture.selected_spatial_region.radius) - im_shape = (flux_cube.shape[1], flux_cube.shape[0]) # Reversed like center + im_shape = (flux_cube.shape[0], flux_cube.shape[1]) aper_method = self.aperture_method_selected.lower() for index, radius in enumerate(radii): aperture = CircularAperture(center, r=radius) slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) + # FIXME: ~(slice_mask > 0) changes the float values to boolean + # but looks like you need zeroes to indicate "good" pixels, so not + # sure how you want to do this with "fractional" coverage from photutils. masks_float_values[:, :, index] = ~(slice_mask > 0) return masks_float_values From 0c1ed552de6680d51266cb1a76faa810380a160a Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 22 Feb 2024 13:54:22 -0500 Subject: [PATCH 32/56] Apply fractional pixel array when needed and update test --- .../spectral_extraction.py | 32 +++++++++++------- .../tests/test_spectral_extraction.py | 33 ++++++++++--------- jdaviz/conftest.py | 16 ++++++++- 3 files changed, 52 insertions(+), 29 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index e82a64a356..aa2d3dc237 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -7,7 +7,7 @@ import astropy.units as u from astropy.utils.decorators import deprecated from astropy.nddata import ( - NDDataArray, StdDevUncertainty, NDUncertainty + NDDataArray, StdDevUncertainty, NDUncertainty, NDData, NDDataRef ) from traitlets import Any, Bool, Dict, Float, List, Unicode, observe from photutils.aperture import CircularAperture @@ -230,7 +230,16 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): uncertainties = uncert_cube.get_subset_object( subset_id=self.aperture.selected, cls=StdDevUncertainty ) - mask = self.cone_aperture() + # Returns an NDDataArray cube with the exact slice mask + # in the `data` attribute and the boolean version of the mask + # in the `mask` attribute + cone_mask = self.cone_aperture() + if self.aperture_method_selected.lower() == 'center': + flux = nddata.data << nddata.unit + else: + # Apply the fractional pixel array to the flux cube + flux = (cone_mask.data * nddata.data) << nddata.unit + mask = cone_mask.mask elif self.aperture.selected != self.aperture.default_text: nddata = spectral_cube.get_subset_object( @@ -240,11 +249,13 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): uncertainties = uncert_cube.get_subset_object( subset_id=self.aperture.selected, cls=StdDevUncertainty ) + flux = nddata.data << nddata.unit mask = nddata.mask else: nddata = spectral_cube.get_object(cls=NDDataArray) if uncert_cube: uncertainties = uncert_cube.get_object(cls=StdDevUncertainty) + flux = nddata.data << nddata.unit mask = nddata.mask # Use the spectral coordinate from the WCS: if '_orig_spec' in spectral_cube.meta: @@ -252,12 +263,9 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): else: wcs = spectral_cube.coords.spectral - flux = nddata.data << nddata.unit - nddata_reshaped = NDDataArray( flux, mask=mask, uncertainty=uncertainties, wcs=wcs, meta=nddata.meta ) - # by default we want to use operation_ignores_mask=True in nddata: kwargs.setdefault("operation_ignores_mask", True) # by default we want to propagate uncertainties: @@ -288,7 +296,6 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): uncertainty=uncertainty, mask=mask ) - # stuff for exporting to file self.extracted_spec = collapsed_spec self.extracted_spec_available = True @@ -320,7 +327,8 @@ def cone_aperture(self): color="error", sender=self)) return - masks_float_values = np.zeros_like(flux_cube.flux.value, dtype=np.float32) + masks_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) + masks_bool_values = np.ones_like(flux_cube.flux.value, dtype=bool) # Center is reverse coordinates center = (self.aperture.selected_spatial_region.center.y, @@ -336,11 +344,11 @@ def cone_aperture(self): for index, radius in enumerate(radii): aperture = CircularAperture(center, r=radius) slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) - # FIXME: ~(slice_mask > 0) changes the float values to boolean - # but looks like you need zeroes to indicate "good" pixels, so not - # sure how you want to do this with "fractional" coverage from photutils. - masks_float_values[:, :, index] = ~(slice_mask > 0) - return masks_float_values + # Send exact fractional pixel array in the `data` attribute + # and the boolean mask in the `mask` attribute + masks_bool_values[:, :, index] = ~(slice_mask > 0) + masks_weights[:, :, index] = slice_mask + return NDDataArray(data=masks_weights, mask=masks_bool_values) def vue_spectral_extraction(self, *args, **kwargs): self.collapse_to_spectrum(add_data=True) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 7c13e318cc..5dc55ed285 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -233,27 +233,28 @@ def test_aperture_markers(cubeviz_helper, spectrum1d_cube): assert slice_plg.slice == 1 -def test_cone_aperture(cubeviz_helper, spectrum1d_cube_largest): +@pytest.mark.parametrize( + ('aperture_method', 'expected_flux_1000', 'expected_flux_2400'), + [('Exact', [16.51429064, 16.52000853, 16.52572818, 16.53145005, 16.53717344, 16.54289928, + 16.54862712, 16.55435647, 16.56008781, 16.56582186], + [26.812409, 26.821692, 26.830979, 26.840268, 26.849561, 26.858857, + 26.868156, 26.877459, 26.886765, 26.896074]), + ('Subpixel', [16.84000006] * 10, [26.92] * 10), + ('Center', [21] * 10, [25] * 10)] +) +def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_largest, + aperture_method, expected_flux_1000, + expected_flux_2400): cubeviz_helper.load_data(spectrum1d_cube_largest) - cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(1, 1), radius=0.5)]) + cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(14, 15), radius=2.5)]) extract_plg = cubeviz_helper.plugins['Spectral Extraction'] - slice_plg = cubeviz_helper.plugins['Slice'] extract_plg.aperture = 'Subset 1' - extract_plg.aperture_method.selected = "Exact" + extract_plg.aperture_method.selected = aperture_method extract_plg.wavelength_dependent = True - slice_plg.slice = 1 - extract_plg._obj.vue_adopt_slice_as_reference() - cone_aperture = extract_plg._obj.cone_aperture() - assert cone_aperture.shape == (2, 4, 1000) - - # Make sure that the cone created when the reference slice is 988 is different - # to the cone made at reference slice 1. - slice_plg.slice = 988 - extract_plg._obj.vue_adopt_slice_as_reference() - cone_aperture_2 = extract_plg._obj.cone_aperture() + collapsed_spec_2 = extract_plg.collapse_to_spectrum() - with pytest.raises(AssertionError, match="Arrays are not equal"): - np.testing.assert_array_equal(cone_aperture, cone_aperture_2) + np.testing.assert_allclose(collapsed_spec_2.flux.value[1000:1010], expected_flux_1000, atol=1e-9) + np.testing.assert_allclose(collapsed_spec_2.flux.value[2400:2410], expected_flux_2400, atol=1e-9) diff --git a/jdaviz/conftest.py b/jdaviz/conftest.py index cb1f293d0d..77876ef3f7 100644 --- a/jdaviz/conftest.py +++ b/jdaviz/conftest.py @@ -240,7 +240,21 @@ def spectrum1d_cube_larger(): @pytest.fixture def spectrum1d_cube_largest(): - return _create_spectrum1d_cube_with_fluxunit(fluxunit=u.Jy, shape=(1000, 2, 4)) + flux = np.zeros((30, 30, 3001)).astype(int) + 1 + + # 1 um to 4 um + dlam = 0.001 + wavelength = 1.0 + np.arange(3001) * dlam + + wcs_dict = {"CTYPE1": "WAVE-LOG", "CTYPE2": "DEC--TAN", "CTYPE3": "RA---TAN", + "CRVAL1": 4.622e-7, "CRVAL2": 27, "CRVAL3": 205, + "CDELT1": 8e-11, "CDELT2": 0.0001, "CDELT3": -0.0001, + "CRPIX1": 0, "CRPIX2": 0, "CRPIX3": 0} + + w = WCS(wcs_dict) + + spec3d = Spectrum1D(flux=flux * u.Jy, wcs=w) + return spec3d @pytest.fixture From c63cd0d0ba07640ed347a3158d60b7bdf274c4eb Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 22 Feb 2024 13:59:38 -0500 Subject: [PATCH 33/56] Fix style --- jdaviz/conftest.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/jdaviz/conftest.py b/jdaviz/conftest.py index 77876ef3f7..868d6d9cfc 100644 --- a/jdaviz/conftest.py +++ b/jdaviz/conftest.py @@ -240,11 +240,7 @@ def spectrum1d_cube_larger(): @pytest.fixture def spectrum1d_cube_largest(): - flux = np.zeros((30, 30, 3001)).astype(int) + 1 - - # 1 um to 4 um - dlam = 0.001 - wavelength = 1.0 + np.arange(3001) * dlam + flux = np.ones((30, 30, 3001)).astype(int) wcs_dict = {"CTYPE1": "WAVE-LOG", "CTYPE2": "DEC--TAN", "CTYPE3": "RA---TAN", "CRVAL1": 4.622e-7, "CRVAL2": 27, "CRVAL3": 205, From 2350c724bafd58b4fb7f1008ea403a6dfe83c13c Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 22 Feb 2024 14:01:33 -0500 Subject: [PATCH 34/56] More fixes --- .../plugins/spectral_extraction/spectral_extraction.py | 2 +- .../spectral_extraction/tests/test_spectral_extraction.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index aa2d3dc237..1c40fe4f57 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -7,7 +7,7 @@ import astropy.units as u from astropy.utils.decorators import deprecated from astropy.nddata import ( - NDDataArray, StdDevUncertainty, NDUncertainty, NDData, NDDataRef + NDDataArray, StdDevUncertainty, NDUncertainty ) from traitlets import Any, Bool, Dict, Float, List, Unicode, observe from photutils.aperture import CircularAperture diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 5dc55ed285..6e93999069 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -256,5 +256,7 @@ def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_la collapsed_spec_2 = extract_plg.collapse_to_spectrum() - np.testing.assert_allclose(collapsed_spec_2.flux.value[1000:1010], expected_flux_1000, atol=1e-9) - np.testing.assert_allclose(collapsed_spec_2.flux.value[2400:2410], expected_flux_2400, atol=1e-9) + np.testing.assert_allclose(collapsed_spec_2.flux.value[1000:1010], expected_flux_1000, + atol=1e-9) + np.testing.assert_allclose(collapsed_spec_2.flux.value[2400:2410], expected_flux_2400, + atol=1e-9) From 76ac07071d37da5a54831c31f1263bea06b8e114 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 22 Feb 2024 15:09:07 -0500 Subject: [PATCH 35/56] Return cone aperture as float32 pixel array --- .../spectral_extraction/spectral_extraction.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 1c40fe4f57..7f9be42f59 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -230,16 +230,15 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): uncertainties = uncert_cube.get_subset_object( subset_id=self.aperture.selected, cls=StdDevUncertainty ) - # Returns an NDDataArray cube with the exact slice mask - # in the `data` attribute and the boolean version of the mask - # in the `mask` attribute + # Exact slice mask of cone aperture through the cube cone_mask = self.cone_aperture() if self.aperture_method_selected.lower() == 'center': flux = nddata.data << nddata.unit else: # Apply the fractional pixel array to the flux cube - flux = (cone_mask.data * nddata.data) << nddata.unit - mask = cone_mask.mask + flux = (cone_mask * nddata.data) << nddata.unit + # Boolean mask cube that represents the cone aperture + mask = ~(cone_mask > 0) elif self.aperture.selected != self.aperture.default_text: nddata = spectral_cube.get_subset_object( @@ -328,7 +327,6 @@ def cone_aperture(self): return masks_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) - masks_bool_values = np.ones_like(flux_cube.flux.value, dtype=bool) # Center is reverse coordinates center = (self.aperture.selected_spatial_region.center.y, @@ -344,11 +342,9 @@ def cone_aperture(self): for index, radius in enumerate(radii): aperture = CircularAperture(center, r=radius) slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) - # Send exact fractional pixel array in the `data` attribute - # and the boolean mask in the `mask` attribute - masks_bool_values[:, :, index] = ~(slice_mask > 0) + # Add slice mask to fractional pixel array masks_weights[:, :, index] = slice_mask - return NDDataArray(data=masks_weights, mask=masks_bool_values) + return masks_weights def vue_spectral_extraction(self, *args, **kwargs): self.collapse_to_spectrum(add_data=True) From 0c968aea852efb85c544a9fdeb92fceeb8f4a927 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Fri, 23 Feb 2024 12:48:58 -0500 Subject: [PATCH 36/56] Apply suggestions from code review Co-authored-by: Brett M. Morris --- .../spectral_extraction/spectral_extraction.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 7f9be42f59..56b05103a8 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -230,15 +230,18 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): uncertainties = uncert_cube.get_subset_object( subset_id=self.aperture.selected, cls=StdDevUncertainty ) - # Exact slice mask of cone aperture through the cube + # Exact slice mask of cone aperture through the cube. `cone_mask` is + # a 3D array with fractions of each pixel within an aperture at each + # wavelength, on the range [0, 1]. cone_mask = self.cone_aperture() if self.aperture_method_selected.lower() == 'center': flux = nddata.data << nddata.unit else: # Apply the fractional pixel array to the flux cube flux = (cone_mask * nddata.data) << nddata.unit - # Boolean mask cube that represents the cone aperture - mask = ~(cone_mask > 0) + # Boolean cube which is True outside of the aperture + # (i.e., the numpy boolean mask convention) + mask = np.isclose(cone_mask, 0) elif self.aperture.selected != self.aperture.default_text: nddata = spectral_cube.get_subset_object( @@ -333,7 +336,7 @@ def cone_aperture(self): self.aperture.selected_spatial_region.center.x) # Loop through cube and create cone aperture at each wavelength. Then convert that to a - # mask using the selected aperture method and add that to a mask cube. + # weight array using the selected aperture method, and add it to a weight cube. # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit conversion. radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * self.aperture.selected_spatial_region.radius) From 5b575c54759a721c61636da4d0d49b0b72ed1b48 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Fri, 23 Feb 2024 12:55:44 -0500 Subject: [PATCH 37/56] Fix whitespace --- .../cubeviz/plugins/spectral_extraction/spectral_extraction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 56b05103a8..8bbb005cae 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -231,7 +231,7 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): subset_id=self.aperture.selected, cls=StdDevUncertainty ) # Exact slice mask of cone aperture through the cube. `cone_mask` is - # a 3D array with fractions of each pixel within an aperture at each + # a 3D array with fractions of each pixel within an aperture at each # wavelength, on the range [0, 1]. cone_mask = self.cone_aperture() if self.aperture_method_selected.lower() == 'center': From 2fd9e63b500c74fd6eac4a8be4e105d4de709d4d Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Fri, 23 Feb 2024 16:21:37 -0500 Subject: [PATCH 38/56] Cover case where function is mean --- .../spectral_extraction.py | 19 +++++++++++--- .../tests/test_spectral_extraction.py | 25 +++++++++++++------ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 8bbb005cae..289c9637af 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -277,10 +277,21 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): # is always wavelength. This may need adjustment after the following # specutils PR is merged: https://github.com/astropy/specutils/pull/1033 spatial_axes = (0, 1) - - collapsed_nddata = getattr(nddata_reshaped, self.function_selected.lower())( - axis=spatial_axes, **kwargs - ) # returns an NDDataArray + if self.wavelength_dependent and self.function_selected.lower() == 'mean': + # Use built-in sum function to collapse NDDataArray + collapsed_for_mean = nddata_reshaped.sum(axis=spatial_axes, **kwargs) + # Then normalize the flux based on the fractional pixel array + flux_for_mean = (collapsed_for_mean.data / + np.sum(cone_mask, axis=spatial_axes)) << nddata_reshaped.unit + # Combine that information into a new NDDataArray + collapsed_nddata = NDDataArray(flux_for_mean, mask=collapsed_for_mean.mask, + uncertainty=collapsed_for_mean.uncertainty, + wcs=collapsed_for_mean.wcs, + meta=collapsed_for_mean.meta) + else: + collapsed_nddata = getattr(nddata_reshaped, self.function_selected.lower())( + axis=spatial_axes, **kwargs + ) # returns an NDDataArray # Convert to Spectrum1D, with the spectral axis in correct units: if hasattr(spectral_cube.coords, 'spectral_wcs'): diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 6e93999069..6328ae67a4 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -234,17 +234,19 @@ def test_aperture_markers(cubeviz_helper, spectrum1d_cube): @pytest.mark.parametrize( - ('aperture_method', 'expected_flux_1000', 'expected_flux_2400'), + ('aperture_method', 'expected_flux_1000', 'expected_flux_2400', 'expected_flux_mean'), [('Exact', [16.51429064, 16.52000853, 16.52572818, 16.53145005, 16.53717344, 16.54289928, 16.54862712, 16.55435647, 16.56008781, 16.56582186], [26.812409, 26.821692, 26.830979, 26.840268, 26.849561, 26.858857, - 26.868156, 26.877459, 26.886765, 26.896074]), - ('Subpixel', [16.84000006] * 10, [26.92] * 10), - ('Center', [21] * 10, [25] * 10)] + 26.868156, 26.877459, 26.886765, 26.896074], + [0.99999993, 1.00000014, 1.00000011, 0.99999987, 0.99999995, 0.99999995, + 1.00000007, 1.00000005, 0.99999992, 0.99999996]), + ('Subpixel', [16.84000006] * 10, [26.92] * 10, [0.99999988] * 10), + ('Center', [21] * 10, [25] * 10, [1] * 10)] ) def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_largest, aperture_method, expected_flux_1000, - expected_flux_2400): + expected_flux_2400, expected_flux_mean): cubeviz_helper.load_data(spectrum1d_cube_largest) cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(14, 15), radius=2.5)]) @@ -253,10 +255,17 @@ def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_la extract_plg.aperture = 'Subset 1' extract_plg.aperture_method.selected = aperture_method extract_plg.wavelength_dependent = True + extract_plg.function = 'Sum' - collapsed_spec_2 = extract_plg.collapse_to_spectrum() + collapsed_spec = extract_plg.collapse_to_spectrum() - np.testing.assert_allclose(collapsed_spec_2.flux.value[1000:1010], expected_flux_1000, + np.testing.assert_allclose(collapsed_spec.flux.value[1000:1010], expected_flux_1000, atol=1e-9) - np.testing.assert_allclose(collapsed_spec_2.flux.value[2400:2410], expected_flux_2400, + np.testing.assert_allclose(collapsed_spec.flux.value[2400:2410], expected_flux_2400, + atol=1e-9) + + extract_plg.function = 'Mean' + collapsed_spec_mean = extract_plg.collapse_to_spectrum() + + np.testing.assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean, atol=1e-9) From 2e47c77e36f7edd8952c87115d1b66a6f339933f Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Mon, 26 Feb 2024 10:59:47 -0500 Subject: [PATCH 39/56] Remove subpixel option and add warning for exact plus min or max --- .../spectral_extraction.py | 24 +++++++++++++++++-- .../spectral_extraction.vue | 8 ++++++- .../tests/test_spectral_extraction.py | 1 - 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 289c9637af..edcc1b23bb 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -84,6 +84,8 @@ class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin, aperture_method_items = List().tag(sync=True) aperture_method_selected = Unicode('Center').tag(sync=True) + conflicting_aperture_and_function = Bool(False).tag(sync=True) + # export_enabled controls whether saving to a file is enabled via the UI. This # is a temporary measure to allow server-installations to disable saving server-side until # saving client-side is supported @@ -120,11 +122,12 @@ def __init__(self, *args, **kwargs): selected='function_selected', manual_options=['Mean', 'Min', 'Max', 'Sum'] ) + self.aperture_method_manual_options = ['Exact', 'Center'] self.aperture_method = SelectPluginComponent( self, items='aperture_method_items', selected='aperture_method_selected', - manual_options=['Exact', 'Subpixel', 'Center'] + manual_options=self.aperture_method_manual_options ) self._set_default_results_label() self.add_results.viewer.filters = ['is_spectrum_viewer'] @@ -200,6 +203,14 @@ def _update_mark_scale(self, *args): else: self.background.scale_factor = self.slice_wavelength/self.reference_wavelength + @observe('function_selected', 'aperture_method_selected') + def _update_aperture_method_on_function_change(self, *args): + if (self.function_selected.lower() in ['min', 'max'] and + self.aperture_method_selected.lower() != 'center'): + self.conflicting_aperture_and_function = True + else: + self.conflicting_aperture_and_function = False + @with_spinner() def collapse_to_spectrum(self, add_data=True, **kwargs): """ @@ -214,6 +225,12 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): Additional keyword arguments passed to the NDDataArray collapse operation. Examples include ``propagate_uncertainties`` and ``operation_ignores_mask``. """ + if self.conflicting_aperture_and_function: + self.hub.broadcast(SnackbarMessage("Aperture method Exact cannot be selected" + " along with Min or Max.", + color="error", sender=self)) + return + spectral_cube = self._app._jdaviz_helper._loaded_flux_cube uncert_cube = self._app._jdaviz_helper._loaded_uncert_cube uncertainties = None @@ -233,7 +250,10 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): # Exact slice mask of cone aperture through the cube. `cone_mask` is # a 3D array with fractions of each pixel within an aperture at each # wavelength, on the range [0, 1]. - cone_mask = self.cone_aperture() + if self.function_selected.lower() in ['min', 'max']: + cone_mask = self.cone_aperture().astype(int) + else: + cone_mask = self.cone_aperture() if self.aperture_method_selected.lower() == 'center': flux = nddata.data << nddata.unit else: diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue index 3757218cf8..d0973688d2 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue @@ -160,6 +160,12 @@ persistent-hint >
+ + + Aperture method Exact cannot be selected along with Min or Max. + + + diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 6328ae67a4..e627a0daf8 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -241,7 +241,6 @@ def test_aperture_markers(cubeviz_helper, spectrum1d_cube): 26.868156, 26.877459, 26.886765, 26.896074], [0.99999993, 1.00000014, 1.00000011, 0.99999987, 0.99999995, 0.99999995, 1.00000007, 1.00000005, 0.99999992, 0.99999996]), - ('Subpixel', [16.84000006] * 10, [26.92] * 10, [0.99999988] * 10), ('Center', [21] * 10, [25] * 10, [1] * 10)] ) def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_largest, From beed6529cbac18a61d2fddc37d741c1828150bab Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 11:04:21 -0500 Subject: [PATCH 40/56] Extend fractional pixel functionality to cylindrical apertures --- docs/cubeviz/plugins.rst | 7 +- .../spectral_extraction.py | 70 +++++++++++++++---- .../spectral_extraction.vue | 2 +- .../tests/test_spectral_extraction.py | 51 ++++++++++++++ 4 files changed, 112 insertions(+), 18 deletions(-) diff --git a/docs/cubeviz/plugins.rst b/docs/cubeviz/plugins.rst index 668d816a68..bf22fe220a 100644 --- a/docs/cubeviz/plugins.rst +++ b/docs/cubeviz/plugins.rst @@ -291,7 +291,8 @@ Click :guilabel:`EXTRACT` to produce a new 1D spectrum dataset from the spectral cube, which has uncertainties propagated by `astropy.nddata `_. -If using a simple subset for the spatial aperture, an option to +If using a simple subset (currently only a circular subset works for this) +for the spatial aperture, an option to make the aperture wavelength dependent will appear. If checked, this will create a cone aperture that increases linearly with wavelength. The formula for that is:: @@ -305,7 +306,9 @@ The reference wavelength for the cone can be changed using the The method of aperture masking can also be changed using the :guilabel:`Aperture masking method` dropdown. To see a description for each of these options, please see -:ref:`photutils:photutils-aperture-overlap`. +:ref:`photutils:photutils-aperture-overlap`. Using the exact aperture +method with the min or max functions is not supported. Spatial axes not in +wavelength are also not supported. .. _cubeviz-aper-phot: diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index edcc1b23bb..aa77219afb 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -85,6 +85,8 @@ class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin, aperture_method_selected = Unicode('Center').tag(sync=True) conflicting_aperture_and_function = Bool(False).tag(sync=True) + conflicting_aperture_error_message = Unicode('Aperture method Exact cannot be selected along' + ' with Min or Max.').tag(sync=True) # export_enabled controls whether saving to a file is enabled via the UI. This # is a temporary measure to allow server-installations to disable saving server-side until @@ -226,9 +228,9 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): Examples include ``propagate_uncertainties`` and ``operation_ignores_mask``. """ if self.conflicting_aperture_and_function: - self.hub.broadcast(SnackbarMessage("Aperture method Exact cannot be selected" - " along with Min or Max.", + self.hub.broadcast(SnackbarMessage(self.conflicting_aperture_error_message, color="error", sender=self)) + raise ValueError(self.conflicting_aperture_error_message) return spectral_cube = self._app._jdaviz_helper._loaded_flux_cube @@ -239,7 +241,7 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): # defaults to ``No Subset``). Since the Cubeviz parser puts the fluxes # and uncertainties in different glue Data objects, we translate the spectral # cube and its uncertainties into separate NDDataArrays, then combine them: - if self.aperture.selected != self.aperture.default_text and self.wavelength_dependent: + if self.aperture.selected != self.aperture.default_text: nddata = spectral_cube.get_subset_object( subset_id=self.aperture.selected, cls=NDDataArray ) @@ -247,21 +249,26 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): uncertainties = uncert_cube.get_subset_object( subset_id=self.aperture.selected, cls=StdDevUncertainty ) - # Exact slice mask of cone aperture through the cube. `cone_mask` is + # Exact slice mask of cone or cylindrical aperture through the cube. `shape_mask` is # a 3D array with fractions of each pixel within an aperture at each # wavelength, on the range [0, 1]. - if self.function_selected.lower() in ['min', 'max']: - cone_mask = self.cone_aperture().astype(int) + if self.function_selected.lower() in ['min', 'max'] and self.wavelength_dependent: + shape_mask = self.cone_aperture().astype(int) + elif self.function_selected.lower() in ['min', 'max']: + shape_mask = self.cylindrical_aperture().astype(int) + elif self.wavelength_dependent: + shape_mask = self.cone_aperture() else: - cone_mask = self.cone_aperture() + shape_mask = self.cylindrical_aperture() + if self.aperture_method_selected.lower() == 'center': flux = nddata.data << nddata.unit else: # Apply the fractional pixel array to the flux cube - flux = (cone_mask * nddata.data) << nddata.unit + flux = (shape_mask * nddata.data) << nddata.unit # Boolean cube which is True outside of the aperture # (i.e., the numpy boolean mask convention) - mask = np.isclose(cone_mask, 0) + mask = np.isclose(shape_mask, 0) elif self.aperture.selected != self.aperture.default_text: nddata = spectral_cube.get_subset_object( @@ -297,17 +304,20 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): # is always wavelength. This may need adjustment after the following # specutils PR is merged: https://github.com/astropy/specutils/pull/1033 spatial_axes = (0, 1) - if self.wavelength_dependent and self.function_selected.lower() == 'mean': + if self.function_selected.lower() == 'mean': # Use built-in sum function to collapse NDDataArray collapsed_for_mean = nddata_reshaped.sum(axis=spatial_axes, **kwargs) + # But we still need the mean function for everything except flux + collapsed_as_mean = nddata_reshaped.mean(axis=spatial_axes, **kwargs) + # Then normalize the flux based on the fractional pixel array flux_for_mean = (collapsed_for_mean.data / - np.sum(cone_mask, axis=spatial_axes)) << nddata_reshaped.unit + np.sum(shape_mask, axis=spatial_axes)) << nddata_reshaped.unit # Combine that information into a new NDDataArray - collapsed_nddata = NDDataArray(flux_for_mean, mask=collapsed_for_mean.mask, - uncertainty=collapsed_for_mean.uncertainty, - wcs=collapsed_for_mean.wcs, - meta=collapsed_for_mean.meta) + collapsed_nddata = NDDataArray(flux_for_mean, mask=collapsed_as_mean.mask, + uncertainty=collapsed_as_mean.uncertainty, + wcs=collapsed_as_mean.wcs, + meta=collapsed_as_mean.meta) else: collapsed_nddata = getattr(nddata_reshaped, self.function_selected.lower())( axis=spatial_axes, **kwargs @@ -380,6 +390,36 @@ def cone_aperture(self): masks_weights[:, :, index] = slice_mask return masks_weights + def cylindrical_aperture(self): + # Retrieve flux cube and create an array to represent the cone mask + flux_cube = self._app._jdaviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, + statistic=None) + # TODO: Replace with code for retrieving display_unit in cubeviz when it is available + display_unit = flux_cube.spectral_axis.unit + if display_unit.physical_type != 'length': + self.hub.broadcast(SnackbarMessage('Spectral axis unit physical type is ' + f'{display_unit.physical_type}, must be length', + color="error", sender=self)) + return + + masks_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) + + # Center is reverse coordinates + center = (self.aperture.selected_spatial_region.center.y, + self.aperture.selected_spatial_region.center.x) + + # Create a slice_mask and set each slice of a numpy cube to that mask + # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit conversion. + radius = self.aperture.selected_spatial_region.radius + im_shape = (flux_cube.shape[0], flux_cube.shape[1]) + aper_method = self.aperture_method_selected.lower() + aperture = CircularAperture(center, r=radius) + slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) + for index in range(0, len(flux_cube.spectral_axis)): + # Add slice mask to fractional pixel array + masks_weights[:, :, index] = slice_mask + return masks_weights + def vue_spectral_extraction(self, *args, **kwargs): self.collapse_to_spectrum(add_data=True) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue index d0973688d2..4116ad45b0 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue @@ -162,7 +162,7 @@
- Aperture method Exact cannot be selected along with Min or Max. + {{conflicting_aperture_error_message}} diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index e627a0daf8..b11d31eb38 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -268,3 +268,54 @@ def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_la np.testing.assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean, atol=1e-9) + + +@pytest.mark.parametrize( + ('aperture_method', 'expected_flux_1000', 'expected_flux_2400', 'expected_flux_mean'), + [('Exact', [19.6349540849] * 10, [19.6349540849] * 10, [1] * 10), + ('Center', [21] * 10, [21] * 10, [1] * 10)] +) +def test_circular_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_largest, + aperture_method, expected_flux_1000, + expected_flux_2400, expected_flux_mean): + cubeviz_helper.load_data(spectrum1d_cube_largest) + cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(14, 15), radius=2.5)]) + + extract_plg = cubeviz_helper.plugins['Spectral Extraction'] + + extract_plg.aperture = 'Subset 1' + extract_plg.aperture_method.selected = aperture_method + extract_plg.wavelength_dependent = False + extract_plg.function = 'Sum' + + collapsed_spec = extract_plg.collapse_to_spectrum() + + np.testing.assert_allclose(collapsed_spec.flux.value[1000:1010], expected_flux_1000, + atol=1e-9) + np.testing.assert_allclose(collapsed_spec.flux.value[2400:2410], expected_flux_2400, + atol=1e-9) + + extract_plg.function = 'Mean' + collapsed_spec_mean = extract_plg.collapse_to_spectrum() + + np.testing.assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean, + atol=1e-9) + + +def test_cone_and_cylinder_errors(cubeviz_helper, spectrum1d_cube_largest): + cubeviz_helper.load_data(spectrum1d_cube_largest) + cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(14, 15), radius=2.5)]) + + extract_plg = cubeviz_helper.plugins['Spectral Extraction'] + + extract_plg.aperture = 'Subset 1' + extract_plg.aperture_method.selected = 'Exact' + extract_plg.wavelength_dependent = True + extract_plg.function = 'Min' + + with pytest.raises(ValueError, match=extract_plg._obj.conflicting_aperture_error_message): + extract_plg.collapse_to_spectrum() + + extract_plg.function = 'Max' + with pytest.raises(ValueError, match=extract_plg._obj.conflicting_aperture_error_message): + extract_plg.collapse_to_spectrum() From 497e5f9f72e12553664240f59f15287d2fead9f9 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 11:14:56 -0500 Subject: [PATCH 41/56] Fix indentation --- .../spectral_extraction/tests/test_spectral_extraction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index b11d31eb38..98ead79c99 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -276,8 +276,8 @@ def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_la ('Center', [21] * 10, [21] * 10, [1] * 10)] ) def test_circular_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_largest, - aperture_method, expected_flux_1000, - expected_flux_2400, expected_flux_mean): + aperture_method, expected_flux_1000, + expected_flux_2400, expected_flux_mean): cubeviz_helper.load_data(spectrum1d_cube_largest) cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(14, 15), radius=2.5)]) From 8fc323f9a24cf4a7d12f2fc802fb83d26cfcf2c2 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 14:34:55 -0500 Subject: [PATCH 42/56] Move aperture functionality and checks to get_aperture --- .../spectral_extraction.py | 69 +++++++++++++------ jdaviz/conftest.py | 2 +- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index aa77219afb..6b236cc8db 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -231,7 +231,6 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): self.hub.broadcast(SnackbarMessage(self.conflicting_aperture_error_message, color="error", sender=self)) raise ValueError(self.conflicting_aperture_error_message) - return spectral_cube = self._app._jdaviz_helper._loaded_flux_cube uncert_cube = self._app._jdaviz_helper._loaded_uncert_cube @@ -252,14 +251,10 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): # Exact slice mask of cone or cylindrical aperture through the cube. `shape_mask` is # a 3D array with fractions of each pixel within an aperture at each # wavelength, on the range [0, 1]. - if self.function_selected.lower() in ['min', 'max'] and self.wavelength_dependent: - shape_mask = self.cone_aperture().astype(int) - elif self.function_selected.lower() in ['min', 'max']: - shape_mask = self.cylindrical_aperture().astype(int) - elif self.wavelength_dependent: - shape_mask = self.cone_aperture() - else: - shape_mask = self.cylindrical_aperture() + shape_mask = self.get_aperture() + + if self.function_selected.lower() in ['min', 'max']: + shape_mask = shape_mask.astype(int) if self.aperture_method_selected.lower() == 'center': flux = nddata.data << nddata.unit @@ -269,17 +264,6 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): # Boolean cube which is True outside of the aperture # (i.e., the numpy boolean mask convention) mask = np.isclose(shape_mask, 0) - - elif self.aperture.selected != self.aperture.default_text: - nddata = spectral_cube.get_subset_object( - subset_id=self.aperture.selected, cls=NDDataArray - ) - if uncert_cube: - uncertainties = uncert_cube.get_subset_object( - subset_id=self.aperture.selected, cls=StdDevUncertainty - ) - flux = nddata.data << nddata.unit - mask = nddata.mask else: nddata = spectral_cube.get_object(cls=NDDataArray) if uncert_cube: @@ -306,12 +290,12 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): spatial_axes = (0, 1) if self.function_selected.lower() == 'mean': # Use built-in sum function to collapse NDDataArray - collapsed_for_mean = nddata_reshaped.sum(axis=spatial_axes, **kwargs) + collapsed_sum_for_mean = nddata_reshaped.sum(axis=spatial_axes, **kwargs) # But we still need the mean function for everything except flux collapsed_as_mean = nddata_reshaped.mean(axis=spatial_axes, **kwargs) # Then normalize the flux based on the fractional pixel array - flux_for_mean = (collapsed_for_mean.data / + flux_for_mean = (collapsed_sum_for_mean.data / np.sum(shape_mask, axis=spatial_axes)) << nddata_reshaped.unit # Combine that information into a new NDDataArray collapsed_nddata = NDDataArray(flux_for_mean, mask=collapsed_as_mean.mask, @@ -358,6 +342,47 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): return collapsed_spec + def get_aperture(self): + # Retrieve flux cube and create an array to represent the cone mask + flux_cube = self._app._jdaviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, + statistic=None) + # TODO: Replace with code for retrieving display_unit in cubeviz when it is available + display_unit = flux_cube.spectral_axis.unit + if display_unit.physical_type != 'length': + self.hub.broadcast(SnackbarMessage('Spectral axis unit physical type is ' + f'{display_unit.physical_type}, must be length', + color="error", sender=self)) + return + + mask_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) + + # Center is reverse coordinates + center = (self.aperture.selected_spatial_region.center.y, + self.aperture.selected_spatial_region.center.x) + + # Loop through cube and create cone aperture at each wavelength. Then convert that to a + # weight array using the selected aperture method, and add it to a weight cube. + # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit conversion. + im_shape = (flux_cube.shape[0], flux_cube.shape[1]) + aper_method = self.aperture_method_selected.lower() + if self.wavelength_dependent: + # Cone aperture + radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * + self.aperture.selected_spatial_region.radius) + for index, radius in enumerate(radii): + aperture = CircularAperture(center, r=radius) + slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) + # Add slice mask to fractional pixel array + mask_weights[:, :, index] = slice_mask + else: + # Cylindrical aperture + radius = self.aperture.selected_spatial_region.radius + aperture = CircularAperture(center, r=radius) + slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) + # Turn 2D slice_mask into 3D array that is the same shape as the flux cube + mask_weights = np.stack([slice_mask] * len(flux_cube.spectral_axis), axis=2) + return mask_weights + def cone_aperture(self): # Retrieve flux cube and create an array to represent the cone mask flux_cube = self._app._jdaviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, diff --git a/jdaviz/conftest.py b/jdaviz/conftest.py index 868d6d9cfc..5bf423275b 100644 --- a/jdaviz/conftest.py +++ b/jdaviz/conftest.py @@ -240,7 +240,7 @@ def spectrum1d_cube_larger(): @pytest.fixture def spectrum1d_cube_largest(): - flux = np.ones((30, 30, 3001)).astype(int) + flux = np.ones((30, 30, 3001)) wcs_dict = {"CTYPE1": "WAVE-LOG", "CTYPE2": "DEC--TAN", "CTYPE3": "RA---TAN", "CRVAL1": 4.622e-7, "CRVAL2": 27, "CRVAL3": 205, From 2c1dae429cb6bd6ea03e78c02364646deebd794c Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 14:38:30 -0500 Subject: [PATCH 43/56] Remove snackbar message --- .../cubeviz/plugins/spectral_extraction/spectral_extraction.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 6b236cc8db..185cb9086b 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -228,8 +228,6 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): Examples include ``propagate_uncertainties`` and ``operation_ignores_mask``. """ if self.conflicting_aperture_and_function: - self.hub.broadcast(SnackbarMessage(self.conflicting_aperture_error_message, - color="error", sender=self)) raise ValueError(self.conflicting_aperture_error_message) spectral_cube = self._app._jdaviz_helper._loaded_flux_cube From 567053256cef8b810994932c9c564d6b2301da40 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 14:40:17 -0500 Subject: [PATCH 44/56] Update test name --- .../spectral_extraction/tests/test_spectral_extraction.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 98ead79c99..03049dbbf6 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -275,9 +275,9 @@ def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_la [('Exact', [19.6349540849] * 10, [19.6349540849] * 10, [1] * 10), ('Center', [21] * 10, [21] * 10, [1] * 10)] ) -def test_circular_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_largest, - aperture_method, expected_flux_1000, - expected_flux_2400, expected_flux_mean): +def test_cylindrical_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_largest, + aperture_method, expected_flux_1000, + expected_flux_2400, expected_flux_mean): cubeviz_helper.load_data(spectrum1d_cube_largest) cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(14, 15), radius=2.5)]) From 1d7944dbfd85c39961fb1994c2c82570f0060e96 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 14:46:45 -0500 Subject: [PATCH 45/56] Move display unit check to cone aperture section --- .../spectral_extraction/spectral_extraction.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 185cb9086b..9cf10e0e28 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -346,13 +346,6 @@ def get_aperture(self): statistic=None) # TODO: Replace with code for retrieving display_unit in cubeviz when it is available display_unit = flux_cube.spectral_axis.unit - if display_unit.physical_type != 'length': - self.hub.broadcast(SnackbarMessage('Spectral axis unit physical type is ' - f'{display_unit.physical_type}, must be length', - color="error", sender=self)) - return - - mask_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) # Center is reverse coordinates center = (self.aperture.selected_spatial_region.center.y, @@ -365,6 +358,12 @@ def get_aperture(self): aper_method = self.aperture_method_selected.lower() if self.wavelength_dependent: # Cone aperture + if display_unit.physical_type != 'length': + error_msg = (f'Spectral axis unit physical type is {display_unit.physical_type},' + f' must be length for cone aperture') + self.hub.broadcast(SnackbarMessage(error_msg, color="error", sender=self)) + raise ValueError(error_msg) + mask_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * self.aperture.selected_spatial_region.radius) for index, radius in enumerate(radii): From 3efe0fff68d1b338728432fd4da630aed3f565f0 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 14:49:44 -0500 Subject: [PATCH 46/56] Move commentS --- .../plugins/spectral_extraction/spectral_extraction.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 9cf10e0e28..f9dcc6cfa8 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -351,9 +351,6 @@ def get_aperture(self): center = (self.aperture.selected_spatial_region.center.y, self.aperture.selected_spatial_region.center.x) - # Loop through cube and create cone aperture at each wavelength. Then convert that to a - # weight array using the selected aperture method, and add it to a weight cube. - # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit conversion. im_shape = (flux_cube.shape[0], flux_cube.shape[1]) aper_method = self.aperture_method_selected.lower() if self.wavelength_dependent: @@ -364,8 +361,12 @@ def get_aperture(self): self.hub.broadcast(SnackbarMessage(error_msg, color="error", sender=self)) raise ValueError(error_msg) mask_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) + # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit + # conversion. radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * self.aperture.selected_spatial_region.radius) + # Loop through cube and create cone aperture at each wavelength. Then convert that to a + # weight array using the selected aperture method, and add it to a weight cube. for index, radius in enumerate(radii): aperture = CircularAperture(center, r=radius) slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) From f26ab36a36badb215198644726b70eaa47f4fbe2 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 14:54:22 -0500 Subject: [PATCH 47/56] Move radius --- .../plugins/spectral_extraction/spectral_extraction.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index f9dcc6cfa8..d97c99b66a 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -353,6 +353,7 @@ def get_aperture(self): im_shape = (flux_cube.shape[0], flux_cube.shape[1]) aper_method = self.aperture_method_selected.lower() + radius = self.aperture.selected_spatial_region.radius if self.wavelength_dependent: # Cone aperture if display_unit.physical_type != 'length': @@ -363,18 +364,16 @@ def get_aperture(self): mask_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit # conversion. - radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * - self.aperture.selected_spatial_region.radius) + radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * radius) # Loop through cube and create cone aperture at each wavelength. Then convert that to a # weight array using the selected aperture method, and add it to a weight cube. - for index, radius in enumerate(radii): - aperture = CircularAperture(center, r=radius) + for index, cone_radius in enumerate(radii): + aperture = CircularAperture(center, r=cone_radius) slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) # Add slice mask to fractional pixel array mask_weights[:, :, index] = slice_mask else: # Cylindrical aperture - radius = self.aperture.selected_spatial_region.radius aperture = CircularAperture(center, r=radius) slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) # Turn 2D slice_mask into 3D array that is the same shape as the flux cube From da78654c6326c885df268e880ed1c239f46a0f8c Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 15:00:08 -0500 Subject: [PATCH 48/56] Clarify when physical type must be wavelength --- docs/cubeviz/plugins.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/cubeviz/plugins.rst b/docs/cubeviz/plugins.rst index bf22fe220a..086848959b 100644 --- a/docs/cubeviz/plugins.rst +++ b/docs/cubeviz/plugins.rst @@ -291,13 +291,13 @@ Click :guilabel:`EXTRACT` to produce a new 1D spectrum dataset from the spectral cube, which has uncertainties propagated by `astropy.nddata `_. -If using a simple subset (currently only a circular subset works for this) -for the spatial aperture, an option to +If using a simple subset (currently only works for a circular subset applied to data +with spatial axis units in wavelength) for the spatial aperture, an option to make the aperture wavelength dependent will appear. If checked, this will create a cone aperture that increases linearly with wavelength. The formula for that is:: - radius = ((slice_wavelength / reference_wavelength) * + radii = ((all_wavelengths / reference_wavelength) * aperture.selected_spatial_region.radius) The reference wavelength for the cone can be changed using the @@ -307,8 +307,7 @@ The method of aperture masking can also be changed using the :guilabel:`Aperture masking method` dropdown. To see a description for each of these options, please see :ref:`photutils:photutils-aperture-overlap`. Using the exact aperture -method with the min or max functions is not supported. Spatial axes not in -wavelength are also not supported. +method with the min or max functions is not supported. .. _cubeviz-aper-phot: From a3ad411b83fcb3c80d811d3656f96d349a02b671 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 15:04:40 -0500 Subject: [PATCH 49/56] Remove old methods --- .../spectral_extraction.py | 62 ------------------- 1 file changed, 62 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index d97c99b66a..975c603959 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -380,68 +380,6 @@ def get_aperture(self): mask_weights = np.stack([slice_mask] * len(flux_cube.spectral_axis), axis=2) return mask_weights - def cone_aperture(self): - # Retrieve flux cube and create an array to represent the cone mask - flux_cube = self._app._jdaviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, - statistic=None) - # TODO: Replace with code for retrieving display_unit in cubeviz when it is available - display_unit = flux_cube.spectral_axis.unit - if display_unit.physical_type != 'length': - self.hub.broadcast(SnackbarMessage('Spectral axis unit physical type is ' - f'{display_unit.physical_type}, must be length', - color="error", sender=self)) - return - - masks_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) - - # Center is reverse coordinates - center = (self.aperture.selected_spatial_region.center.y, - self.aperture.selected_spatial_region.center.x) - - # Loop through cube and create cone aperture at each wavelength. Then convert that to a - # weight array using the selected aperture method, and add it to a weight cube. - # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit conversion. - radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * - self.aperture.selected_spatial_region.radius) - im_shape = (flux_cube.shape[0], flux_cube.shape[1]) - aper_method = self.aperture_method_selected.lower() - for index, radius in enumerate(radii): - aperture = CircularAperture(center, r=radius) - slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) - # Add slice mask to fractional pixel array - masks_weights[:, :, index] = slice_mask - return masks_weights - - def cylindrical_aperture(self): - # Retrieve flux cube and create an array to represent the cone mask - flux_cube = self._app._jdaviz_helper._loaded_flux_cube.get_object(cls=Spectrum1D, - statistic=None) - # TODO: Replace with code for retrieving display_unit in cubeviz when it is available - display_unit = flux_cube.spectral_axis.unit - if display_unit.physical_type != 'length': - self.hub.broadcast(SnackbarMessage('Spectral axis unit physical type is ' - f'{display_unit.physical_type}, must be length', - color="error", sender=self)) - return - - masks_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) - - # Center is reverse coordinates - center = (self.aperture.selected_spatial_region.center.y, - self.aperture.selected_spatial_region.center.x) - - # Create a slice_mask and set each slice of a numpy cube to that mask - # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit conversion. - radius = self.aperture.selected_spatial_region.radius - im_shape = (flux_cube.shape[0], flux_cube.shape[1]) - aper_method = self.aperture_method_selected.lower() - aperture = CircularAperture(center, r=radius) - slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) - for index in range(0, len(flux_cube.spectral_axis)): - # Add slice mask to fractional pixel array - masks_weights[:, :, index] = slice_mask - return masks_weights - def vue_spectral_extraction(self, *args, **kwargs): self.collapse_to_spectrum(add_data=True) From 628b9195aa8e1e9cbd9e4797fd8568451e7fc57a Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 15:07:07 -0500 Subject: [PATCH 50/56] Change shape of test data --- jdaviz/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/conftest.py b/jdaviz/conftest.py index 5bf423275b..65173e4c95 100644 --- a/jdaviz/conftest.py +++ b/jdaviz/conftest.py @@ -240,7 +240,7 @@ def spectrum1d_cube_larger(): @pytest.fixture def spectrum1d_cube_largest(): - flux = np.ones((30, 30, 3001)) + flux = np.ones((30, 29, 3001)) wcs_dict = {"CTYPE1": "WAVE-LOG", "CTYPE2": "DEC--TAN", "CTYPE3": "RA---TAN", "CRVAL1": 4.622e-7, "CRVAL2": 27, "CRVAL3": 205, From 40c3fedb426bcab1307101775cd7061e43bdaf69 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 15:35:14 -0500 Subject: [PATCH 51/56] Add test for non-wavelength axis units --- .../tests/test_spectral_extraction.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 03049dbbf6..0ee6534ff7 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -319,3 +319,19 @@ def test_cone_and_cylinder_errors(cubeviz_helper, spectrum1d_cube_largest): extract_plg.function = 'Max' with pytest.raises(ValueError, match=extract_plg._obj.conflicting_aperture_error_message): extract_plg.collapse_to_spectrum() + + +def test_cone_aperture_with_frequency_units(cubeviz_helper, spectral_cube_wcs): + data = Spectrum1D(flux=np.ones((128, 129, 256)) * astropy.units.nJy, wcs=spectral_cube_wcs) + cubeviz_helper.load_data(data, data_label="Test Flux") + cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(14, 15), radius=2.5)]) + + extract_plg = cubeviz_helper.plugins['Spectral Extraction'] + + extract_plg.aperture = 'Subset 1' + extract_plg.aperture_method.selected = 'Exact' + extract_plg.wavelength_dependent = True + extract_plg.function = 'Sum' + + with pytest.raises(ValueError, match="Spectral axis unit physical type is"): + extract_plg.collapse_to_spectrum() From 97090bbbd0f357feb5de621c93c1f81c6ab0bd35 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 27 Feb 2024 19:06:26 -0500 Subject: [PATCH 52/56] pllim code clean-up and add tests as part of JDAT-4194 --- CHANGES.rst | 4 +- .../spectral_extraction.py | 68 +++----------- .../tests/test_spectral_extraction.py | 88 ++++++++----------- 3 files changed, 51 insertions(+), 109 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 77e8604364..8c6fff0170 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,8 +13,6 @@ New Features - Live-preview of aperture selection in plugins. [#2664, #2684] -- Add conical aperture support to cubeviz in the spectral extraction plugin. [#2679] - Cubeviz ^^^^^^^ @@ -28,6 +26,8 @@ Cubeviz - Spectral extraction plugin re-organized into subsections to be more consistent with specviz2d. [#2676] +- Add conical aperture support to cubeviz in the spectral extraction plugin. [#2679] + - New aperture photometry plugin that can perform aperture photometry on selected cube slice. [#2666] Imviz diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 975c603959..c82d7fc8b2 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -1,15 +1,14 @@ import os from pathlib import Path -from packaging.version import Version import numpy as np import astropy -import astropy.units as u from astropy.utils.decorators import deprecated from astropy.nddata import ( - NDDataArray, StdDevUncertainty, NDUncertainty + NDDataArray, StdDevUncertainty ) from traitlets import Any, Bool, Dict, Float, List, Unicode, observe +from packaging.version import Version from photutils.aperture import CircularAperture from specutils import Spectrum1D @@ -207,7 +206,7 @@ def _update_mark_scale(self, *args): @observe('function_selected', 'aperture_method_selected') def _update_aperture_method_on_function_change(self, *args): - if (self.function_selected.lower() in ['min', 'max'] and + if (self.function_selected.lower() in ('min', 'max') and self.aperture_method_selected.lower() != 'center'): self.conflicting_aperture_and_function = True else: @@ -233,6 +232,7 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): spectral_cube = self._app._jdaviz_helper._loaded_flux_cube uncert_cube = self._app._jdaviz_helper._loaded_uncert_cube uncertainties = None + selected_func = self.function_selected.lower() # This plugin collapses over the *spatial axes* (optionally over a spatial subset, # defaults to ``No Subset``). Since the Cubeviz parser puts the fluxes @@ -251,12 +251,9 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): # wavelength, on the range [0, 1]. shape_mask = self.get_aperture() - if self.function_selected.lower() in ['min', 'max']: - shape_mask = shape_mask.astype(int) - if self.aperture_method_selected.lower() == 'center': flux = nddata.data << nddata.unit - else: + else: # exact (min/max not allowed here) # Apply the fractional pixel array to the flux cube flux = (shape_mask * nddata.data) << nddata.unit # Boolean cube which is True outside of the aperture @@ -286,7 +283,7 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): # is always wavelength. This may need adjustment after the following # specutils PR is merged: https://github.com/astropy/specutils/pull/1033 spatial_axes = (0, 1) - if self.function_selected.lower() == 'mean': + if selected_func == 'mean': # Use built-in sum function to collapse NDDataArray collapsed_sum_for_mean = nddata_reshaped.sum(axis=spatial_axes, **kwargs) # But we still need the mean function for everything except flux @@ -301,7 +298,7 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): wcs=collapsed_as_mean.wcs, meta=collapsed_as_mean.meta) else: - collapsed_nddata = getattr(nddata_reshaped, self.function_selected.lower())( + collapsed_nddata = getattr(nddata_reshaped, selected_func)( axis=spatial_axes, **kwargs ) # returns an NDDataArray @@ -325,7 +322,7 @@ def collapse_to_spectrum(self, add_data=True, **kwargs): self.extracted_spec = collapsed_spec self.extracted_spec_available = True fname_label = self.dataset_selected.replace("[", "_").replace("]", "") - self.filename = f"extracted_{self.function_selected.lower()}_{fname_label}.fits" + self.filename = f"extracted_{selected_func}_{fname_label}.fits" if add_data: self.add_results.add_results_from_plugin( @@ -358,12 +355,11 @@ def get_aperture(self): # Cone aperture if display_unit.physical_type != 'length': error_msg = (f'Spectral axis unit physical type is {display_unit.physical_type},' - f' must be length for cone aperture') + ' must be length for cone aperture') self.hub.broadcast(SnackbarMessage(error_msg, color="error", sender=self)) raise ValueError(error_msg) mask_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) - # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit - # conversion. + # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit conversion. radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * radius) # Loop through cube and create cone aperture at each wavelength. Then convert that to a # weight array using the selected aperture method, and add it to a weight cube. @@ -437,47 +433,3 @@ def _set_default_results_label(self, event={}): ): label += f' ({self.aperture_selected})' self.results_label_default = label - - -def _move_spectral_axis(wcs, flux, mask=None, uncertainty=None): - """ - Move spectral axis last to match specutils convention. This - function borrows from: - https://github.com/astropy/specutils/blob/ - 6eb7f96498072882c97763d4cd10e07cf81b6d33/specutils/spectra/spectrum1d.py#L185-L225 - """ - naxis = getattr(wcs, 'naxis', len(wcs.world_axis_physical_types)) - if naxis > 1: - temp_axes = [] - phys_axes = wcs.world_axis_physical_types - for i in range(len(phys_axes)): - if phys_axes[i] is None: - continue - if phys_axes[i][0:2] == "em" or phys_axes[i][0:5] == "spect": - temp_axes.append(i) - if len(temp_axes) != 1: - raise ValueError("Input WCS must have exactly one axis with " - "spectral units, found {}".format(len(temp_axes))) - - # Due to FITS conventions, a WCS with spectral axis first corresponds - # to a flux array with spectral axis last. - if temp_axes[0] != 0: - wcs = wcs.swapaxes(0, temp_axes[0]) - if flux is not None: - flux = np.swapaxes(flux, len(flux.shape) - temp_axes[0] - 1, -1) - if mask is not None: - mask = np.swapaxes(mask, len(mask.shape) - temp_axes[0] - 1, -1) - if uncertainty is not None: - if isinstance(uncertainty, NDUncertainty): - # Account for Astropy uncertainty types - unc_len = len(uncertainty.array.shape) - temp_unc = np.swapaxes(uncertainty.array, - unc_len - temp_axes[0] - 1, -1) - if uncertainty.unit is not None: - temp_unc = temp_unc * u.Unit(uncertainty.unit) - uncertainty = type(uncertainty)(temp_unc) - else: - uncertainty = np.swapaxes(uncertainty, - len(uncertainty.shape) - - temp_axes[0] - 1, -1) - return wcs, flux, mask, uncertainty diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 0ee6534ff7..3cc6f04f9b 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -1,24 +1,16 @@ -import os import pytest -from packaging.version import Version + +pytest.importorskip("astropy", minversion="5.3.2") + import numpy as np -import astropy +from astropy import units as u from astropy.nddata import NDDataArray, StdDevUncertainty -from specutils import Spectrum1D -from regions import CirclePixelRegion, PixCoord from astropy.utils.exceptions import AstropyUserWarning - -ASTROPY_LT_5_3_2 = Version(astropy.__version__) < Version('5.3.2') - - -@pytest.mark.skipif(not ASTROPY_LT_5_3_2, reason='Needs astropy <5.3.2') -def test_version_before_nddata_update(cubeviz_helper, spectrum1d_cube_with_uncerts): - # Also test that plugin is disabled before data is loaded. - plg = cubeviz_helper.plugins['Spectral Extraction'] - assert plg._obj.disabled_msg != '' +from numpy.testing import assert_allclose, assert_array_equal +from regions import CirclePixelRegion, EllipsePixelRegion, PixCoord +from specutils import Spectrum1D -@pytest.mark.skipif(ASTROPY_LT_5_3_2, reason='Needs astropy 5.3.2 or later') def test_version_after_nddata_update(cubeviz_helper, spectrum1d_cube_with_uncerts): # Also test that plugin is disabled before data is loaded. plg = cubeviz_helper.plugins['Spectral Extraction'] @@ -41,13 +33,12 @@ def test_version_after_nddata_update(cubeviz_helper, spectrum1d_cube_with_uncert assert isinstance(spectral_cube, NDDataArray) assert isinstance(collapsed_cube_s1d, Spectrum1D) - np.testing.assert_allclose( + assert_allclose( collapsed_cube_nddata.data, collapsed_cube_s1d.flux.to_value(collapsed_cube_nddata.unit) ) -@pytest.mark.skipif(ASTROPY_LT_5_3_2, reason='Needs astropy 5.3.2 or later') def test_gauss_smooth_before_spec_extract(cubeviz_helper, spectrum1d_cube_with_uncerts): # Also test if gaussian smooth plugin is run before spec extract # that spec extract yields results of correct cube data @@ -91,21 +82,17 @@ def test_gauss_smooth_before_spec_extract(cubeviz_helper, spectrum1d_cube_with_u # this single pixel has two wavelengths, and all uncertainties are unity # irrespective of which collapse function is applied: assert len(collapsed_spec.flux) == 2 - assert np.all(np.equal(collapsed_spec.uncertainty.array, 1)) + assert_array_equal(collapsed_spec.uncertainty.array, 1) # this two-pixel region has four unmasked data points per wavelength: extract_plugin.aperture = 'Subset 2' collapsed_spec_2 = extract_plugin.collapse_to_spectrum() - assert np.all(np.equal(collapsed_spec_2.uncertainty.array, expected_uncert)) + assert_array_equal(collapsed_spec_2.uncertainty.array, expected_uncert) -@pytest.mark.skipif(ASTROPY_LT_5_3_2, reason='Needs astropy 5.3.2 or later') @pytest.mark.parametrize( - "function, expected_uncert", - zip( - ["Sum", "Mean", "Min", "Max"], - [2, 0.5, 1, 1] - ) + ("function, expected_uncert"), + [("Sum", 2), ("Mean", 0.5), ("Min", 1), ("Max", 1)] ) def test_subset( cubeviz_helper, spectrum1d_cube_with_uncerts, function, expected_uncert @@ -135,16 +122,16 @@ def test_subset( # this single pixel has two wavelengths, and all uncertainties are unity # irrespective of which collapse function is applied: assert len(collapsed_spec_1.flux) == 2 - assert np.all(np.equal(collapsed_spec_1.uncertainty.array, 1)) + assert_array_equal(collapsed_spec_1.uncertainty.array, 1) # this two-pixel region has four unmasked data points per wavelength: plg.aperture = 'Subset 2' collapsed_spec_2 = plg.collapse_to_spectrum() - assert np.all(np.equal(collapsed_spec_2.uncertainty.array, expected_uncert)) + assert_array_equal(collapsed_spec_2.uncertainty.array, expected_uncert) -def test_save_collapsed_to_fits(cubeviz_helper, spectrum1d_cube_with_uncerts, tmpdir): +def test_save_collapsed_to_fits(cubeviz_helper, spectrum1d_cube_with_uncerts, tmp_path): cubeviz_helper.load_data(spectrum1d_cube_with_uncerts) @@ -161,23 +148,24 @@ def test_save_collapsed_to_fits(cubeviz_helper, spectrum1d_cube_with_uncerts, tm # check that default filename is correct, then change path fname = 'extracted_sum_Unknown spectrum object_FLUX.fits' + fname_path = tmp_path / fname assert extract_plugin._obj.filename == fname - extract_plugin._obj.filename = os.path.join(tmpdir, fname) + extract_plugin._obj.filename = str(fname_path) # save output file with default name, make sure it exists extract_plugin._obj.vue_save_as_fits() - assert os.path.isfile(os.path.join(tmpdir, fname)) + assert fname_path.is_file() # read file back in, make sure it matches - dat = Spectrum1D.read(os.path.join(tmpdir, fname)) - assert np.all(dat.data == extract_plugin._obj.extracted_spec.data) + dat = Spectrum1D.read(fname_path) + assert_array_equal(dat.data, extract_plugin._obj.extracted_spec.data) assert dat.unit == extract_plugin._obj.extracted_spec.unit # make sure correct error message is raised when export_enabled is False # this won't appear in UI, but just to be safe. extract_plugin._obj.export_enabled = False - msg = "Writing out extracted spectrum to file is currently disabled" - with pytest.raises(ValueError, match=msg): + with pytest.raises( + ValueError, match="Writing out extracted spectrum to file is currently disabled"): extract_plugin._obj.vue_save_as_fits() extract_plugin._obj.export_enabled = True # set back to True @@ -258,16 +246,13 @@ def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_la collapsed_spec = extract_plg.collapse_to_spectrum() - np.testing.assert_allclose(collapsed_spec.flux.value[1000:1010], expected_flux_1000, - atol=1e-9) - np.testing.assert_allclose(collapsed_spec.flux.value[2400:2410], expected_flux_2400, - atol=1e-9) + assert_allclose(collapsed_spec.flux.value[1000:1010], expected_flux_1000, atol=1e-9) + assert_allclose(collapsed_spec.flux.value[2400:2410], expected_flux_2400, atol=1e-9) extract_plg.function = 'Mean' collapsed_spec_mean = extract_plg.collapse_to_spectrum() - np.testing.assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean, - atol=1e-9) + assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean, atol=1e-9) @pytest.mark.parametrize( @@ -290,29 +275,28 @@ def test_cylindrical_aperture_with_different_methods(cubeviz_helper, spectrum1d_ collapsed_spec = extract_plg.collapse_to_spectrum() - np.testing.assert_allclose(collapsed_spec.flux.value[1000:1010], expected_flux_1000, - atol=1e-9) - np.testing.assert_allclose(collapsed_spec.flux.value[2400:2410], expected_flux_2400, - atol=1e-9) + assert_allclose(collapsed_spec.flux.value[1000:1010], expected_flux_1000, atol=1e-9) + assert_allclose(collapsed_spec.flux.value[2400:2410], expected_flux_2400, atol=1e-9) extract_plg.function = 'Mean' collapsed_spec_mean = extract_plg.collapse_to_spectrum() - np.testing.assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean, - atol=1e-9) + assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean, atol=1e-9) def test_cone_and_cylinder_errors(cubeviz_helper, spectrum1d_cube_largest): cubeviz_helper.load_data(spectrum1d_cube_largest) - cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(14, 15), radius=2.5)]) + cubeviz_helper.load_regions([ + CirclePixelRegion(PixCoord(14, 15), radius=2.5), + EllipsePixelRegion(center=PixCoord(x=10.5, y=12), width=5, height=3)]) extract_plg = cubeviz_helper.plugins['Spectral Extraction'] extract_plg.aperture = 'Subset 1' extract_plg.aperture_method.selected = 'Exact' extract_plg.wavelength_dependent = True - extract_plg.function = 'Min' + extract_plg.function = 'Min' with pytest.raises(ValueError, match=extract_plg._obj.conflicting_aperture_error_message): extract_plg.collapse_to_spectrum() @@ -320,9 +304,15 @@ def test_cone_and_cylinder_errors(cubeviz_helper, spectrum1d_cube_largest): with pytest.raises(ValueError, match=extract_plg._obj.conflicting_aperture_error_message): extract_plg.collapse_to_spectrum() + extract_plg.function = 'Sum' + extract_plg.aperture = 'Subset 2' + # FIXME: https://jira.stsci.edu/browse/JDAT-4268 + with pytest.raises(AttributeError, match=".* object has no attribute 'radius'"): + extract_plg.collapse_to_spectrum() + def test_cone_aperture_with_frequency_units(cubeviz_helper, spectral_cube_wcs): - data = Spectrum1D(flux=np.ones((128, 129, 256)) * astropy.units.nJy, wcs=spectral_cube_wcs) + data = Spectrum1D(flux=np.ones((128, 129, 256)) * u.nJy, wcs=spectral_cube_wcs) cubeviz_helper.load_data(data, data_label="Test Flux") cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(14, 15), radius=2.5)]) From e43992e581737beba1344b07a3fb22d91c0f47c3 Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Tue, 27 Feb 2024 20:54:14 -0500 Subject: [PATCH 53/56] Add shape check in cone section of get_aperture --- .../spectral_extraction/spectral_extraction.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index c82d7fc8b2..cc94de83c6 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -23,6 +23,7 @@ AddResultsMixin, with_spinner) from jdaviz.core.user_api import PluginUserApi +from jdaviz.core.region_translators import regions2aperture from jdaviz.configs.cubeviz.plugins.parsers import _return_spectrum_with_correct_units @@ -350,7 +351,6 @@ def get_aperture(self): im_shape = (flux_cube.shape[0], flux_cube.shape[1]) aper_method = self.aperture_method_selected.lower() - radius = self.aperture.selected_spatial_region.radius if self.wavelength_dependent: # Cone aperture if display_unit.physical_type != 'length': @@ -358,9 +358,15 @@ def get_aperture(self): ' must be length for cone aperture') self.hub.broadcast(SnackbarMessage(error_msg, color="error", sender=self)) raise ValueError(error_msg) + mask_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) + # Remove when cone support is extended to other shapes + if not hasattr(self.aperture.selected_spatial_region, 'radius'): + raise AttributeError(f"{self.aperture.selected_spatial_region.__str__()} object has" + " no attribute 'radius'") # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit conversion. - radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * radius) + radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * + self.aperture.selected_spatial_region.radius) # Loop through cube and create cone aperture at each wavelength. Then convert that to a # weight array using the selected aperture method, and add it to a weight cube. for index, cone_radius in enumerate(radii): @@ -370,7 +376,8 @@ def get_aperture(self): mask_weights[:, :, index] = slice_mask else: # Cylindrical aperture - aperture = CircularAperture(center, r=radius) + # aperture = CircularAperture(center, r=self.aperture.selected_spatial_region.radius) + aperture = regions2aperture(self.aperture.selected_spatial_region) slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) # Turn 2D slice_mask into 3D array that is the same shape as the flux cube mask_weights = np.stack([slice_mask] * len(flux_cube.spectral_axis), axis=2) From 0827ee627dc0ba7bb954d5d230e69255b091a61e Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 27 Feb 2024 21:24:46 -0500 Subject: [PATCH 54/56] Make test case off-center --- .../tests/test_spectral_extraction.py | 22 +++++++++---------- jdaviz/conftest.py | 9 +++----- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index 3cc6f04f9b..a3007f36a3 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -235,7 +235,7 @@ def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_la aperture_method, expected_flux_1000, expected_flux_2400, expected_flux_mean): cubeviz_helper.load_data(spectrum1d_cube_largest) - cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(14, 15), radius=2.5)]) + cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(5, 10), radius=2.5)]) extract_plg = cubeviz_helper.plugins['Spectral Extraction'] @@ -246,13 +246,13 @@ def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_la collapsed_spec = extract_plg.collapse_to_spectrum() - assert_allclose(collapsed_spec.flux.value[1000:1010], expected_flux_1000, atol=1e-9) - assert_allclose(collapsed_spec.flux.value[2400:2410], expected_flux_2400, atol=1e-9) + assert_allclose(collapsed_spec.flux.value[1000:1010], expected_flux_1000, rtol=1e-6) + assert_allclose(collapsed_spec.flux.value[2400:2410], expected_flux_2400, rtol=1e-6) extract_plg.function = 'Mean' collapsed_spec_mean = extract_plg.collapse_to_spectrum() - assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean, atol=1e-9) + assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean, rtol=1e-6) @pytest.mark.parametrize( @@ -263,8 +263,8 @@ def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_la def test_cylindrical_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_largest, aperture_method, expected_flux_1000, expected_flux_2400, expected_flux_mean): - cubeviz_helper.load_data(spectrum1d_cube_largest) - cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(14, 15), radius=2.5)]) + cubeviz_helper.load_data(spectrum1d_cube_largest, data_label="test") + cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(5, 10), radius=2.5)]) extract_plg = cubeviz_helper.plugins['Spectral Extraction'] @@ -275,20 +275,20 @@ def test_cylindrical_aperture_with_different_methods(cubeviz_helper, spectrum1d_ collapsed_spec = extract_plg.collapse_to_spectrum() - assert_allclose(collapsed_spec.flux.value[1000:1010], expected_flux_1000, atol=1e-9) - assert_allclose(collapsed_spec.flux.value[2400:2410], expected_flux_2400, atol=1e-9) + assert_allclose(collapsed_spec.flux.value[1000:1010], expected_flux_1000) + assert_allclose(collapsed_spec.flux.value[2400:2410], expected_flux_2400) extract_plg.function = 'Mean' collapsed_spec_mean = extract_plg.collapse_to_spectrum() - assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean, atol=1e-9) + assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean) def test_cone_and_cylinder_errors(cubeviz_helper, spectrum1d_cube_largest): cubeviz_helper.load_data(spectrum1d_cube_largest) cubeviz_helper.load_regions([ - CirclePixelRegion(PixCoord(14, 15), radius=2.5), - EllipsePixelRegion(center=PixCoord(x=10.5, y=12), width=5, height=3)]) + CirclePixelRegion(PixCoord(5, 10), radius=2.5), + EllipsePixelRegion(center=PixCoord(x=7, y=12), width=5, height=3)]) extract_plg = cubeviz_helper.plugins['Spectral Extraction'] diff --git a/jdaviz/conftest.py b/jdaviz/conftest.py index 65173e4c95..56f7a73d0a 100644 --- a/jdaviz/conftest.py +++ b/jdaviz/conftest.py @@ -240,17 +240,14 @@ def spectrum1d_cube_larger(): @pytest.fixture def spectrum1d_cube_largest(): - flux = np.ones((30, 29, 3001)) - wcs_dict = {"CTYPE1": "WAVE-LOG", "CTYPE2": "DEC--TAN", "CTYPE3": "RA---TAN", "CRVAL1": 4.622e-7, "CRVAL2": 27, "CRVAL3": 205, "CDELT1": 8e-11, "CDELT2": 0.0001, "CDELT3": -0.0001, "CRPIX1": 0, "CRPIX2": 0, "CRPIX3": 0} - w = WCS(wcs_dict) - - spec3d = Spectrum1D(flux=flux * u.Jy, wcs=w) - return spec3d + flux = np.zeros((30, 20, 3001), dtype=np.float32) # nx=20 ny=30 nz=3001 + flux[5:15, 1:11, :] = 1 # Bright corner + return Spectrum1D(flux=flux * u.Jy, wcs=w, meta=wcs_dict) @pytest.fixture From 26043c7d1d91ba87404215eb413f53b1d7f2f259 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 27 Feb 2024 21:34:04 -0500 Subject: [PATCH 55/56] pllim clean up exception handling --- .../spectral_extraction/spectral_extraction.py | 18 +++++++++--------- .../tests/test_spectral_extraction.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index cc94de83c6..bdd488905d 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -354,16 +354,17 @@ def get_aperture(self): if self.wavelength_dependent: # Cone aperture if display_unit.physical_type != 'length': - error_msg = (f'Spectral axis unit physical type is {display_unit.physical_type},' - ' must be length for cone aperture') - self.hub.broadcast(SnackbarMessage(error_msg, color="error", sender=self)) - raise ValueError(error_msg) + raise ValueError( + f'Spectral axis unit physical type is {display_unit.physical_type}, ' + 'must be length for cone aperture') - mask_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) - # Remove when cone support is extended to other shapes + # TODO: Remove when cone support is extended to other shapes if not hasattr(self.aperture.selected_spatial_region, 'radius'): - raise AttributeError(f"{self.aperture.selected_spatial_region.__str__()} object has" - " no attribute 'radius'") + raise NotImplementedError( + f"{self.aperture.selected_spatial_region.__class__.__name__} is not supported") + + mask_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) + # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit conversion. radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * self.aperture.selected_spatial_region.radius) @@ -376,7 +377,6 @@ def get_aperture(self): mask_weights[:, :, index] = slice_mask else: # Cylindrical aperture - # aperture = CircularAperture(center, r=self.aperture.selected_spatial_region.radius) aperture = regions2aperture(self.aperture.selected_spatial_region) slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) # Turn 2D slice_mask into 3D array that is the same shape as the flux cube diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index a3007f36a3..b942ac52b6 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -307,7 +307,7 @@ def test_cone_and_cylinder_errors(cubeviz_helper, spectrum1d_cube_largest): extract_plg.function = 'Sum' extract_plg.aperture = 'Subset 2' # FIXME: https://jira.stsci.edu/browse/JDAT-4268 - with pytest.raises(AttributeError, match=".* object has no attribute 'radius'"): + with pytest.raises(NotImplementedError, match=".* is not supported"): extract_plg.collapse_to_spectrum() From 7816905b04cd7bca187e3ac31b8dec19f49f0437 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 28 Feb 2024 12:26:24 -0500 Subject: [PATCH 56/56] Implement JDAT-4268 and add tests and also more clean-ups --- docs/cubeviz/plugins.rst | 3 +- .../spectral_extraction.py | 39 ++++++--- .../tests/test_spectral_extraction.py | 83 +++++++++++++------ 3 files changed, 86 insertions(+), 39 deletions(-) diff --git a/docs/cubeviz/plugins.rst b/docs/cubeviz/plugins.rst index 086848959b..8b502c2938 100644 --- a/docs/cubeviz/plugins.rst +++ b/docs/cubeviz/plugins.rst @@ -295,7 +295,8 @@ If using a simple subset (currently only works for a circular subset applied to with spatial axis units in wavelength) for the spatial aperture, an option to make the aperture wavelength dependent will appear. If checked, this will create a cone aperture that increases linearly with wavelength. -The formula for that is:: +The formula for a circular aperture is (for other shapes, radius is +replaced by appropriate shape attributes):: radii = ((all_wavelengths / reference_wavelength) * aperture.selected_spatial_region.radius) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index bdd488905d..a300908fb9 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -9,7 +9,7 @@ ) from traitlets import Any, Bool, Dict, Float, List, Unicode, observe from packaging.version import Version -from photutils.aperture import CircularAperture +from photutils.aperture import CircularAperture, EllipticalAperture, RectangularAperture from specutils import Spectrum1D from jdaviz.core.custom_traitlets import FloatHandleEmpty @@ -348,6 +348,8 @@ def get_aperture(self): # Center is reverse coordinates center = (self.aperture.selected_spatial_region.center.y, self.aperture.selected_spatial_region.center.x) + aperture = regions2aperture(self.aperture.selected_spatial_region) + aperture.positions = center im_shape = (flux_cube.shape[0], flux_cube.shape[1]) aper_method = self.aperture_method_selected.lower() @@ -358,26 +360,39 @@ def get_aperture(self): f'Spectral axis unit physical type is {display_unit.physical_type}, ' 'must be length for cone aperture') - # TODO: Remove when cone support is extended to other shapes - if not hasattr(self.aperture.selected_spatial_region, 'radius'): - raise NotImplementedError( - f"{self.aperture.selected_spatial_region.__class__.__name__} is not supported") - - mask_weights = np.zeros_like(flux_cube.flux.value, dtype=np.float32) + fac = flux_cube.spectral_axis.value / self.reference_wavelength # TODO: Use flux_cube.spectral_axis.to_value(display_unit) when we have unit conversion. - radii = ((flux_cube.spectral_axis.value / self.reference_wavelength) * - self.aperture.selected_spatial_region.radius) + if isinstance(aperture, CircularAperture): + radii = fac * aperture.r # radius + elif isinstance(aperture, EllipticalAperture): + radii = fac * aperture.a # semimajor axis + radii_b = fac * aperture.b # semiminor axis + elif isinstance(aperture, RectangularAperture): + radii = fac * aperture.w # full width + radii_h = fac * aperture.h # full height + else: + raise NotImplementedError(f"{aperture.__class__.__name__} is not supported") + + mask_weights = np.zeros(flux_cube.shape, dtype=np.float32) + # Loop through cube and create cone aperture at each wavelength. Then convert that to a # weight array using the selected aperture method, and add it to a weight cube. - for index, cone_radius in enumerate(radii): - aperture = CircularAperture(center, r=cone_radius) + for index, cone_r in enumerate(radii): + if isinstance(aperture, CircularAperture): + aperture.r = cone_r + elif isinstance(aperture, EllipticalAperture): + aperture.a = cone_r + aperture.b = radii_b[index] + else: # RectangularAperture + aperture.w = cone_r + aperture.h = radii_h[index] + slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) # Add slice mask to fractional pixel array mask_weights[:, :, index] = slice_mask else: # Cylindrical aperture - aperture = regions2aperture(self.aperture.selected_spatial_region) slice_mask = aperture.to_mask(method=aper_method).to_image(im_shape) # Turn 2D slice_mask into 3D array that is the same shape as the flux cube mask_weights = np.stack([slice_mask] * len(flux_cube.spectral_axis), axis=2) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py index b942ac52b6..9536fd4c77 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -7,7 +7,8 @@ from astropy.nddata import NDDataArray, StdDevUncertainty from astropy.utils.exceptions import AstropyUserWarning from numpy.testing import assert_allclose, assert_array_equal -from regions import CirclePixelRegion, EllipsePixelRegion, PixCoord +from regions import (CirclePixelRegion, CircleAnnulusPixelRegion, EllipsePixelRegion, + RectanglePixelRegion, PixCoord) from specutils import Spectrum1D @@ -221,25 +222,28 @@ def test_aperture_markers(cubeviz_helper, spectrum1d_cube): assert slice_plg.slice == 1 +@pytest.mark.parametrize('subset', ['Subset 1', 'Subset 2']) @pytest.mark.parametrize( - ('aperture_method', 'expected_flux_1000', 'expected_flux_2400', 'expected_flux_mean'), - [('Exact', [16.51429064, 16.52000853, 16.52572818, 16.53145005, 16.53717344, 16.54289928, - 16.54862712, 16.55435647, 16.56008781, 16.56582186], + ('aperture_method', 'expected_flux_1000', 'expected_flux_2400'), + [('Exact', + [16.51429064, 16.52000853, 16.52572818, 16.53145005, 16.53717344, 16.54289928, + 16.54862712, 16.55435647, 16.56008781, 16.56582186], [26.812409, 26.821692, 26.830979, 26.840268, 26.849561, 26.858857, - 26.868156, 26.877459, 26.886765, 26.896074], - [0.99999993, 1.00000014, 1.00000011, 0.99999987, 0.99999995, 0.99999995, - 1.00000007, 1.00000005, 0.99999992, 0.99999996]), - ('Center', [21] * 10, [25] * 10, [1] * 10)] + 26.868156, 26.877459, 26.886765, 26.896074]), + ('Center', 21, 25)] ) def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_largest, - aperture_method, expected_flux_1000, - expected_flux_2400, expected_flux_mean): + subset, aperture_method, expected_flux_1000, + expected_flux_2400): cubeviz_helper.load_data(spectrum1d_cube_largest) - cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(5, 10), radius=2.5)]) + center = PixCoord(5, 10) + cubeviz_helper.load_regions([ + CirclePixelRegion(center, radius=2.5), + EllipsePixelRegion(center, width=5, height=5)]) extract_plg = cubeviz_helper.plugins['Spectral Extraction'] - extract_plg.aperture = 'Subset 1' + extract_plg.aperture = subset extract_plg.aperture_method.selected = aperture_method extract_plg.wavelength_dependent = True extract_plg.function = 'Sum' @@ -252,43 +256,71 @@ def test_cone_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_la extract_plg.function = 'Mean' collapsed_spec_mean = extract_plg.collapse_to_spectrum() - assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean, rtol=1e-6) + assert_allclose(collapsed_spec_mean.flux.value, 1) +@pytest.mark.parametrize('subset', ['Subset 1', 'Subset 2']) @pytest.mark.parametrize( - ('aperture_method', 'expected_flux_1000', 'expected_flux_2400', 'expected_flux_mean'), - [('Exact', [19.6349540849] * 10, [19.6349540849] * 10, [1] * 10), - ('Center', [21] * 10, [21] * 10, [1] * 10)] + ('aperture_method', 'expected_flux_wav'), + [('Exact', 19.6349540849), + ('Center', 21)] ) def test_cylindrical_aperture_with_different_methods(cubeviz_helper, spectrum1d_cube_largest, - aperture_method, expected_flux_1000, - expected_flux_2400, expected_flux_mean): + subset, aperture_method, expected_flux_wav): cubeviz_helper.load_data(spectrum1d_cube_largest, data_label="test") - cubeviz_helper.load_regions([CirclePixelRegion(PixCoord(5, 10), radius=2.5)]) + center = PixCoord(5, 10) + cubeviz_helper.load_regions([ + CirclePixelRegion(center, radius=2.5), + EllipsePixelRegion(center, width=5, height=5)]) extract_plg = cubeviz_helper.plugins['Spectral Extraction'] - extract_plg.aperture = 'Subset 1' + extract_plg.aperture = subset extract_plg.aperture_method.selected = aperture_method extract_plg.wavelength_dependent = False extract_plg.function = 'Sum' collapsed_spec = extract_plg.collapse_to_spectrum() - assert_allclose(collapsed_spec.flux.value[1000:1010], expected_flux_1000) - assert_allclose(collapsed_spec.flux.value[2400:2410], expected_flux_2400) + assert_allclose(collapsed_spec.flux.value, expected_flux_wav) extract_plg.function = 'Mean' collapsed_spec_mean = extract_plg.collapse_to_spectrum() - assert_allclose(collapsed_spec_mean.flux.value[1000:1010], expected_flux_mean) + assert_allclose(collapsed_spec_mean.flux.value, 1) + + +# NOTE: Not as thorough as circle and ellipse above but good enough. +def test_rectangle_aperture_with_exact(cubeviz_helper, spectrum1d_cube_largest): + cubeviz_helper.load_data(spectrum1d_cube_largest) + cubeviz_helper.load_regions(RectanglePixelRegion(PixCoord(5, 10), width=4, height=4)) + + extract_plg = cubeviz_helper.plugins['Spectral Extraction'] + + extract_plg.aperture = "Subset 1" + extract_plg.aperture_method.selected = "Exact" + extract_plg.wavelength_dependent = True + extract_plg.function = 'Sum' + collapsed_spec = extract_plg.collapse_to_spectrum() + + # The extracted spectrum has "steps" (aliased) but perhaps that is due to + # how photutils is extracting a boxy aperture. There is still a slope. + expected_flux_step = [9.378906, 10.5625, 11.816406, 13.140625, 14.535156, + 16, 17.535156, 19.691406, 21.972656, 24.378906] + assert_allclose(collapsed_spec.flux.value[::301], expected_flux_step) + + extract_plg.wavelength_dependent = False + collapsed_spec = extract_plg.collapse_to_spectrum() + + assert_allclose(collapsed_spec.flux.value, 16) # 4 x 4 def test_cone_and_cylinder_errors(cubeviz_helper, spectrum1d_cube_largest): cubeviz_helper.load_data(spectrum1d_cube_largest) + center = PixCoord(5, 10) cubeviz_helper.load_regions([ - CirclePixelRegion(PixCoord(5, 10), radius=2.5), - EllipsePixelRegion(center=PixCoord(x=7, y=12), width=5, height=3)]) + CirclePixelRegion(center, radius=2.5), + CircleAnnulusPixelRegion(center, inner_radius=2.5, outer_radius=4)]) extract_plg = cubeviz_helper.plugins['Spectral Extraction'] @@ -306,7 +338,6 @@ def test_cone_and_cylinder_errors(cubeviz_helper, spectrum1d_cube_largest): extract_plg.function = 'Sum' extract_plg.aperture = 'Subset 2' - # FIXME: https://jira.stsci.edu/browse/JDAT-4268 with pytest.raises(NotImplementedError, match=".* is not supported"): extract_plg.collapse_to_spectrum()