LSSTApplications  18.1.0
LSSTDataManagementBasePackage
dcrModel.py
Go to the documentation of this file.
1 # This file is part of ip_diffim.
2 #
3 # LSST Data Management System
4 # This product includes software developed by the
5 # LSST Project (http://www.lsst.org/).
6 # See COPYRIGHT file at the top of the source tree.
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 
23 import numpy as np
24 from scipy import ndimage
25 from lsst.afw.coord.refraction import differentialRefraction
26 import lsst.afw.geom as afwGeom
27 import lsst.afw.image as afwImage
28 from lsst.geom import radians
29 
30 __all__ = ["DcrModel", "applyDcr", "calculateDcr", "calculateImageParallacticAngle"]
31 
32 
33 class DcrModel:
34  """A model of the true sky after correcting chromatic effects.
35 
36  Attributes
37  ----------
38  dcrNumSubfilters : `int`
39  Number of sub-filters used to model chromatic effects within a band.
40  modelImages : `list` of `lsst.afw.image.Image`
41  A list of masked images, each containing the model for one subfilter
42 
43  Notes
44  -----
45  The ``DcrModel`` contains an estimate of the true sky, at a higher
46  wavelength resolution than the input observations. It can be forward-
47  modeled to produce Differential Chromatic Refraction (DCR) matched
48  templates for a given ``Exposure``, and provides utilities for conditioning
49  the model in ``dcrAssembleCoadd`` to avoid oscillating solutions between
50  iterations of forward modeling or between the subfilters of the model.
51  """
52 
53  def __init__(self, modelImages, filterInfo=None, psf=None, mask=None, variance=None):
54  self.dcrNumSubfilters = len(modelImages)
55  self.modelImages = modelImages
56  self._filter = filterInfo
57  self._psf = psf
58  self._mask = mask
59  self._variance = variance
60 
61  @classmethod
62  def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None):
63  """Initialize a DcrModel by dividing a coadd between the subfilters.
64 
65  Parameters
66  ----------
67  maskedImage : `lsst.afw.image.MaskedImage`
68  Input coadded image to divide equally between the subfilters.
69  dcrNumSubfilters : `int`
70  Number of sub-filters used to model chromatic effects within a band.
71  filterInfo : `lsst.afw.image.Filter`, optional
72  The filter definition, set in the current instruments' obs package.
73  Required for any calculation of DCR, including making matched templates.
74  psf : `lsst.afw.detection.Psf`, optional
75  Point spread function (PSF) of the model.
76  Required if the ``DcrModel`` will be persisted.
77 
78  Returns
79  -------
80  dcrModel : `lsst.pipe.tasks.DcrModel`
81  Best fit model of the true sky after correcting chromatic effects.
82 
83  Raises
84  ------
85  ValueError
86  If there are any unmasked NAN values in ``maskedImage``.
87  """
88  # NANs will potentially contaminate the entire image,
89  # depending on the shift or convolution type used.
90  model = maskedImage.image.clone()
91  mask = maskedImage.mask.clone()
92  # We divide the variance by N and not N**2 because we will assume each
93  # subfilter is independent. That means that the significance of
94  # detected sources will be lower by a factor of sqrt(N) in the
95  # subfilter images, but we will recover it when we combine the
96  # subfilter images to construct matched templates.
97  variance = maskedImage.variance.clone()
98  variance /= dcrNumSubfilters
99  model /= dcrNumSubfilters
100  modelImages = [model, ]
101  for subfilter in range(1, dcrNumSubfilters):
102  modelImages.append(model.clone())
103  return cls(modelImages, filterInfo, psf, mask, variance)
104 
105  @classmethod
106  def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, **kwargs):
107  """Load an existing DcrModel from a repository.
108 
109  Parameters
110  ----------
111  dataRef : `lsst.daf.persistence.ButlerDataRef`
112  Data reference defining the patch for coaddition and the
113  reference Warp
114  datasetType : `str`, optional
115  Name of the DcrModel in the registry {"dcrCoadd", "dcrCoadd_sub"}
116  numSubfilters : `int`
117  Number of sub-filters used to model chromatic effects within a band.
118  **kwargs
119  Additional keyword arguments to pass to look up the model in the data registry.
120  Common keywords and their types include: ``tract``:`str`, ``patch``:`str`,
121  ``bbox``:`lsst.afw.geom.Box2I`
122 
123  Returns
124  -------
125  dcrModel : `lsst.pipe.tasks.DcrModel`
126  Best fit model of the true sky after correcting chromatic effects.
127  """
128  modelImages = []
129  filterInfo = None
130  psf = None
131  mask = None
132  variance = None
133  for subfilter in range(numSubfilters):
134  dcrCoadd = dataRef.get(datasetType, subfilter=subfilter,
135  numSubfilters=numSubfilters, **kwargs)
136  if filterInfo is None:
137  filterInfo = dcrCoadd.getFilter()
138  if psf is None:
139  psf = dcrCoadd.getPsf()
140  if mask is None:
141  mask = dcrCoadd.mask
142  if variance is None:
143  variance = dcrCoadd.variance
144  modelImages.append(dcrCoadd.image)
145  return cls(modelImages, filterInfo, psf, mask, variance)
146 
147  def __len__(self):
148  """Return the number of subfilters.
149 
150  Returns
151  -------
152  dcrNumSubfilters : `int`
153  The number of DCR subfilters in the model.
154  """
155  return self.dcrNumSubfilters
156 
157  def __getitem__(self, subfilter):
158  """Iterate over the subfilters of the DCR model.
159 
160  Parameters
161  ----------
162  subfilter : `int`
163  Index of the current ``subfilter`` within the full band.
164  Negative indices are allowed, and count in reverse order
165  from the highest ``subfilter``.
166 
167  Returns
168  -------
169  modelImage : `lsst.afw.image.Image`
170  The DCR model for the given ``subfilter``.
171 
172  Raises
173  ------
174  IndexError
175  If the requested ``subfilter`` is greater or equal to the number
176  of subfilters in the model.
177  """
178  if np.abs(subfilter) >= len(self):
179  raise IndexError("subfilter out of bounds.")
180  return self.modelImages[subfilter]
181 
182  def __setitem__(self, subfilter, maskedImage):
183  """Update the model image for one subfilter.
184 
185  Parameters
186  ----------
187  subfilter : `int`
188  Index of the current subfilter within the full band.
189  maskedImage : `lsst.afw.image.Image`
190  The DCR model to set for the given ``subfilter``.
191 
192  Raises
193  ------
194  IndexError
195  If the requested ``subfilter`` is greater or equal to the number
196  of subfilters in the model.
197  ValueError
198  If the bounding box of the new image does not match.
199  """
200  if np.abs(subfilter) >= len(self):
201  raise IndexError("subfilter out of bounds.")
202  if maskedImage.getBBox() != self.bbox:
203  raise ValueError("The bounding box of a subfilter must not change.")
204  self.modelImages[subfilter] = maskedImage
205 
206  @property
207  def filter(self):
208  """Return the filter of the model.
209 
210  Returns
211  -------
212  filter : `lsst.afw.image.Filter`
213  The filter definition, set in the current instruments' obs package.
214  """
215  return self._filter
216 
217  @property
218  def psf(self):
219  """Return the psf of the model.
220 
221  Returns
222  -------
223  psf : `lsst.afw.detection.Psf`
224  Point spread function (PSF) of the model.
225  """
226  return self._psf
227 
228  @property
229  def bbox(self):
230  """Return the common bounding box of each subfilter image.
231 
232  Returns
233  -------
234  bbox : `lsst.afw.geom.Box2I`
235  Bounding box of the DCR model.
236  """
237  return self[0].getBBox()
238 
239  @property
240  def mask(self):
241  """Return the common mask of each subfilter image.
242 
243  Returns
244  -------
245  mask : `lsst.afw.image.Mask`
246  Mask plane of the DCR model.
247  """
248  return self._mask
249 
250  @property
251  def variance(self):
252  """Return the common variance of each subfilter image.
253 
254  Returns
255  -------
256  variance : `lsst.afw.image.Image`
257  Variance plane of the DCR model.
258  """
259  return self._variance
260 
261  def getReferenceImage(self, bbox=None):
262  """Calculate a reference image from the average of the subfilter images.
263 
264  Parameters
265  ----------
266  bbox : `lsst.afw.geom.Box2I`, optional
267  Sub-region of the coadd. Returns the entire image if `None`.
268 
269  Returns
270  -------
271  refImage : `numpy.ndarray`
272  The reference image with no chromatic effects applied.
273  """
274  bbox = bbox or self.bbox
275  return np.mean([model[bbox].array for model in self], axis=0)
276 
277  def assign(self, dcrSubModel, bbox=None):
278  """Update a sub-region of the ``DcrModel`` with new values.
279 
280  Parameters
281  ----------
282  dcrSubModel : `lsst.pipe.tasks.DcrModel`
283  New model of the true scene after correcting chromatic effects.
284  bbox : `lsst.afw.geom.Box2I`, optional
285  Sub-region of the coadd.
286  Defaults to the bounding box of ``dcrSubModel``.
287 
288  Raises
289  ------
290  ValueError
291  If the new model has a different number of subfilters.
292  """
293  if len(dcrSubModel) != len(self):
294  raise ValueError("The number of DCR subfilters must be the same "
295  "between the old and new models.")
296  bbox = bbox or self.bbox
297  for model, subModel in zip(self, dcrSubModel):
298  model.assign(subModel[bbox], bbox)
299 
300  def buildMatchedTemplate(self, exposure=None, order=3,
301  visitInfo=None, bbox=None, wcs=None, mask=None,
302  splitSubfilters=True, amplifyModel=1.):
303  """Create a DCR-matched template image for an exposure.
304 
305  Parameters
306  ----------
307  exposure : `lsst.afw.image.Exposure`, optional
308  The input exposure to build a matched template for.
309  May be omitted if all of the metadata is supplied separately
310  order : `int`, optional
311  Interpolation order of the DCR shift.
312  visitInfo : `lsst.afw.image.VisitInfo`, optional
313  Metadata for the exposure. Ignored if ``exposure`` is set.
314  bbox : `lsst.afw.geom.Box2I`, optional
315  Sub-region of the coadd. Ignored if ``exposure`` is set.
316  wcs : `lsst.afw.geom.SkyWcs`, optional
317  Coordinate system definition (wcs) for the exposure.
318  Ignored if ``exposure`` is set.
319  mask : `lsst.afw.image.Mask`, optional
320  reference mask to use for the template image.
321  splitSubfilters : `bool`, optional
322  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
323  instead of at the midpoint. Default: True
324  amplifyModel : `float`, optional
325  Multiplication factor to amplify differences between model planes.
326  Used to speed convergence of iterative forward modeling.
327 
328  Returns
329  -------
330  templateImage : `lsst.afw.image.ImageF`
331  The DCR-matched template
332 
333  Raises
334  ------
335  ValueError
336  If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set.
337  """
338  if self.filter is None:
339  raise ValueError("'filterInfo' must be set for the DcrModel in order to calculate DCR.")
340  if exposure is not None:
341  visitInfo = exposure.getInfo().getVisitInfo()
342  bbox = exposure.getBBox()
343  wcs = exposure.getInfo().getWcs()
344  elif visitInfo is None or bbox is None or wcs is None:
345  raise ValueError("Either exposure or visitInfo, bbox, and wcs must be set.")
346  dcrShift = calculateDcr(visitInfo, wcs, self.filter, len(self), splitSubfilters=splitSubfilters)
347  templateImage = afwImage.ImageF(bbox)
348  refModel = self.getReferenceImage(bbox)
349  for subfilter, dcr in enumerate(dcrShift):
350  if amplifyModel > 1:
351  model = (self[subfilter][bbox].array - refModel)*amplifyModel + refModel
352  else:
353  model = self[subfilter][bbox].array
354  templateImage.array += applyDcr(model, dcr, splitSubfilters=splitSubfilters, order=order)
355  return templateImage
356 
357  def buildMatchedExposure(self, exposure=None,
358  visitInfo=None, bbox=None, wcs=None, mask=None):
359  """Wrapper to create an exposure from a template image.
360 
361  Parameters
362  ----------
363  exposure : `lsst.afw.image.Exposure`, optional
364  The input exposure to build a matched template for.
365  May be omitted if all of the metadata is supplied separately
366  visitInfo : `lsst.afw.image.VisitInfo`, optional
367  Metadata for the exposure. Ignored if ``exposure`` is set.
368  bbox : `lsst.afw.geom.Box2I`, optional
369  Sub-region of the coadd. Ignored if ``exposure`` is set.
370  wcs : `lsst.afw.geom.SkyWcs`, optional
371  Coordinate system definition (wcs) for the exposure.
372  Ignored if ``exposure`` is set.
373  mask : `lsst.afw.image.Mask`, optional
374  reference mask to use for the template image.
375 
376  Returns
377  -------
378  templateExposure : `lsst.afw.image.exposureF`
379  The DCR-matched template
380  """
381  if bbox is None:
382  bbox = exposure.getBBox()
383  templateImage = self.buildMatchedTemplate(exposure=exposure, visitInfo=visitInfo,
384  bbox=bbox, wcs=wcs, mask=mask)
385  maskedImage = afwImage.MaskedImageF(bbox)
386  maskedImage.image = templateImage[bbox]
387  maskedImage.mask = self.mask[bbox]
388  maskedImage.variance = self.variance[bbox]
389  templateExposure = afwImage.ExposureF(bbox, wcs)
390  templateExposure.setMaskedImage(maskedImage[bbox])
391  templateExposure.setPsf(self.psf)
392  templateExposure.setFilter(self.filter)
393  return templateExposure
394 
395  def conditionDcrModel(self, modelImages, bbox, gain=1.):
396  """Average two iterations' solutions to reduce oscillations.
397 
398  Parameters
399  ----------
400  modelImages : `list` of `lsst.afw.image.Image`
401  The new DCR model images from the current iteration.
402  The values will be modified in place.
403  bbox : `lsst.afw.geom.Box2I`
404  Sub-region of the coadd
405  gain : `float`, optional
406  Relative weight to give the new solution when updating the model.
407  Defaults to 1.0, which gives equal weight to both solutions.
408  """
409  # Calculate weighted averages of the images.
410  for model, newModel in zip(self, modelImages):
411  newModel *= gain
412  newModel += model[bbox]
413  newModel /= 1. + gain
414 
415  def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor,
416  regularizationWidth=2):
417  """Restrict large variations in the model between iterations.
418 
419  Parameters
420  ----------
421  subfilter : `int`
422  Index of the current subfilter within the full band.
423  newModel : `lsst.afw.image.Image`
424  The new DCR model for one subfilter from the current iteration.
425  Values in ``newModel`` that are extreme compared with the last
426  iteration are modified in place.
427  bbox : `lsst.afw.geom.Box2I`
428  Sub-region to coadd
429  regularizationFactor : `float`
430  Maximum relative change of the model allowed between iterations.
431  regularizationWidth : int, optional
432  Minimum radius of a region to include in regularization, in pixels.
433  """
434  refImage = self[subfilter][bbox].array
435  highThreshold = np.abs(refImage)*regularizationFactor
436  lowThreshold = refImage/regularizationFactor
437  newImage = newModel.array
438  self.applyImageThresholds(newImage, highThreshold=highThreshold, lowThreshold=lowThreshold,
439  regularizationWidth=regularizationWidth)
440 
441  def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor,
442  regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
443  """Restrict large variations in the model between subfilters.
444 
445  Parameters
446  ----------
447  modelImages : `list` of `lsst.afw.image.Image`
448  The new DCR model images from the current iteration.
449  The values will be modified in place.
450  bbox : `lsst.afw.geom.Box2I`
451  Sub-region to coadd
452  statsCtrl : `lsst.afw.math.StatisticsControl`
453  Statistics control object for coaddition.
454  regularizationFactor : `float`
455  Maximum relative change of the model allowed between subfilters.
456  regularizationWidth : `int`, optional
457  Minimum radius of a region to include in regularization, in pixels.
458  mask : `lsst.afw.image.Mask`, optional
459  Optional alternate mask
460  convergenceMaskPlanes : `list` of `str`, or `str`, optional
461  Mask planes to use to calculate convergence.
462 
463  Notes
464  -----
465  This implementation of frequency regularization restricts each subfilter
466  image to be a smoothly-varying function times a reference image.
467  """
468  # ``regularizationFactor`` is the maximum change between subfilter images, so the maximum difference
469  # between one subfilter image and the average will be the square root of that.
470  maxDiff = np.sqrt(regularizationFactor)
471  noiseLevel = self.calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
472  referenceImage = self.getReferenceImage(bbox)
473  badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
474  if np.sum(~badPixels) == 0:
475  # Skip regularization if there are no valid pixels
476  return
477  referenceImage[badPixels] = 0.
478  filterWidth = regularizationWidth
479  fwhm = 2.*filterWidth
480  # The noise should be lower in the smoothed image by sqrt(Nsmooth) ~ fwhm pixels
481  noiseLevel /= fwhm
482  smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth, mode='constant')
483  # Add a three sigma offset to both the reference and model to prevent dividing by zero.
484  # Note that this will also slightly suppress faint variations in color.
485  smoothRef += 3.*noiseLevel
486 
487  lowThreshold = smoothRef/maxDiff
488  highThreshold = smoothRef*maxDiff
489  for model in modelImages:
490  self.applyImageThresholds(model.array,
491  highThreshold=highThreshold,
492  lowThreshold=lowThreshold,
493  regularizationWidth=regularizationWidth)
494  smoothModel = ndimage.filters.gaussian_filter(model.array, filterWidth, mode='constant')
495  smoothModel += 3.*noiseLevel
496  relativeModel = smoothModel/smoothRef
497  # Now sharpen the smoothed relativeModel using an alpha of 3.
498  alpha = 3.
499  relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/alpha)
500  relativeModel += alpha*(relativeModel - relativeModel2)
501  model.array = relativeModel*referenceImage
502 
503  def calculateNoiseCutoff(self, image, statsCtrl, bufferSize,
504  convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
505  """Helper function to calculate the background noise level of an image.
506 
507  Parameters
508  ----------
509  image : `lsst.afw.image.Image`
510  The input image to evaluate the background noise properties.
511  statsCtrl : `lsst.afw.math.StatisticsControl`
512  Statistics control object for coaddition.
513  bufferSize : `int`
514  Number of additional pixels to exclude
515  from the edges of the bounding box.
516  convergenceMaskPlanes : `list` of `str`, or `str`
517  Mask planes to use to calculate convergence.
518  mask : `lsst.afw.image.Mask`, Optional
519  Optional alternate mask
520  bbox : `lsst.afw.geom.Box2I`, optional
521  Sub-region of the masked image to calculate the noise level over.
522 
523  Returns
524  -------
525  noiseCutoff : `float`
526  The threshold value to treat pixels as noise in an image..
527  """
528  if bbox is None:
529  bbox = self.bbox
530  if mask is None:
531  mask = self.mask[bbox]
532  bboxShrink = afwGeom.Box2I(bbox)
533  bboxShrink.grow(-bufferSize)
534  convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
535 
536  backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
537  noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
538  return noiseCutoff
539 
540  def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2):
541  """Restrict image values to be between upper and lower limits.
542 
543  This method flags all pixels in an image that are outside of the given
544  threshold values. The threshold values are taken from a reference image,
545  so noisy pixels are likely to get flagged. In order to exclude those
546  noisy pixels, the array of flags is eroded and dilated, which removes
547  isolated pixels outside of the thresholds from the list of pixels to be
548  modified. Pixels that remain flagged after this operation have their
549  values set to the appropriate upper or lower threshold value.
550 
551  Parameters
552  ----------
553  image : `numpy.ndarray`
554  The image to apply the thresholds to.
555  The values will be modified in place.
556  highThreshold : `numpy.ndarray`, optional
557  Array of upper limit values for each pixel of ``image``.
558  lowThreshold : `numpy.ndarray`, optional
559  Array of lower limit values for each pixel of ``image``.
560  regularizationWidth : `int`, optional
561  Minimum radius of a region to include in regularization, in pixels.
562  """
563  # Generate the structure for binary erosion and dilation, which is used to remove noise-like pixels.
564  # Groups of pixels with a radius smaller than ``regularizationWidth``
565  # will be excluded from regularization.
566  filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
567  regularizationWidth)
568  if highThreshold is not None:
569  highPixels = image > highThreshold
570  if regularizationWidth > 0:
571  # Erode and dilate ``highPixels`` to exclude noisy pixels.
572  highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
573  image[highPixels] = highThreshold[highPixels]
574  if lowThreshold is not None:
575  lowPixels = image < lowThreshold
576  if regularizationWidth > 0:
577  # Erode and dilate ``lowPixels`` to exclude noisy pixels.
578  lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
579  image[lowPixels] = lowThreshold[lowPixels]
580 
581 
582 def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, **kwargs):
583  """Shift an image along the X and Y directions.
584 
585  Parameters
586  ----------
587  image : `numpy.ndarray`
588  The input image to shift.
589  dcr : `tuple`
590  Shift calculated with ``calculateDcr``.
591  Uses numpy axes ordering (Y, X).
592  If ``splitSubfilters`` is set, each element is itself a `tuple`
593  of two `float`, corresponding to the DCR shift at the two wavelengths.
594  Otherwise, each element is a `float` corresponding to the DCR shift at
595  the effective wavelength of the subfilter.
596  useInverse : `bool`, optional
597  Apply the shift in the opposite direction. Default: False
598  splitSubfilters : `bool`, optional
599  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
600  instead of at the midpoint. Default: False
601  kwargs
602  Additional keyword parameters to pass in to
603  `scipy.ndimage.interpolation.shift`
604 
605  Returns
606  -------
607  shiftedImage : `numpy.ndarray`
608  A copy of the input image with the specified shift applied.
609  """
610  if splitSubfilters:
611  if useInverse:
612  shift = [-1.*s for s in dcr[0]]
613  shift1 = [-1.*s for s in dcr[1]]
614  else:
615  shift = dcr[0]
616  shift1 = dcr[1]
617  shiftedImage = ndimage.interpolation.shift(image, shift, **kwargs)
618  shiftedImage += ndimage.interpolation.shift(image, shift1, **kwargs)
619  shiftedImage /= 2.
620  else:
621  if useInverse:
622  shift = [-1.*s for s in dcr]
623  else:
624  shift = dcr
625  shiftedImage = ndimage.interpolation.shift(image, shift, **kwargs)
626  return shiftedImage
627 
628 
629 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
630  """Calculate the shift in pixels of an exposure due to DCR.
631 
632  Parameters
633  ----------
634  visitInfo : `lsst.afw.image.VisitInfo`
635  Metadata for the exposure.
636  wcs : `lsst.afw.geom.SkyWcs`
637  Coordinate system definition (wcs) for the exposure.
638  filterInfo : `lsst.afw.image.Filter`
639  The filter definition, set in the current instruments' obs package.
640  dcrNumSubfilters : `int`
641  Number of sub-filters used to model chromatic effects within a band.
642  splitSubfilters : `bool`, optional
643  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
644  instead of at the midpoint. Default: False
645 
646  Returns
647  -------
648  dcrShift : `tuple` of two `float`
649  The 2D shift due to DCR, in pixels.
650  Uses numpy axes ordering (Y, X).
651  """
652  rotation = calculateImageParallacticAngle(visitInfo, wcs)
653  dcrShift = []
654  weight = [0.75, 0.25]
655  lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
656  for wl0, wl1 in wavelengthGenerator(filterInfo, dcrNumSubfilters):
657  # Note that diffRefractAmp can be negative, since it's relative to the midpoint of the full band
658  diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=lambdaEff,
659  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
660  observatory=visitInfo.getObservatory(),
661  weather=visitInfo.getWeather())
662  diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=lambdaEff,
663  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
664  observatory=visitInfo.getObservatory(),
665  weather=visitInfo.getWeather())
666  if splitSubfilters:
667  diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
668  diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
669  diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
670  diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
671  shiftX = [diffRefractPix*np.sin(rotation.asRadians()) for diffRefractPix in diffRefractArr]
672  shiftY = [diffRefractPix*np.cos(rotation.asRadians()) for diffRefractPix in diffRefractArr]
673  dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1])))
674  else:
675  diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
676  diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
677  shiftX = diffRefractPix*np.sin(rotation.asRadians())
678  shiftY = diffRefractPix*np.cos(rotation.asRadians())
679  dcrShift.append((shiftY, shiftX))
680  return dcrShift
681 
682 
683 def calculateImageParallacticAngle(visitInfo, wcs):
684  """Calculate the total sky rotation angle of an exposure.
685 
686  Parameters
687  ----------
688  visitInfo : `lsst.afw.image.VisitInfo`
689  Metadata for the exposure.
690  wcs : `lsst.afw.geom.SkyWcs`
691  Coordinate system definition (wcs) for the exposure.
692 
693  Returns
694  -------
695  `lsst.geom.Angle`
696  The rotation of the image axis, East from North.
697  Equal to the parallactic angle plus any additional rotation of the
698  coordinate system.
699  A rotation angle of 0 degrees is defined with
700  North along the +y axis and East along the +x axis.
701  A rotation angle of 90 degrees is defined with
702  North along the +x axis and East along the -y axis.
703  """
704  parAngle = visitInfo.getBoresightParAngle().asRadians()
705  cd = wcs.getCdMatrix()
706  if wcs.isFlipped:
707  cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
708  else:
709  cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
710  rotAngle = (cdAngle + parAngle)*radians
711  return rotAngle
712 
713 
714 def wavelengthGenerator(filterInfo, dcrNumSubfilters):
715  """Iterate over the wavelength endpoints of subfilters.
716 
717  Parameters
718  ----------
719  filterInfo : `lsst.afw.image.Filter`
720  The filter definition, set in the current instruments' obs package.
721  dcrNumSubfilters : `int`
722  Number of sub-filters used to model chromatic effects within a band.
723 
724  Yields
725  ------
726  `tuple` of two `float`
727  The next set of wavelength endpoints for a subfilter, in nm.
728  """
729  lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
730  lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
731  wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
732  for wl in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=False):
733  yield (wl, wl + wlStep)
def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None)
Definition: dcrModel.py:62
def __setitem__(self, subfilter, maskedImage)
Definition: dcrModel.py:182
def calculateImageParallacticAngle(visitInfo, wcs)
Definition: dcrModel.py:683
def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, regularizationWidth=2)
Definition: dcrModel.py:416
def buildMatchedTemplate(self, exposure=None, order=3, visitInfo=None, bbox=None, wcs=None, mask=None, splitSubfilters=True, amplifyModel=1.)
Definition: dcrModel.py:302
def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, kwargs)
Definition: dcrModel.py:582
def __getitem__(self, subfilter)
Definition: dcrModel.py:157
def calculateNoiseCutoff(self, image, statsCtrl, bufferSize, convergenceMaskPlanes="DETECTED", mask=None, bbox=None)
Definition: dcrModel.py:504
def __init__(self, modelImages, filterInfo=None, psf=None, mask=None, variance=None)
Definition: dcrModel.py:53
def wavelengthGenerator(filterInfo, dcrNumSubfilters)
Definition: dcrModel.py:714
def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2)
Definition: dcrModel.py:540
def buildMatchedExposure(self, exposure=None, visitInfo=None, bbox=None, wcs=None, mask=None)
Definition: dcrModel.py:358
def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, kwargs)
Definition: dcrModel.py:106
def getReferenceImage(self, bbox=None)
Definition: dcrModel.py:261
def differentialRefraction(wavelength, wavelengthRef, elevation, observatory, weather=None)
Definition: refraction.py:95
def assign(self, dcrSubModel, bbox=None)
Definition: dcrModel.py:277
def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False)
Definition: dcrModel.py:629
def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor, regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED")
Definition: dcrModel.py:442
def conditionDcrModel(self, modelImages, bbox, gain=1.)
Definition: dcrModel.py:395
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
An integer coordinate rectangle.
Definition: Box.h:54