diff --git a/CHANGES.rst b/CHANGES.rst
index c24c865435..578ae64918 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -45,6 +45,9 @@ Specviz
Specviz2d
^^^^^^^^^
+- Update to be compatible with changes in specreduce 1.3, including FitTrace
+ with Polynomial, Spline, and Legendre options. [#1889]
+
API Changes
-----------
diff --git a/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.py
index 53d16a3a82..30ac4df0bc 100644
--- a/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.py
+++ b/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.py
@@ -13,14 +13,20 @@
from jdaviz.core.custom_traitlets import IntHandleEmpty, FloatHandleEmpty
from jdaviz.core.marks import PluginLine
-from astropy.nddata import NDData, StdDevUncertainty, VarianceUncertainty, UnknownUncertainty
-from specutils import Spectrum1D
+from astropy.modeling import models
+from astropy.nddata import StdDevUncertainty, VarianceUncertainty, UnknownUncertainty
+from astropy import units
from specreduce import tracing
from specreduce import background
from specreduce import extract
__all__ = ['SpectralExtraction']
+_model_cls = {'Spline': models.Spline1D,
+ 'Polynomial': models.Polynomial1D,
+ 'Legendre': models.Legendre1D,
+ 'Chebyshev': models.Chebyshev1D}
+
@tray_registry('spectral-extraction', label="Spectral Extraction",
viewer_requirements=['spectrum', 'spectrum-2d'])
@@ -42,12 +48,15 @@ class SpectralExtraction(PluginTemplateMixin):
* ``trace_type`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`):
controls the type of trace to be generated.
* ``trace_peak_method`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`):
- only applicable if ``trace_type`` is set to ``Auto``.
+ only applicable if ``trace_type`` is not ``Flat``.
* :attr:`trace_pixel` :
- pixel of the trace. If ``trace_type`` is set to ``Auto``, then this
+ pixel of the trace. If ``trace_type`` is not ``Flat``, then this
is the "guess" for the automated trace.
+ * :attr:`trace_do_binning` :
+ only applicable if ``trace_type`` is not ``Flat``. Bin the input data when fitting the
+ trace.
* :attr:`trace_bins` :
- only applicable if ``trace_type`` is set to ``Auto``.
+ only applicable if ``trace_type`` is not ``Flat`` and ``trace_do_binning``.
* :attr:`trace_window` :
full width of the trace.
* :meth:`import_trace`
@@ -101,10 +110,12 @@ class SpectralExtraction(PluginTemplateMixin):
trace_type_selected = Unicode().tag(sync=True)
trace_pixel = FloatHandleEmpty(0).tag(sync=True)
+ trace_order = IntHandleEmpty(3).tag(sync=True)
trace_peak_method_items = List().tag(sync=True)
trace_peak_method_selected = Unicode().tag(sync=True)
+ trace_do_binning = Bool(True).tag(sync=True)
trace_bins = IntHandleEmpty(20).tag(sync=True)
trace_window = IntHandleEmpty(0).tag(sync=True)
@@ -205,7 +216,9 @@ def __init__(self, *args, **kwargs):
self.trace_type = SelectPluginComponent(self,
items='trace_type_items',
selected='trace_type_selected',
- manual_options=['Flat', 'Auto'])
+ manual_options=['Flat', 'Polynomial',
+ 'Legendre', 'Chebyshev',
+ 'Spline'])
self.trace_peak_method = SelectPluginComponent(self,
items='trace_peak_method_items',
@@ -302,8 +315,10 @@ def __init__(self, *args, **kwargs):
@property
def user_api(self):
return PluginUserApi(self, expose=('interactive_extract',
- 'trace_dataset', 'trace_type', 'trace_peak_method',
- 'trace_pixel', 'trace_bins', 'trace_window',
+ 'trace_dataset', 'trace_type',
+ 'trace_order', 'trace_peak_method',
+ 'trace_pixel',
+ 'trace_do_binning', 'trace_bins', 'trace_window',
'import_trace',
'export_trace',
'bg_dataset', 'bg_type',
@@ -407,7 +422,7 @@ def _update_plugin_marks(self, *args):
sp1d = self.export_extract_spectrum(add_data=False)
except Exception as e:
# NOTE: ignore error, but will be raised when clicking ANY of the export buttons
- # NOTE: KosmosTrace or manual background are often giving a
+ # NOTE: FitTrace or manual background are often giving a
# "background regions overlapped" error from specreduce
self.ext_specreduce_err = repr(e)
self.marks['extract'].clear()
@@ -473,9 +488,9 @@ def marks(self):
return self._marks
@observe('trace_dataset_selected', 'trace_type_selected',
- 'trace_trace_selected', 'trace_offset',
+ 'trace_trace_selected', 'trace_offset', 'trace_order',
'trace_pixel', 'trace_peak_method_selected',
- 'trace_bins', 'trace_window', 'active_step')
+ 'trace_do_binning', 'trace_bins', 'trace_window', 'active_step')
def _interaction_in_trace_step(self, event={}):
if not self.plugin_opened or not self._do_marks:
return
@@ -613,11 +628,14 @@ def import_trace(self, trace):
if isinstance(trace, tracing.FlatTrace):
self.trace_type_selected = 'Flat'
self.trace_pixel = trace.trace_pos
- elif isinstance(trace, tracing.KosmosTrace):
- self.trace_type_selected = 'Auto'
+ elif isinstance(trace, tracing.FitTrace):
+ self.trace_type_selected = trace.trace_model.__class__.__name__.strip('1D')
self.trace_pixel = trace.guess
self.trace_window = trace.window
self.trace_bins = trace.bins
+ self.trace_do_binning = True
+ if hasattr(trace.trace_model, 'degree'):
+ self.trace_order = trace.trace_model.degree
elif isinstance(trace, tracing.ArrayTrace): # pragma: no cover
raise NotImplementedError(f"cannot import ArrayTrace into plugin. Use viz.load_trace instead") # noqa
else: # pragma: no cover
@@ -644,22 +662,24 @@ def export_trace(self, add_data=False, **kwargs):
# being able to load back into the plugin)
orig_trace = self.trace_trace.selected_obj
if isinstance(orig_trace, tracing.FlatTrace):
- trace = tracing.FlatTrace(self.trace_dataset.selected_obj.data,
+ trace = tracing.FlatTrace(self.trace_dataset.selected_obj,
orig_trace.trace_pos+self.trace_offset)
else:
- trace = tracing.ArrayTrace(self.trace_dataset.selected_obj.data,
+ trace = tracing.ArrayTrace(self.trace_dataset.selected_obj,
self.trace_trace.selected_obj.trace+self.trace_offset)
elif self.trace_type_selected == 'Flat':
- trace = tracing.FlatTrace(self.trace_dataset.selected_obj.data,
+ trace = tracing.FlatTrace(self.trace_dataset.selected_obj,
self.trace_pixel)
- elif self.trace_type_selected == 'Auto':
- trace = tracing.KosmosTrace(self.trace_dataset.selected_obj.data,
- guess=self.trace_pixel,
- bins=int(self.trace_bins),
- window=self.trace_window,
- peak_method=self.trace_peak_method_selected.lower())
+ elif self.trace_type_selected in _model_cls:
+ trace_model = _model_cls[self.trace_type_selected](degree=self.trace_order)
+ trace = tracing.FitTrace(self.trace_dataset.selected_obj,
+ guess=self.trace_pixel,
+ bins=int(self.trace_bins) if self.trace_do_binning else None,
+ window=self.trace_window,
+ peak_method=self.trace_peak_method_selected.lower(),
+ trace_model=trace_model)
else:
raise NotImplementedError(f"trace_type={self.trace_type_selected} not implemented")
@@ -674,7 +694,7 @@ def vue_create_trace(self, *args):
def _get_bg_trace(self):
if self.bg_type_selected == 'Manual':
- trace = tracing.FlatTrace(self.trace_dataset.selected_obj.data,
+ trace = tracing.FlatTrace(self.trace_dataset.selected_obj,
self.bg_trace_pixel)
elif self.bg_trace_selected == 'From Plugin':
trace = self.export_trace(add_data=False)
@@ -736,15 +756,15 @@ def export_bg(self, **kwargs):
trace = self._get_bg_trace()
if self.bg_type_selected == 'Manual':
- bg = background.Background(self.bg_dataset.selected_obj.data,
+ bg = background.Background(self.bg_dataset.selected_obj,
[trace], width=self.bg_width)
elif self.bg_type_selected == 'OneSided':
- bg = background.Background.one_sided(self.bg_dataset.selected_obj.data,
+ bg = background.Background.one_sided(self.bg_dataset.selected_obj,
trace,
self.bg_separation,
width=self.bg_width)
elif self.bg_type_selected == 'TwoSided':
- bg = background.Background.two_sided(self.bg_dataset.selected_obj.data,
+ bg = background.Background.two_sided(self.bg_dataset.selected_obj,
trace,
self.bg_separation,
width=self.bg_width)
@@ -763,10 +783,7 @@ def export_bg_img(self, add_data=False, **kwargs):
Whether to add the resulting image to the application, according to the options
defined in the plugin.
"""
- bg = self.export_bg(**kwargs)
-
- bg_spec = Spectrum1D(spectral_axis=self.bg_dataset.selected_obj.spectral_axis,
- flux=bg.bkg_image()*self.bg_dataset.selected_obj.flux.unit)
+ bg_spec = self.export_bg(**kwargs).bkg_image()
if add_data:
self.bg_add_results.add_results_from_plugin(bg_spec, replace=True)
@@ -792,12 +809,15 @@ def export_bg_spectrum(self, add_data=False, **kwargs):
Whether to add the resulting spectrum to the application, according to the options
defined in the plugin.
"""
- bg = self.export_bg(**kwargs)
- spec = bg.bkg_spectrum()
+ spec = self.export_bg(**kwargs).bkg_spectrum()
if add_data:
self.bg_spec_add_results.add_results_from_plugin(spec, replace=False)
+ # TEMPORARY: override spectral axis to be in pixels until properly supporting plotting
+ # in wavelength/frequency
+ spec._spectral_axis = np.arange(len(spec.spectral_axis)) * units.pix
+
return spec
def vue_create_bg_spec(self, *args):
@@ -813,10 +833,7 @@ def export_bg_sub(self, add_data=False, **kwargs):
Whether to add the resulting image to the application, according to the options
defined in the plugin.
"""
- bg = self.export_bg(**kwargs)
-
- bg_sub_spec = Spectrum1D(spectral_axis=self.bg_dataset.selected_obj.spectral_axis,
- flux=bg.sub_image()*self.bg_dataset.selected_obj.flux.unit)
+ bg_sub_spec = self.export_bg(**kwargs).sub_image()
if add_data:
self.bg_sub_add_results.add_results_from_plugin(bg_sub_spec, replace=True)
@@ -867,13 +884,13 @@ def export_extract(self, **kwargs):
inp_sp2d = self._get_ext_input_spectrum()
if self.ext_type_selected == 'Boxcar':
- ext = extract.BoxcarExtract(inp_sp2d.data, trace, width=self.ext_width)
+ ext = extract.BoxcarExtract(inp_sp2d, trace, width=self.ext_width)
elif self.ext_type_selected == 'Horne':
- uncert = inp_sp2d.uncertainty if inp_sp2d.uncertainty is not None else VarianceUncertainty(np.ones_like(inp_sp2d.data)) # noqa
- if not hasattr(uncert, 'uncertainty_type'):
- uncert = StdDevUncertainty(uncert)
- image = NDData(inp_sp2d.data, uncertainty=uncert)
- ext = extract.HorneExtract(image, trace)
+ if inp_sp2d.uncertainty is None:
+ inp_sp2d.uncertainty = VarianceUncertainty(np.ones_like(inp_sp2d.data))
+ if not hasattr(inp_sp2d.uncertainty, 'uncertainty_type'):
+ inp_sp2d.uncertainty = StdDevUncertainty(inp_sp2d.uncert)
+ ext = extract.HorneExtract(inp_sp2d, trace)
else:
raise NotImplementedError(f"extraction type '{self.ext_type_selected}' not supported") # noqa
@@ -892,12 +909,9 @@ def export_extract_spectrum(self, add_data=False, **kwargs):
extract = self.export_extract(**kwargs)
spectrum = extract.spectrum
- # Specreduce returns a spectral axis in pixels, so we'll replace with input spectral_axis
- # NOTE: this is currently disabled until proper handling of axes-limit linking between
- # the 2D spectrum image (plotted in pixels) and a 1D spectrum (plotted in freq or
- # wavelength) is implemented.
-
- # spectrum = Spectrum1D(spectral_axis=inp_sp2d.spectral_axis, flux=spectrum.flux)
+ # TEMPORARY: override spectral axis to be in pixels until properly supporting plotting
+ # in wavelength/frequency
+ spectrum._spectral_axis = np.arange(len(spectrum.spectral_axis)) * units.pix
if add_data:
self.ext_add_results.add_results_from_plugin(spectrum, replace=False)
diff --git a/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.vue b/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.vue
index 0305c46723..0a27ef7654 100644
--- a/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.vue
+++ b/jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.vue
@@ -75,43 +75,77 @@
>
+
+
+
+
+
-
+
+
-
+
+
+ WARNING: Trace fitting may be slow without binning.
+
+
+
+
+
+ WARNING: Trace fitting may be slow with a large number of bins.
+
+
+
+
+
-
+
=0.3.5,<0.4
pyyaml>=5.4.1
specutils>=1.9
- specreduce>=1.2.0,<1.3.0
+ specreduce>=1.3.0,<1.4.0
photutils>=1.4
glue-astronomy>=0.5.1
asteval>=0.9.23