Skip to content

Commit cfc2092

Browse files
authored
support specreduce 1.3 (#1889)
* updates for Spectrum1D support in specreduce * TEMP: force spectrum to plot in pixel-space * revert this commit once glue handles non-linear wavelength scaling * update spectral extraction with new model types * update to make use of FitTrace * binning toggle * update units in parser test * order-dependent validation for number of bins * pin specreduce 1.3 * re-introduce uncertainty fallbacks for HorneExtract * fix minor typo in UI * add support for Chebyshev traces * add UI warning for slow FitTrace * separate messages shown if binning is disabled or if nbins > 20
1 parent 232efad commit cfc2092

File tree

6 files changed

+114
-63
lines changed

6 files changed

+114
-63
lines changed

CHANGES.rst

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ Specviz
4848
Specviz2d
4949
^^^^^^^^^
5050

51+
- Update to be compatible with changes in specreduce 1.3, including FitTrace
52+
with Polynomial, Spline, and Legendre options. [#1889]
53+
5154
API Changes
5255
-----------
5356

jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.py

+62-48
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,20 @@
1313
from jdaviz.core.custom_traitlets import IntHandleEmpty, FloatHandleEmpty
1414
from jdaviz.core.marks import PluginLine
1515

16-
from astropy.nddata import NDData, StdDevUncertainty, VarianceUncertainty, UnknownUncertainty
17-
from specutils import Spectrum1D
16+
from astropy.modeling import models
17+
from astropy.nddata import StdDevUncertainty, VarianceUncertainty, UnknownUncertainty
18+
from astropy import units
1819
from specreduce import tracing
1920
from specreduce import background
2021
from specreduce import extract
2122

2223
__all__ = ['SpectralExtraction']
2324

25+
_model_cls = {'Spline': models.Spline1D,
26+
'Polynomial': models.Polynomial1D,
27+
'Legendre': models.Legendre1D,
28+
'Chebyshev': models.Chebyshev1D}
29+
2430

2531
@tray_registry('spectral-extraction', label="Spectral Extraction",
2632
viewer_requirements=['spectrum', 'spectrum-2d'])
@@ -42,12 +48,15 @@ class SpectralExtraction(PluginTemplateMixin):
4248
* ``trace_type`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`):
4349
controls the type of trace to be generated.
4450
* ``trace_peak_method`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`):
45-
only applicable if ``trace_type`` is set to ``Auto``.
51+
only applicable if ``trace_type`` is not ``Flat``.
4652
* :attr:`trace_pixel` :
47-
pixel of the trace. If ``trace_type`` is set to ``Auto``, then this
53+
pixel of the trace. If ``trace_type`` is not ``Flat``, then this
4854
is the "guess" for the automated trace.
55+
* :attr:`trace_do_binning` :
56+
only applicable if ``trace_type`` is not ``Flat``. Bin the input data when fitting the
57+
trace.
4958
* :attr:`trace_bins` :
50-
only applicable if ``trace_type`` is set to ``Auto``.
59+
only applicable if ``trace_type`` is not ``Flat`` and ``trace_do_binning``.
5160
* :attr:`trace_window` :
5261
full width of the trace.
5362
* :meth:`import_trace`
@@ -101,10 +110,12 @@ class SpectralExtraction(PluginTemplateMixin):
101110
trace_type_selected = Unicode().tag(sync=True)
102111

103112
trace_pixel = FloatHandleEmpty(0).tag(sync=True)
113+
trace_order = IntHandleEmpty(3).tag(sync=True)
104114

105115
trace_peak_method_items = List().tag(sync=True)
106116
trace_peak_method_selected = Unicode().tag(sync=True)
107117

118+
trace_do_binning = Bool(True).tag(sync=True)
108119
trace_bins = IntHandleEmpty(20).tag(sync=True)
109120
trace_window = IntHandleEmpty(0).tag(sync=True)
110121

@@ -205,7 +216,9 @@ def __init__(self, *args, **kwargs):
205216
self.trace_type = SelectPluginComponent(self,
206217
items='trace_type_items',
207218
selected='trace_type_selected',
208-
manual_options=['Flat', 'Auto'])
219+
manual_options=['Flat', 'Polynomial',
220+
'Legendre', 'Chebyshev',
221+
'Spline'])
209222

210223
self.trace_peak_method = SelectPluginComponent(self,
211224
items='trace_peak_method_items',
@@ -302,8 +315,10 @@ def __init__(self, *args, **kwargs):
302315
@property
303316
def user_api(self):
304317
return PluginUserApi(self, expose=('interactive_extract',
305-
'trace_dataset', 'trace_type', 'trace_peak_method',
306-
'trace_pixel', 'trace_bins', 'trace_window',
318+
'trace_dataset', 'trace_type',
319+
'trace_order', 'trace_peak_method',
320+
'trace_pixel',
321+
'trace_do_binning', 'trace_bins', 'trace_window',
307322
'import_trace',
308323
'export_trace',
309324
'bg_dataset', 'bg_type',
@@ -407,7 +422,7 @@ def _update_plugin_marks(self, *args):
407422
sp1d = self.export_extract_spectrum(add_data=False)
408423
except Exception as e:
409424
# NOTE: ignore error, but will be raised when clicking ANY of the export buttons
410-
# NOTE: KosmosTrace or manual background are often giving a
425+
# NOTE: FitTrace or manual background are often giving a
411426
# "background regions overlapped" error from specreduce
412427
self.ext_specreduce_err = repr(e)
413428
self.marks['extract'].clear()
@@ -473,9 +488,9 @@ def marks(self):
473488
return self._marks
474489

475490
@observe('trace_dataset_selected', 'trace_type_selected',
476-
'trace_trace_selected', 'trace_offset',
491+
'trace_trace_selected', 'trace_offset', 'trace_order',
477492
'trace_pixel', 'trace_peak_method_selected',
478-
'trace_bins', 'trace_window', 'active_step')
493+
'trace_do_binning', 'trace_bins', 'trace_window', 'active_step')
479494
def _interaction_in_trace_step(self, event={}):
480495
if not self.plugin_opened or not self._do_marks:
481496
return
@@ -613,11 +628,14 @@ def import_trace(self, trace):
613628
if isinstance(trace, tracing.FlatTrace):
614629
self.trace_type_selected = 'Flat'
615630
self.trace_pixel = trace.trace_pos
616-
elif isinstance(trace, tracing.KosmosTrace):
617-
self.trace_type_selected = 'Auto'
631+
elif isinstance(trace, tracing.FitTrace):
632+
self.trace_type_selected = trace.trace_model.__class__.__name__.strip('1D')
618633
self.trace_pixel = trace.guess
619634
self.trace_window = trace.window
620635
self.trace_bins = trace.bins
636+
self.trace_do_binning = True
637+
if hasattr(trace.trace_model, 'degree'):
638+
self.trace_order = trace.trace_model.degree
621639
elif isinstance(trace, tracing.ArrayTrace): # pragma: no cover
622640
raise NotImplementedError(f"cannot import ArrayTrace into plugin. Use viz.load_trace instead") # noqa
623641
else: # pragma: no cover
@@ -644,22 +662,24 @@ def export_trace(self, add_data=False, **kwargs):
644662
# being able to load back into the plugin)
645663
orig_trace = self.trace_trace.selected_obj
646664
if isinstance(orig_trace, tracing.FlatTrace):
647-
trace = tracing.FlatTrace(self.trace_dataset.selected_obj.data,
665+
trace = tracing.FlatTrace(self.trace_dataset.selected_obj,
648666
orig_trace.trace_pos+self.trace_offset)
649667
else:
650-
trace = tracing.ArrayTrace(self.trace_dataset.selected_obj.data,
668+
trace = tracing.ArrayTrace(self.trace_dataset.selected_obj,
651669
self.trace_trace.selected_obj.trace+self.trace_offset)
652670

653671
elif self.trace_type_selected == 'Flat':
654-
trace = tracing.FlatTrace(self.trace_dataset.selected_obj.data,
672+
trace = tracing.FlatTrace(self.trace_dataset.selected_obj,
655673
self.trace_pixel)
656674

657-
elif self.trace_type_selected == 'Auto':
658-
trace = tracing.KosmosTrace(self.trace_dataset.selected_obj.data,
659-
guess=self.trace_pixel,
660-
bins=int(self.trace_bins),
661-
window=self.trace_window,
662-
peak_method=self.trace_peak_method_selected.lower())
675+
elif self.trace_type_selected in _model_cls:
676+
trace_model = _model_cls[self.trace_type_selected](degree=self.trace_order)
677+
trace = tracing.FitTrace(self.trace_dataset.selected_obj,
678+
guess=self.trace_pixel,
679+
bins=int(self.trace_bins) if self.trace_do_binning else None,
680+
window=self.trace_window,
681+
peak_method=self.trace_peak_method_selected.lower(),
682+
trace_model=trace_model)
663683

664684
else:
665685
raise NotImplementedError(f"trace_type={self.trace_type_selected} not implemented")
@@ -674,7 +694,7 @@ def vue_create_trace(self, *args):
674694

675695
def _get_bg_trace(self):
676696
if self.bg_type_selected == 'Manual':
677-
trace = tracing.FlatTrace(self.trace_dataset.selected_obj.data,
697+
trace = tracing.FlatTrace(self.trace_dataset.selected_obj,
678698
self.bg_trace_pixel)
679699
elif self.bg_trace_selected == 'From Plugin':
680700
trace = self.export_trace(add_data=False)
@@ -736,15 +756,15 @@ def export_bg(self, **kwargs):
736756
trace = self._get_bg_trace()
737757

738758
if self.bg_type_selected == 'Manual':
739-
bg = background.Background(self.bg_dataset.selected_obj.data,
759+
bg = background.Background(self.bg_dataset.selected_obj,
740760
[trace], width=self.bg_width)
741761
elif self.bg_type_selected == 'OneSided':
742-
bg = background.Background.one_sided(self.bg_dataset.selected_obj.data,
762+
bg = background.Background.one_sided(self.bg_dataset.selected_obj,
743763
trace,
744764
self.bg_separation,
745765
width=self.bg_width)
746766
elif self.bg_type_selected == 'TwoSided':
747-
bg = background.Background.two_sided(self.bg_dataset.selected_obj.data,
767+
bg = background.Background.two_sided(self.bg_dataset.selected_obj,
748768
trace,
749769
self.bg_separation,
750770
width=self.bg_width)
@@ -763,10 +783,7 @@ def export_bg_img(self, add_data=False, **kwargs):
763783
Whether to add the resulting image to the application, according to the options
764784
defined in the plugin.
765785
"""
766-
bg = self.export_bg(**kwargs)
767-
768-
bg_spec = Spectrum1D(spectral_axis=self.bg_dataset.selected_obj.spectral_axis,
769-
flux=bg.bkg_image()*self.bg_dataset.selected_obj.flux.unit)
786+
bg_spec = self.export_bg(**kwargs).bkg_image()
770787

771788
if add_data:
772789
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):
792809
Whether to add the resulting spectrum to the application, according to the options
793810
defined in the plugin.
794811
"""
795-
bg = self.export_bg(**kwargs)
796-
spec = bg.bkg_spectrum()
812+
spec = self.export_bg(**kwargs).bkg_spectrum()
797813

798814
if add_data:
799815
self.bg_spec_add_results.add_results_from_plugin(spec, replace=False)
800816

817+
# TEMPORARY: override spectral axis to be in pixels until properly supporting plotting
818+
# in wavelength/frequency
819+
spec._spectral_axis = np.arange(len(spec.spectral_axis)) * units.pix
820+
801821
return spec
802822

803823
def vue_create_bg_spec(self, *args):
@@ -813,10 +833,7 @@ def export_bg_sub(self, add_data=False, **kwargs):
813833
Whether to add the resulting image to the application, according to the options
814834
defined in the plugin.
815835
"""
816-
bg = self.export_bg(**kwargs)
817-
818-
bg_sub_spec = Spectrum1D(spectral_axis=self.bg_dataset.selected_obj.spectral_axis,
819-
flux=bg.sub_image()*self.bg_dataset.selected_obj.flux.unit)
836+
bg_sub_spec = self.export_bg(**kwargs).sub_image()
820837

821838
if add_data:
822839
self.bg_sub_add_results.add_results_from_plugin(bg_sub_spec, replace=True)
@@ -867,13 +884,13 @@ def export_extract(self, **kwargs):
867884
inp_sp2d = self._get_ext_input_spectrum()
868885

869886
if self.ext_type_selected == 'Boxcar':
870-
ext = extract.BoxcarExtract(inp_sp2d.data, trace, width=self.ext_width)
887+
ext = extract.BoxcarExtract(inp_sp2d, trace, width=self.ext_width)
871888
elif self.ext_type_selected == 'Horne':
872-
uncert = inp_sp2d.uncertainty if inp_sp2d.uncertainty is not None else VarianceUncertainty(np.ones_like(inp_sp2d.data)) # noqa
873-
if not hasattr(uncert, 'uncertainty_type'):
874-
uncert = StdDevUncertainty(uncert)
875-
image = NDData(inp_sp2d.data, uncertainty=uncert)
876-
ext = extract.HorneExtract(image, trace)
889+
if inp_sp2d.uncertainty is None:
890+
inp_sp2d.uncertainty = VarianceUncertainty(np.ones_like(inp_sp2d.data))
891+
if not hasattr(inp_sp2d.uncertainty, 'uncertainty_type'):
892+
inp_sp2d.uncertainty = StdDevUncertainty(inp_sp2d.uncert)
893+
ext = extract.HorneExtract(inp_sp2d, trace)
877894
else:
878895
raise NotImplementedError(f"extraction type '{self.ext_type_selected}' not supported") # noqa
879896

@@ -892,12 +909,9 @@ def export_extract_spectrum(self, add_data=False, **kwargs):
892909
extract = self.export_extract(**kwargs)
893910
spectrum = extract.spectrum
894911

895-
# Specreduce returns a spectral axis in pixels, so we'll replace with input spectral_axis
896-
# NOTE: this is currently disabled until proper handling of axes-limit linking between
897-
# the 2D spectrum image (plotted in pixels) and a 1D spectrum (plotted in freq or
898-
# wavelength) is implemented.
899-
900-
# spectrum = Spectrum1D(spectral_axis=inp_sp2d.spectral_axis, flux=spectrum.flux)
912+
# TEMPORARY: override spectral axis to be in pixels until properly supporting plotting
913+
# in wavelength/frequency
914+
spectrum._spectral_axis = np.arange(len(spectrum.spectral_axis)) * units.pix
901915

902916
if add_data:
903917
self.ext_add_results.add_results_from_plugin(spectrum, replace=False)

jdaviz/configs/specviz2d/plugins/spectral_extraction/spectral_extraction.vue

+41-7
Original file line numberDiff line numberDiff line change
@@ -75,43 +75,77 @@
7575
></v-select>
7676
</v-row>
7777

78+
<v-row v-if="trace_type_selected!=='Flat'">
79+
<v-text-field
80+
label="Order"
81+
type="number"
82+
v-model.number="trace_order"
83+
:rules="[() => trace_order!=='' || 'This field is required',
84+
() => trace_order>=0 || 'Order must be positive',
85+
() => (trace_type_selected!=='Spline' || (trace_order > 0 && trace_order <= 5)) || 'Spline order must be between 1 and 5']"
86+
hint="Order of the trace model."
87+
persistent-hint
88+
>
89+
</v-text-field>
90+
</v-row>
91+
7892
<v-row>
7993
<v-text-field
8094
label="Pixel"
8195
type="number"
8296
v-model.number="trace_pixel"
8397
:rules="[() => trace_pixel!=='' || 'This field is required']"
84-
:hint="trace_type_selected === 'Flat' ? 'Pixel row for flat trace.' : 'Pixel row initial guess for auto trace.'"
98+
:hint="trace_type_selected === 'Flat' ? 'Pixel row for flat trace.' : 'Pixel row initial guess for fitting the trace.'"
8599
persistent-hint
86100
>
87101
</v-text-field>
88102
</v-row>
89103

90-
<v-row v-if="trace_type_selected==='Auto'">
104+
<v-row v-if="trace_type_selected!=='Flat'">
105+
<v-switch
106+
v-model="trace_do_binning"
107+
label="Bin input spectrum"
108+
></v-switch>
91109
<v-text-field
110+
v-if="trace_do_binning"
92111
label="Bins"
93112
type="number"
94113
v-model.number="trace_bins"
95-
:rules="[() => trace_bins!=='' || 'This field is required']"
114+
:rules="[() => trace_bins!=='' || 'This field is required',
115+
() => trace_bins>=Math.max(4, trace_order+1) || 'Bins must be >= '+Math.max(4, trace_order+1)]"
96116
hint="Number of bins in the dispersion direction."
97117
persistent-hint
98118
>
99119
</v-text-field>
100120
</v-row>
101121

102-
<v-row v-if="trace_type_selected==='Auto'">
122+
<v-row v-if="trace_type_selected!=='Flat' && !trace_do_binning">
123+
<span class="v-messages v-messages__message text--secondary">
124+
<b style="color: red !important">WARNING:</b> Trace fitting may be slow without binning.
125+
</span>
126+
</v-row>
127+
128+
<v-row v-if="trace_type_selected!=='Flat' && trace_do_binning && trace_bins > 20">
129+
<span class="v-messages v-messages__message text--secondary">
130+
<b style="color: red !important">WARNING:</b> Trace fitting may be slow with a large number of bins.
131+
</span>
132+
</v-row>
133+
134+
135+
<v-row v-if="trace_type_selected!=='Flat'">
103136
<v-text-field
104137
label="Window Width"
105138
type="number"
106139
v-model.number="trace_window"
107-
:rules="[() => trace_window!=='' || 'This field is required']"
140+
:rules="[() => trace_window!=='' || 'This field is required',
141+
() => trace_window > 0 || 'Window must be positive']"
108142
hint="Width in rows to consider for peak finding."
109143
persistent-hint
110144
>
111145
</v-text-field>
112146
</v-row>
113147

114-
<v-row v-if="trace_type_selected==='Auto'">
148+
<v-row v-if="trace_type_selected!=='Flat'">
115149
<v-select
116150
attach
117151
:menu-props="{ left: true }"
@@ -313,7 +347,7 @@
313347
:selected.sync="ext_dataset_selected"
314348
:show_if_single_entry="false"
315349
label="2D Spectrum"
316-
hint="Select the data used to extract the spectrum. 'From Plugin' uses background-subtraced image defined in Background section above."
350+
hint="Select the data used to extract the spectrum. 'From Plugin' uses background-subtracted image defined in Background section above."
317351
/>
318352

319353
<plugin-dataset-select

jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ def test_plugin(specviz2d_helper):
4545
trace = pext.export_trace(add_data=True) # overwrite
4646
assert isinstance(trace, tracing.FlatTrace)
4747

48-
# create KosmosTrace
48+
# create FitTrace
4949
pext.trace_trace_selected = 'New Trace'
50-
pext.trace_type_selected = 'Auto'
50+
pext.trace_type_selected = 'Polynomial'
5151
trace = pext.export_trace(add_data=True)
52-
assert isinstance(trace, tracing.KosmosTrace)
52+
assert isinstance(trace, tracing.FitTrace)
5353
assert trace.guess == 27
5454
trace = pext.export_trace(trace_pixel=26, add_data=False)
5555
assert trace.guess == 26
@@ -135,7 +135,7 @@ def test_plugin(specviz2d_helper):
135135
pext.update_marks(step)
136136

137137
# test exception handling
138-
pext.trace_type = 'Auto'
138+
pext.trace_type = 'Polynomial'
139139
pext.bg_type_selected = 'TwoSided'
140140
pext.bg_separation = 1
141141
pext.bg_width = 5

0 commit comments

Comments
 (0)