Skip to content

Commit c015d73

Browse files
committed
Added image parsers to all classes that take images
Quantity arrays are now acceptable inputs. After parsing, the attribute is now of type instead of as before.
1 parent 945fb1c commit c015d73

File tree

3 files changed

+207
-84
lines changed

3 files changed

+207
-84
lines changed

specreduce/background.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
from dataclasses import dataclass, field
55

66
import numpy as np
7-
from astropy.nddata import NDData
7+
from astropy.nddata import NDData, VarianceUncertainty
88
from astropy import units as u
9+
from specutils import Spectrum1D
910

1011
from specreduce.extract import _ap_weight_image, _to_spectrum1d_pixels
1112
from specreduce.tracing import Trace, FlatTrace
@@ -54,6 +55,41 @@ class Background:
5455
disp_axis: int = 1
5556
crossdisp_axis: int = 0
5657

58+
def _parse_image(self):
59+
"""
60+
Convert all accepted image types to a consistently formatted Spectrum1D.
61+
"""
62+
63+
if isinstance(self.image, np.ndarray):
64+
img = self.image
65+
elif isinstance(self.image, u.quantity.Quantity):
66+
img = self.image.value
67+
else: # NDData, including CCDData and Spectrum1D
68+
img = self.image.data
69+
70+
# mask and uncertainty are set as None when they aren't specified upon
71+
# creating a Spectrum1D object, so we must check whether these
72+
# attributes are absent *and* whether they are present but set as None
73+
if getattr(self.image, 'mask', None) is not None:
74+
mask = self.image.mask
75+
else:
76+
mask = np.ma.masked_invalid(img).mask
77+
78+
if getattr(self.image, 'uncertainty', None) is not None:
79+
uncertainty = self.image.uncertainty
80+
else:
81+
uncertainty = VarianceUncertainty(np.ones(img.shape))
82+
83+
unit = getattr(self.image, 'unit', u.Unit('DN')) # or u.Unit()?
84+
85+
spectral_axis = getattr(self.image, 'spectral_axis',
86+
(np.arange(img.shape[self.disp_axis])
87+
if hasattr(self, 'disp_axis')
88+
else np.arange(img.shape[1])) * u.pix)
89+
90+
self.image = Spectrum1D(img * unit, spectral_axis=spectral_axis,
91+
uncertainty=uncertainty, mask=mask)
92+
5793
def __post_init__(self):
5894
"""
5995
Determine the background from an image for subtraction.
@@ -86,12 +122,9 @@ def _to_trace(trace):
86122
raise ValueError('trace_object.trace_pos must be >= 1')
87123
return trace
88124

89-
if isinstance(self.image, NDData):
90-
# NOTE: should the NDData structure instead be preserved?
91-
# (NDData includes Spectrum1D under its umbrella)
92-
self.image = self.image.data
125+
self._parse_image()
93126

94-
bkg_wimage = np.zeros_like(self.image, dtype=np.float64)
127+
bkg_wimage = np.zeros_like(self.image.data, dtype=np.float64)
95128
for trace in self.traces:
96129
trace = _to_trace(trace)
97130
if (np.any(trace.trace.data >= self.image.shape[self.crossdisp_axis]) or
@@ -116,9 +149,10 @@ def _to_trace(trace):
116149

117150
self.bkg_wimage = bkg_wimage
118151
if self.statistic == 'average':
119-
self.bkg_array = np.average(self.image, weights=self.bkg_wimage, axis=0)
152+
self.bkg_array = np.average(self.image.data,
153+
weights=self.bkg_wimage, axis=0)
120154
elif self.statistic == 'median':
121-
med_image = self.image.copy()
155+
med_image = self.image.data.copy()
122156
med_image[np.where(self.bkg_wimage) == 0] = np.nan
123157
self.bkg_array = np.nanmedian(med_image, axis=0)
124158
else:

specreduce/extract.py

Lines changed: 125 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from astropy import units as u
99
from astropy.modeling import Model, models, fitting
10-
from astropy.nddata import NDData
10+
from astropy.nddata import NDData, VarianceUncertainty
1111

1212
from specreduce.core import SpecreduceOperation
1313
from specreduce.tracing import Trace, FlatTrace
@@ -149,6 +149,41 @@ class BoxcarExtract(SpecreduceOperation):
149149
def spectrum(self):
150150
return self.__call__()
151151

152+
def _parse_image(self):
153+
"""
154+
Convert all accepted image types to a consistently formatted Spectrum1D.
155+
"""
156+
157+
if isinstance(self.image, np.ndarray):
158+
img = self.image
159+
elif isinstance(self.image, u.quantity.Quantity):
160+
img = self.image.value
161+
else: # NDData, including CCDData and Spectrum1D
162+
img = self.image.data
163+
164+
# mask and uncertainty are set as None when they aren't specified upon
165+
# creating a Spectrum1D object, so we must check whether these
166+
# attributes are absent *and* whether they are present but set as None
167+
if getattr(self.image, 'mask', None) is not None:
168+
mask = self.image.mask
169+
else:
170+
mask = np.ma.masked_invalid(img).mask
171+
172+
if getattr(self.image, 'uncertainty', None) is not None:
173+
uncertainty = self.image.uncertainty
174+
else:
175+
uncertainty = VarianceUncertainty(np.ones(img.shape))
176+
177+
unit = getattr(self.image, 'unit', u.Unit('DN')) # or u.Unit()?
178+
179+
spectral_axis = getattr(self.image, 'spectral_axis',
180+
(np.arange(img.shape[self.disp_axis])
181+
if hasattr(self, 'disp_axis')
182+
else np.arange(img.shape[1])) * u.pix)
183+
184+
self.image = Spectrum1D(img * unit, spectral_axis=spectral_axis,
185+
uncertainty=uncertainty, mask=mask)
186+
152187
def __call__(self, image=None, trace_object=None, width=None,
153188
disp_axis=None, crossdisp_axis=None):
154189
"""
@@ -267,6 +302,86 @@ class HorneExtract(SpecreduceOperation):
267302
def spectrum(self):
268303
return self.__call__()
269304

305+
def _parse_image(self, variance=None, mask=None, unit=None):
306+
"""
307+
Convert all accepted image types to a consistently formatted Spectrum1D.
308+
Takes some extra arguments exactly as they come from self.__call__() to
309+
handle cases where users specify them as arguments instead of as
310+
attributes of their image object.
311+
"""
312+
313+
if isinstance(self.image, np.ndarray):
314+
img = self.image
315+
elif isinstance(self.image, u.quantity.Quantity):
316+
img = self.image.value
317+
else: # NDData, including CCDData and Spectrum1D
318+
img = self.image.data
319+
320+
# mask is set as None when not specified upon creating a Spectrum1D
321+
# object, so we must check whether it is absent *and* whether it's
322+
# present but set as None
323+
if getattr(self.image, 'mask', None) is not None:
324+
mask = self.image.mask
325+
else:
326+
mask = np.ma.masked_invalid(img).mask
327+
328+
# Process uncertainties, converting to variances when able and throwing
329+
# an error when uncertainties are missing or less easily converted
330+
if (hasattr(self.image, 'uncertainty')
331+
and self.image.uncertainty is not None):
332+
if self.image.uncertainty.uncertainty_type == 'var':
333+
variance = self.image.uncertainty.array
334+
elif self.image.uncertainty.uncertainty_type == 'std':
335+
warnings.warn("image NDData object's uncertainty "
336+
"interpreted as standard deviation. if "
337+
"incorrect, use VarianceUncertainty when "
338+
"assigning image object's uncertainty.")
339+
variance = self.image.uncertainty.array**2
340+
elif self.image.uncertainty.uncertainty_type == 'ivar':
341+
variance = 1 / self.image.uncertainty.array
342+
else:
343+
# other options are InverseVariance and UnknownVariance
344+
raise ValueError("image NDData object has unexpected "
345+
"uncertainty type. instead, try "
346+
"VarianceUncertainty or StdDevUncertainty.")
347+
elif (hasattr(self.image, 'uncertainty')
348+
and self.image.uncertainty is None):
349+
# ignore variance arg to focus on updating NDData object
350+
raise ValueError('image NDData object lacks uncertainty')
351+
else:
352+
if variance is None:
353+
raise ValueError("if image is a numpy or Quantity array, a "
354+
"variance must be specified. consider "
355+
"wrapping it into one object by instead "
356+
"passing an NDData image.")
357+
elif self.image.shape != variance.shape:
358+
raise ValueError("image and variance shapes must match")
359+
360+
if np.any(variance < 0):
361+
raise ValueError("variance must be fully positive")
362+
if np.all(variance == 0):
363+
# technically would result in infinities, but since they're all
364+
# zeros, we can override ones to simulate an unweighted case
365+
variance = np.ones_like(variance)
366+
if np.any(variance == 0):
367+
# exclude such elements by editing the input mask
368+
mask[variance == 0] = True
369+
# replace the variances to avoid a divide by zero warning
370+
variance[variance == 0] = np.nan
371+
372+
variance = VarianceUncertainty(variance)
373+
374+
unit = getattr(self.image, 'unit',
375+
u.Unit(self.unit) if self.unit is not None else u.Unit())
376+
377+
spectral_axis = getattr(self.image, 'spectral_axis',
378+
(np.arange(img.shape[self.disp_axis])
379+
if hasattr(self, 'disp_axis')
380+
else np.arange(img.shape[1])) * u.pix)
381+
382+
self.image = Spectrum1D(img * unit, spectral_axis=spectral_axis,
383+
uncertainty=variance, mask=mask)
384+
270385
def __call__(self, image=None, trace_object=None,
271386
disp_axis=None, crossdisp_axis=None,
272387
bkgrd_prof=None,
@@ -329,71 +444,16 @@ def __call__(self, image=None, trace_object=None,
329444
mask = mask if mask is not None else self.mask
330445
unit = unit if unit is not None else self.unit
331446

332-
# handle image and associated data based on image's type
333-
if isinstance(image, NDData):
334-
# (NDData includes Spectrum1D under its umbrella)
335-
img = np.ma.array(image.data, mask=image.mask)
336-
unit = image.unit if image.unit is not None else u.Unit()
337-
338-
if image.uncertainty is not None:
339-
# prioritize NDData's uncertainty over variance argument
340-
if image.uncertainty.uncertainty_type == 'var':
341-
variance = image.uncertainty.array
342-
elif image.uncertainty.uncertainty_type == 'std':
343-
# NOTE: CCDData defaults uncertainties given as pure arrays
344-
# to std and logs a warning saying so upon object creation.
345-
# should we remind users again here?
346-
warnings.warn("image NDData object's uncertainty "
347-
"interpreted as standard deviation. if "
348-
"incorrect, use VarianceUncertainty when "
349-
"assigning image object's uncertainty.")
350-
variance = image.uncertainty.array**2
351-
elif image.uncertainty.uncertainty_type == 'ivar':
352-
variance = 1 / image.uncertainty.array
353-
else:
354-
# other options are InverseVariance and UnknownVariance
355-
raise ValueError("image NDData object has unexpected "
356-
"uncertainty type. instead, try "
357-
"VarianceUncertainty or StdDevUncertainty.")
358-
else:
359-
# ignore variance arg to focus on updating NDData object
360-
raise ValueError('image NDData object lacks uncertainty')
361-
362-
else:
363-
if variance is None:
364-
raise ValueError('if image is a numpy array, a variance must '
365-
'be specified. consider wrapping it into one '
366-
'object by instead passing an NDData image.')
367-
elif image.shape != variance.shape:
368-
raise ValueError('image and variance shapes must match')
369-
370-
# check optional arguments, filling them in if absent
371-
if mask is None:
372-
mask = np.ma.masked_invalid(image).mask
373-
elif image.shape != mask.shape:
374-
raise ValueError('image and mask shapes must match.')
375-
376-
if isinstance(unit, str):
377-
unit = u.Unit(unit)
378-
else:
379-
unit = unit if unit is not None else u.Unit()
380-
381-
# create image
382-
img = np.ma.array(image, mask=mask)
383-
384-
if np.all(variance == 0):
385-
# technically would result in infinities, but since they're all zeros
386-
# we can just do the unweighted case by overriding with all ones
387-
variance = np.ones_like(variance)
447+
# parse image and replace optional arguments with updated values
448+
self._parse_image(variance, mask, unit)
449+
variance = self.image.uncertainty.array
450+
unit = self.image.unit
388451

389-
if np.any(variance < 0):
390-
raise ValueError("variance must be fully positive")
391-
392-
if np.any(variance == 0):
393-
# exclude these elements by editing the input mask
394-
img.mask[variance == 0] = True
395-
# replace the variances to avoid a divide by zero warning
396-
variance[variance == 0] = np.nan
452+
# mask any previously uncaught invalid values
453+
or_mask = np.logical_or(mask,
454+
np.ma.masked_invalid(self.image.data).mask)
455+
img = np.ma.masked_array(self.image.data, or_mask)
456+
mask = img.mask
397457

398458
# co-add signal in each image column
399459
ncols = img.shape[crossdisp_axis]

specreduce/tracing.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
import warnings
66

77
from astropy.modeling import fitting, models
8-
from astropy.nddata import NDData
8+
from astropy.nddata import NDData, VarianceUncertainty
99
from astropy.stats import gaussian_sigma_to_fwhm
10+
from astropy import units as u
1011
from scipy.interpolate import UnivariateSpline
1112
from specutils import Spectrum1D
1213
import numpy as np
@@ -39,9 +40,39 @@ def __getitem__(self, i):
3940
return self.trace[i]
4041

4142
def _parse_image(self):
42-
if isinstance(self.image, Spectrum1D):
43-
# NOTE: should the Spectrum1D structure instead be preserved?
44-
self.image = self.image.data
43+
"""
44+
Convert all accepted image types to a consistently formatted Spectrum1D.
45+
"""
46+
47+
if isinstance(self.image, np.ndarray):
48+
img = self.image
49+
elif isinstance(self.image, u.quantity.Quantity):
50+
img = self.image.value
51+
else: # NDData, including CCDData and Spectrum1D
52+
img = self.image.data
53+
54+
# mask and uncertainty are set as None when they aren't specified upon
55+
# creating a Spectrum1D object, so we must check whether these
56+
# attributes are absent *and* whether they are present but set as None
57+
if getattr(self.image, 'mask', None) is not None:
58+
mask = self.image.mask
59+
else:
60+
mask = np.ma.masked_invalid(img).mask
61+
62+
if getattr(self.image, 'uncertainty', None) is not None:
63+
uncertainty = self.image.uncertainty
64+
else:
65+
uncertainty = VarianceUncertainty(np.ones(img.shape))
66+
67+
unit = getattr(self.image, 'unit', u.Unit('DN')) # or u.Unit()?
68+
69+
spectral_axis = getattr(self.image, 'spectral_axis',
70+
(np.arange(img.shape[self._disp_axis])
71+
if hasattr(self, '_disp_axis')
72+
else np.arange(img.shape[1])) * u.pix)
73+
74+
self.image = Spectrum1D(img * unit, spectral_axis=spectral_axis,
75+
uncertainty=uncertainty, mask=mask)
4576

4677
@property
4778
def shape(self):
@@ -115,7 +146,7 @@ def set_position(self, trace_pos):
115146
Position of the trace
116147
"""
117148
self.trace_pos = trace_pos
118-
self.trace = np.ones_like(self.image[0]) * self.trace_pos
149+
self.trace = np.ones_like(self.image.data[0]) * self.trace_pos
119150
self._bound_trace()
120151

121152

@@ -212,12 +243,10 @@ class KosmosTrace(Trace):
212243
def __post_init__(self):
213244
super()._parse_image()
214245

215-
# handle multiple image types and mask uncaught invalid values
216-
if isinstance(self.image, NDData):
217-
img = np.ma.masked_invalid(np.ma.masked_array(self.image.data,
218-
mask=self.image.mask))
219-
else:
220-
img = np.ma.masked_invalid(self.image)
246+
# mask any previously uncaught invalid values
247+
or_mask = np.logical_or(self.image.mask,
248+
np.ma.masked_invalid(self.image.data).mask)
249+
img = np.ma.masked_array(self.image.data, or_mask)
221250

222251
# validate arguments
223252
valid_peak_methods = ('gaussian', 'centroid', 'max')

0 commit comments

Comments
 (0)