|
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
|
@@ -164,6 +164,41 @@ class BoxcarExtract(SpecreduceOperation):
|
164 | 164 | def spectrum(self):
|
165 | 165 | return self.__call__()
|
166 | 166 |
|
| 167 | + def _parse_image(self): |
| 168 | + """ |
| 169 | + Convert all accepted image types to a consistently formatted Spectrum1D. |
| 170 | + """ |
| 171 | + |
| 172 | + if isinstance(self.image, np.ndarray): |
| 173 | + img = self.image |
| 174 | + elif isinstance(self.image, u.quantity.Quantity): |
| 175 | + img = self.image.value |
| 176 | + else: # NDData, including CCDData and Spectrum1D |
| 177 | + img = self.image.data |
| 178 | + |
| 179 | + # mask and uncertainty are set as None when they aren't specified upon |
| 180 | + # creating a Spectrum1D object, so we must check whether these |
| 181 | + # attributes are absent *and* whether they are present but set as None |
| 182 | + if getattr(self.image, 'mask', None) is not None: |
| 183 | + mask = self.image.mask |
| 184 | + else: |
| 185 | + mask = np.ma.masked_invalid(img).mask |
| 186 | + |
| 187 | + if getattr(self.image, 'uncertainty', None) is not None: |
| 188 | + uncertainty = self.image.uncertainty |
| 189 | + else: |
| 190 | + uncertainty = VarianceUncertainty(np.ones(img.shape)) |
| 191 | + |
| 192 | + unit = getattr(self.image, 'unit', u.Unit('DN')) # or u.Unit()? |
| 193 | + |
| 194 | + spectral_axis = getattr(self.image, 'spectral_axis', |
| 195 | + (np.arange(img.shape[self.disp_axis]) |
| 196 | + if hasattr(self, 'disp_axis') |
| 197 | + else np.arange(img.shape[1])) * u.pix) |
| 198 | + |
| 199 | + self.image = Spectrum1D(img * unit, spectral_axis=spectral_axis, |
| 200 | + uncertainty=uncertainty, mask=mask) |
| 201 | + |
167 | 202 | def __call__(self, image=None, trace_object=None, width=None,
|
168 | 203 | disp_axis=None, crossdisp_axis=None):
|
169 | 204 | """
|
@@ -285,6 +320,86 @@ class HorneExtract(SpecreduceOperation):
|
285 | 320 | def spectrum(self):
|
286 | 321 | return self.__call__()
|
287 | 322 |
|
| 323 | + def _parse_image(self, variance=None, mask=None, unit=None): |
| 324 | + """ |
| 325 | + Convert all accepted image types to a consistently formatted Spectrum1D. |
| 326 | + Takes some extra arguments exactly as they come from self.__call__() to |
| 327 | + handle cases where users specify them as arguments instead of as |
| 328 | + attributes of their image object. |
| 329 | + """ |
| 330 | + |
| 331 | + if isinstance(self.image, np.ndarray): |
| 332 | + img = self.image |
| 333 | + elif isinstance(self.image, u.quantity.Quantity): |
| 334 | + img = self.image.value |
| 335 | + else: # NDData, including CCDData and Spectrum1D |
| 336 | + img = self.image.data |
| 337 | + |
| 338 | + # mask is set as None when not specified upon creating a Spectrum1D |
| 339 | + # object, so we must check whether it is absent *and* whether it's |
| 340 | + # present but set as None |
| 341 | + if getattr(self.image, 'mask', None) is not None: |
| 342 | + mask = self.image.mask |
| 343 | + else: |
| 344 | + mask = np.ma.masked_invalid(img).mask |
| 345 | + |
| 346 | + # Process uncertainties, converting to variances when able and throwing |
| 347 | + # an error when uncertainties are missing or less easily converted |
| 348 | + if (hasattr(self.image, 'uncertainty') |
| 349 | + and self.image.uncertainty is not None): |
| 350 | + if self.image.uncertainty.uncertainty_type == 'var': |
| 351 | + variance = self.image.uncertainty.array |
| 352 | + elif self.image.uncertainty.uncertainty_type == 'std': |
| 353 | + warnings.warn("image NDData object's uncertainty " |
| 354 | + "interpreted as standard deviation. if " |
| 355 | + "incorrect, use VarianceUncertainty when " |
| 356 | + "assigning image object's uncertainty.") |
| 357 | + variance = self.image.uncertainty.array**2 |
| 358 | + elif self.image.uncertainty.uncertainty_type == 'ivar': |
| 359 | + variance = 1 / self.image.uncertainty.array |
| 360 | + else: |
| 361 | + # other options are InverseVariance and UnknownVariance |
| 362 | + raise ValueError("image NDData object has unexpected " |
| 363 | + "uncertainty type. instead, try " |
| 364 | + "VarianceUncertainty or StdDevUncertainty.") |
| 365 | + elif (hasattr(self.image, 'uncertainty') |
| 366 | + and self.image.uncertainty is None): |
| 367 | + # ignore variance arg to focus on updating NDData object |
| 368 | + raise ValueError('image NDData object lacks uncertainty') |
| 369 | + else: |
| 370 | + if variance is None: |
| 371 | + raise ValueError("if image is a numpy or Quantity array, a " |
| 372 | + "variance must be specified. consider " |
| 373 | + "wrapping it into one object by instead " |
| 374 | + "passing an NDData image.") |
| 375 | + elif self.image.shape != variance.shape: |
| 376 | + raise ValueError("image and variance shapes must match") |
| 377 | + |
| 378 | + if np.any(variance < 0): |
| 379 | + raise ValueError("variance must be fully positive") |
| 380 | + if np.all(variance == 0): |
| 381 | + # technically would result in infinities, but since they're all |
| 382 | + # zeros, we can override ones to simulate an unweighted case |
| 383 | + variance = np.ones_like(variance) |
| 384 | + if np.any(variance == 0): |
| 385 | + # exclude such elements by editing the input mask |
| 386 | + mask[variance == 0] = True |
| 387 | + # replace the variances to avoid a divide by zero warning |
| 388 | + variance[variance == 0] = np.nan |
| 389 | + |
| 390 | + variance = VarianceUncertainty(variance) |
| 391 | + |
| 392 | + unit = getattr(self.image, 'unit', |
| 393 | + u.Unit(self.unit) if self.unit is not None else u.Unit()) |
| 394 | + |
| 395 | + spectral_axis = getattr(self.image, 'spectral_axis', |
| 396 | + (np.arange(img.shape[self.disp_axis]) |
| 397 | + if hasattr(self, 'disp_axis') |
| 398 | + else np.arange(img.shape[1])) * u.pix) |
| 399 | + |
| 400 | + self.image = Spectrum1D(img * unit, spectral_axis=spectral_axis, |
| 401 | + uncertainty=variance, mask=mask) |
| 402 | + |
288 | 403 | def __call__(self, image=None, trace_object=None,
|
289 | 404 | disp_axis=None, crossdisp_axis=None,
|
290 | 405 | bkgrd_prof=None,
|
@@ -347,71 +462,16 @@ def __call__(self, image=None, trace_object=None,
|
347 | 462 | mask = mask if mask is not None else self.mask
|
348 | 463 | unit = unit if unit is not None else self.unit
|
349 | 464 |
|
350 |
| - # handle image and associated data based on image's type |
351 |
| - if isinstance(image, NDData): |
352 |
| - # (NDData includes Spectrum1D under its umbrella) |
353 |
| - img = np.ma.array(image.data, mask=image.mask) |
354 |
| - unit = image.unit if image.unit is not None else u.Unit() |
355 |
| - |
356 |
| - if image.uncertainty is not None: |
357 |
| - # prioritize NDData's uncertainty over variance argument |
358 |
| - if image.uncertainty.uncertainty_type == 'var': |
359 |
| - variance = image.uncertainty.array |
360 |
| - elif image.uncertainty.uncertainty_type == 'std': |
361 |
| - # NOTE: CCDData defaults uncertainties given as pure arrays |
362 |
| - # to std and logs a warning saying so upon object creation. |
363 |
| - # should we remind users again here? |
364 |
| - warnings.warn("image NDData object's uncertainty " |
365 |
| - "interpreted as standard deviation. if " |
366 |
| - "incorrect, use VarianceUncertainty when " |
367 |
| - "assigning image object's uncertainty.") |
368 |
| - variance = image.uncertainty.array**2 |
369 |
| - elif image.uncertainty.uncertainty_type == 'ivar': |
370 |
| - variance = 1 / image.uncertainty.array |
371 |
| - else: |
372 |
| - # other options are InverseVariance and UnknownVariance |
373 |
| - raise ValueError("image NDData object has unexpected " |
374 |
| - "uncertainty type. instead, try " |
375 |
| - "VarianceUncertainty or StdDevUncertainty.") |
376 |
| - else: |
377 |
| - # ignore variance arg to focus on updating NDData object |
378 |
| - raise ValueError('image NDData object lacks uncertainty') |
379 |
| - |
380 |
| - else: |
381 |
| - if variance is None: |
382 |
| - raise ValueError('if image is a numpy array, a variance must ' |
383 |
| - 'be specified. consider wrapping it into one ' |
384 |
| - 'object by instead passing an NDData image.') |
385 |
| - elif image.shape != variance.shape: |
386 |
| - raise ValueError('image and variance shapes must match') |
387 |
| - |
388 |
| - # check optional arguments, filling them in if absent |
389 |
| - if mask is None: |
390 |
| - mask = np.ma.masked_invalid(image).mask |
391 |
| - elif image.shape != mask.shape: |
392 |
| - raise ValueError('image and mask shapes must match.') |
393 |
| - |
394 |
| - if isinstance(unit, str): |
395 |
| - unit = u.Unit(unit) |
396 |
| - else: |
397 |
| - unit = unit if unit is not None else u.Unit() |
398 |
| - |
399 |
| - # create image |
400 |
| - img = np.ma.array(image, mask=mask) |
401 |
| - |
402 |
| - if np.all(variance == 0): |
403 |
| - # technically would result in infinities, but since they're all zeros |
404 |
| - # we can just do the unweighted case by overriding with all ones |
405 |
| - variance = np.ones_like(variance) |
| 465 | + # parse image and replace optional arguments with updated values |
| 466 | + self._parse_image(variance, mask, unit) |
| 467 | + variance = self.image.uncertainty.array |
| 468 | + unit = self.image.unit |
406 | 469 |
|
407 |
| - if np.any(variance < 0): |
408 |
| - raise ValueError("variance must be fully positive") |
409 |
| - |
410 |
| - if np.any(variance == 0): |
411 |
| - # exclude these elements by editing the input mask |
412 |
| - img.mask[variance == 0] = True |
413 |
| - # replace the variances to avoid a divide by zero warning |
414 |
| - variance[variance == 0] = np.nan |
| 470 | + # mask any previously uncaught invalid values |
| 471 | + or_mask = np.logical_or(mask, |
| 472 | + np.ma.masked_invalid(self.image.data).mask) |
| 473 | + img = np.ma.masked_array(self.image.data, or_mask) |
| 474 | + mask = img.mask |
415 | 475 |
|
416 | 476 | # co-add signal in each image column
|
417 | 477 | ncols = img.shape[crossdisp_axis]
|
|
0 commit comments