|
7 | 7 |
|
8 | 8 | from astropy import units as u
|
9 | 9 | from astropy.modeling import Model, models, fitting
|
10 |
| -from astropy.nddata import NDData |
| 10 | +from astropy.nddata import NDData, VarianceUncertainty |
11 | 11 |
|
12 | 12 | from specreduce.core import SpecreduceOperation
|
13 | 13 | from specreduce.tracing import Trace, FlatTrace
|
@@ -149,6 +149,41 @@ class BoxcarExtract(SpecreduceOperation):
|
149 | 149 | def spectrum(self):
|
150 | 150 | return self.__call__()
|
151 | 151 |
|
| 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 | + |
152 | 187 | def __call__(self, image=None, trace_object=None, width=None,
|
153 | 188 | disp_axis=None, crossdisp_axis=None):
|
154 | 189 | """
|
@@ -267,6 +302,86 @@ class HorneExtract(SpecreduceOperation):
|
267 | 302 | def spectrum(self):
|
268 | 303 | return self.__call__()
|
269 | 304 |
|
| 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 | + |
270 | 385 | def __call__(self, image=None, trace_object=None,
|
271 | 386 | disp_axis=None, crossdisp_axis=None,
|
272 | 387 | bkgrd_prof=None,
|
@@ -329,71 +444,16 @@ def __call__(self, image=None, trace_object=None,
|
329 | 444 | mask = mask if mask is not None else self.mask
|
330 | 445 | unit = unit if unit is not None else self.unit
|
331 | 446 |
|
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 |
388 | 451 |
|
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 |
397 | 457 |
|
398 | 458 | # co-add signal in each image column
|
399 | 459 | ncols = img.shape[crossdisp_axis]
|
|
0 commit comments