lsst.ip.diffim  17.0.1-3-gc20ba7d+2
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=False):
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  visitInfo : `lsst.afw.image.VisitInfo`, optional
311  Metadata for the exposure. Ignored if ``exposure`` is set.
312  bbox : `lsst.afw.geom.Box2I`, optional
313  Sub-region of the coadd. Ignored if ``exposure`` is set.
314  wcs : `lsst.afw.geom.SkyWcs`, optional
315  Coordinate system definition (wcs) for the exposure.
316  Ignored if ``exposure`` is set.
317  mask : `lsst.afw.image.Mask`, optional
318  reference mask to use for the template image.
319  splitSubfilters : `bool`, optional
320  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
321  instead of at the midpoint. Default: False
322 
323  Returns
324  -------
325  templateImage : `lsst.afw.image.ImageF`
326  The DCR-matched template
327 
328  Raises
329  ------
330  ValueError
331  If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set.
332  """
333  if self.filter is None:
334  raise ValueError("'filterInfo' must be set for the DcrModel in order to calculate DCR.")
335  if exposure is not None:
336  visitInfo = exposure.getInfo().getVisitInfo()
337  bbox = exposure.getBBox()
338  wcs = exposure.getInfo().getWcs()
339  elif visitInfo is None or bbox is None or wcs is None:
340  raise ValueError("Either exposure or visitInfo, bbox, and wcs must be set.")
341  dcrShift = calculateDcr(visitInfo, wcs, self.filter, len(self), splitSubfilters=splitSubfilters)
342  templateImage = afwImage.ImageF(bbox)
343  for subfilter, dcr in enumerate(dcrShift):
344  templateImage.array += applyDcr(self[subfilter][bbox].array, dcr,
345  splitSubfilters=splitSubfilters, order=order)
346  return templateImage
347 
348  def buildMatchedExposure(self, exposure=None,
349  visitInfo=None, bbox=None, wcs=None, mask=None):
350  """Wrapper to create an exposure from a template image.
351 
352  Parameters
353  ----------
354  exposure : `lsst.afw.image.Exposure`, optional
355  The input exposure to build a matched template for.
356  May be omitted if all of the metadata is supplied separately
357  visitInfo : `lsst.afw.image.VisitInfo`, optional
358  Metadata for the exposure. Ignored if ``exposure`` is set.
359  bbox : `lsst.afw.geom.Box2I`, optional
360  Sub-region of the coadd. Ignored if ``exposure`` is set.
361  wcs : `lsst.afw.geom.SkyWcs`, optional
362  Coordinate system definition (wcs) for the exposure.
363  Ignored if ``exposure`` is set.
364  mask : `lsst.afw.image.Mask`, optional
365  reference mask to use for the template image.
366 
367  Returns
368  -------
369  templateExposure : `lsst.afw.image.exposureF`
370  The DCR-matched template
371  """
372  templateImage = self.buildMatchedTemplate(exposure, visitInfo, bbox, wcs, mask)
373  maskedImage = afwImage.MaskedImageF(bbox)
374  maskedImage.image = templateImage
375  maskedImage.mask = self.mask
376  maskedImage.variance = self.variance
377  templateExposure = afwImage.ExposureF(bbox, wcs)
378  templateExposure.setMaskedImage(templateImage)
379  templateExposure.setPsf(self.psf)
380  templateExposure.setFilter(self.filter)
381  return templateExposure
382 
383  def conditionDcrModel(self, modelImages, bbox, gain=1.):
384  """Average two iterations' solutions to reduce oscillations.
385 
386  Parameters
387  ----------
388  modelImages : `list` of `lsst.afw.image.Image`
389  The new DCR model images from the current iteration.
390  The values will be modified in place.
391  bbox : `lsst.afw.geom.Box2I`
392  Sub-region of the coadd
393  gain : `float`, optional
394  Relative weight to give the new solution when updating the model.
395  Defaults to 1.0, which gives equal weight to both solutions.
396  """
397  # Calculate weighted averages of the images.
398  for model, newModel in zip(self, modelImages):
399  newModel *= gain
400  newModel += model[bbox]
401  newModel /= 1. + gain
402 
403  def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor,
404  regularizationWidth=2):
405  """Restrict large variations in the model between iterations.
406 
407  Parameters
408  ----------
409  subfilter : `int`
410  Index of the current subfilter within the full band.
411  newModel : `lsst.afw.image.Image`
412  The new DCR model for one subfilter from the current iteration.
413  Values in ``newModel`` that are extreme compared with the last
414  iteration are modified in place.
415  bbox : `lsst.afw.geom.Box2I`
416  Sub-region to coadd
417  regularizationFactor : `float`
418  Maximum relative change of the model allowed between iterations.
419  regularizationWidth : int, optional
420  Minimum radius of a region to include in regularization, in pixels.
421  """
422  refImage = self[subfilter][bbox].array
423  highThreshold = np.abs(refImage)*regularizationFactor
424  lowThreshold = refImage/regularizationFactor
425  newImage = newModel.array
426  self.applyImageThresholds(newImage, highThreshold=highThreshold, lowThreshold=lowThreshold,
427  regularizationWidth=regularizationWidth)
428 
429  def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor,
430  regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
431  """Restrict large variations in the model between subfilters.
432 
433  Parameters
434  ----------
435  modelImages : `list` of `lsst.afw.image.Image`
436  The new DCR model images from the current iteration.
437  The values will be modified in place.
438  bbox : `lsst.afw.geom.Box2I`
439  Sub-region to coadd
440  statsCtrl : `lsst.afw.math.StatisticsControl`
441  Statistics control object for coaddition.
442  regularizationFactor : `float`
443  Maximum relative change of the model allowed between subfilters.
444  regularizationWidth : `int`, optional
445  Minimum radius of a region to include in regularization, in pixels.
446  mask : `lsst.afw.image.Mask`, optional
447  Optional alternate mask
448  convergenceMaskPlanes : `list` of `str`, or `str`, optional
449  Mask planes to use to calculate convergence.
450 
451  Notes
452  -----
453  This implementation of frequency regularization restricts each subfilter
454  image to be a smoothly-varying function times a reference image.
455  """
456  # ``regularizationFactor`` is the maximum change between subfilter images, so the maximum difference
457  # between one subfilter image and the average will be the square root of that.
458  maxDiff = np.sqrt(regularizationFactor)
459  noiseLevel = self.calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
460  referenceImage = self.getReferenceImage(bbox)
461  badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
462  if np.sum(~badPixels) == 0:
463  # Skip regularization if there are no valid pixels
464  return
465  referenceImage[badPixels] = 0.
466  filterWidth = regularizationWidth
467  fwhm = 2.*filterWidth
468  # The noise should be lower in the smoothed image by sqrt(Nsmooth) ~ fwhm pixels
469  noiseLevel /= fwhm
470  smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth) + noiseLevel
471 
472  baseThresh = np.ones_like(referenceImage)
473  highThreshold = baseThresh*maxDiff
474  lowThreshold = baseThresh/maxDiff
475  for subfilter, model in enumerate(modelImages):
476  smoothModel = ndimage.filters.gaussian_filter(model.array, filterWidth) + noiseLevel
477  relativeModel = smoothModel/smoothRef
478  # Now sharpen the smoothed relativeModel using an alpha of 3.
479  relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/3.)
480  relativeModel = relativeModel + 3.*(relativeModel - relativeModel2)
481  self.applyImageThresholds(relativeModel,
482  highThreshold=highThreshold,
483  lowThreshold=lowThreshold,
484  regularizationWidth=regularizationWidth)
485  relativeModel *= referenceImage
486  modelImages[subfilter].array = relativeModel
487 
488  def calculateNoiseCutoff(self, image, statsCtrl, bufferSize,
489  convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
490  """Helper function to calculate the background noise level of an image.
491 
492  Parameters
493  ----------
494  image : `lsst.afw.image.Image`
495  The input image to evaluate the background noise properties.
496  statsCtrl : `lsst.afw.math.StatisticsControl`
497  Statistics control object for coaddition.
498  bufferSize : `int`
499  Number of additional pixels to exclude
500  from the edges of the bounding box.
501  convergenceMaskPlanes : `list` of `str`, or `str`
502  Mask planes to use to calculate convergence.
503  mask : `lsst.afw.image.Mask`, Optional
504  Optional alternate mask
505  bbox : `lsst.afw.geom.Box2I`, optional
506  Sub-region of the masked image to calculate the noise level over.
507 
508  Returns
509  -------
510  noiseCutoff : `float`
511  The threshold value to treat pixels as noise in an image..
512  """
513  if bbox is None:
514  bbox = self.bbox
515  if mask is None:
516  mask = self.mask[bbox]
517  bboxShrink = afwGeom.Box2I(bbox)
518  bboxShrink.grow(-bufferSize)
519  convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
520 
521  backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
522  noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
523  return noiseCutoff
524 
525  def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2):
526  """Restrict image values to be between upper and lower limits.
527 
528  This method flags all pixels in an image that are outside of the given
529  threshold values. The threshold values are taken from a reference image,
530  so noisy pixels are likely to get flagged. In order to exclude those
531  noisy pixels, the array of flags is eroded and dilated, which removes
532  isolated pixels outside of the thresholds from the list of pixels to be
533  modified. Pixels that remain flagged after this operation have their
534  values set to the appropriate upper or lower threshold value.
535 
536  Parameters
537  ----------
538  image : `numpy.ndarray`
539  The image to apply the thresholds to.
540  The values will be modified in place.
541  highThreshold : `numpy.ndarray`, optional
542  Array of upper limit values for each pixel of ``image``.
543  lowThreshold : `numpy.ndarray`, optional
544  Array of lower limit values for each pixel of ``image``.
545  regularizationWidth : `int`, optional
546  Minimum radius of a region to include in regularization, in pixels.
547  """
548  # Generate the structure for binary erosion and dilation, which is used to remove noise-like pixels.
549  # Groups of pixels with a radius smaller than ``regularizationWidth``
550  # will be excluded from regularization.
551  filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
552  regularizationWidth)
553  if highThreshold is not None:
554  highPixels = image > highThreshold
555  if regularizationWidth > 0:
556  # Erode and dilate ``highPixels`` to exclude noisy pixels.
557  highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
558  image[highPixels] = highThreshold[highPixels]
559  if lowThreshold is not None:
560  lowPixels = image < lowThreshold
561  if regularizationWidth > 0:
562  # Erode and dilate ``lowPixels`` to exclude noisy pixels.
563  lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
564  image[lowPixels] = lowThreshold[lowPixels]
565 
566 
567 def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, **kwargs):
568  """Shift an image along the X and Y directions.
569 
570  Parameters
571  ----------
572  image : `numpy.ndarray`
573  The input image to shift.
574  dcr : `tuple`
575  Shift calculated with ``calculateDcr``.
576  Uses numpy axes ordering (Y, X).
577  If ``splitSubfilters`` is set, each element is itself a `tuple`
578  of two `float`, corresponding to the DCR shift at the two wavelengths.
579  Otherwise, each element is a `float` corresponding to the DCR shift at
580  the effective wavelength of the subfilter.
581  useInverse : `bool`, optional
582  Apply the shift in the opposite direction. Default: False
583  splitSubfilters : `bool`, optional
584  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
585  instead of at the midpoint. Default: False
586  kwargs
587  Additional keyword parameters to pass in to
588  `scipy.ndimage.interpolation.shift`
589 
590  Returns
591  -------
592  shiftedImage : `numpy.ndarray`
593  A copy of the input image with the specified shift applied.
594  """
595  if splitSubfilters:
596  if useInverse:
597  shift = [-1.*s for s in dcr[0]]
598  shift1 = [-1.*s for s in dcr[1]]
599  else:
600  shift = dcr[0]
601  shift1 = dcr[1]
602  shiftedImage = ndimage.interpolation.shift(image, shift, **kwargs)
603  shiftedImage += ndimage.interpolation.shift(image, shift1, **kwargs)
604  shiftedImage /= 2.
605  else:
606  if useInverse:
607  shift = [-1.*s for s in dcr]
608  else:
609  shift = dcr
610  shiftedImage = ndimage.interpolation.shift(image, shift, **kwargs)
611  return shiftedImage
612 
613 
614 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
615  """Calculate the shift in pixels of an exposure due to DCR.
616 
617  Parameters
618  ----------
619  visitInfo : `lsst.afw.image.VisitInfo`
620  Metadata for the exposure.
621  wcs : `lsst.afw.geom.SkyWcs`
622  Coordinate system definition (wcs) for the exposure.
623  filterInfo : `lsst.afw.image.Filter`
624  The filter definition, set in the current instruments' obs package.
625  dcrNumSubfilters : `int`
626  Number of sub-filters used to model chromatic effects within a band.
627  splitSubfilters : `bool`, optional
628  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
629  instead of at the midpoint. Default: False
630 
631  Returns
632  -------
633  dcrShift : `tuple` of two `float`
634  The 2D shift due to DCR, in pixels.
635  Uses numpy axes ordering (Y, X).
636  """
637  rotation = calculateImageParallacticAngle(visitInfo, wcs)
638  dcrShift = []
639  weight = [0.75, 0.25]
640  lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
641  for wl0, wl1 in wavelengthGenerator(filterInfo, dcrNumSubfilters):
642  # Note that diffRefractAmp can be negative, since it's relative to the midpoint of the full band
643  diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=lambdaEff,
644  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
645  observatory=visitInfo.getObservatory(),
646  weather=visitInfo.getWeather())
647  diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=lambdaEff,
648  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
649  observatory=visitInfo.getObservatory(),
650  weather=visitInfo.getWeather())
651  if splitSubfilters:
652  diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
653  diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
654  diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
655  diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
656  shiftX = [diffRefractPix*np.sin(rotation.asRadians()) for diffRefractPix in diffRefractArr]
657  shiftY = [diffRefractPix*np.cos(rotation.asRadians()) for diffRefractPix in diffRefractArr]
658  dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1])))
659  else:
660  diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
661  diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
662  shiftX = diffRefractPix*np.sin(rotation.asRadians())
663  shiftY = diffRefractPix*np.cos(rotation.asRadians())
664  dcrShift.append((shiftY, shiftX))
665  return dcrShift
666 
667 
668 def calculateImageParallacticAngle(visitInfo, wcs):
669  """Calculate the total sky rotation angle of an exposure.
670 
671  Parameters
672  ----------
673  visitInfo : `lsst.afw.image.VisitInfo`
674  Metadata for the exposure.
675  wcs : `lsst.afw.geom.SkyWcs`
676  Coordinate system definition (wcs) for the exposure.
677 
678  Returns
679  -------
680  `lsst.geom.Angle`
681  The rotation of the image axis, East from North.
682  Equal to the parallactic angle plus any additional rotation of the
683  coordinate system.
684  A rotation angle of 0 degrees is defined with
685  North along the +y axis and East along the +x axis.
686  A rotation angle of 90 degrees is defined with
687  North along the +x axis and East along the -y axis.
688  """
689  parAngle = visitInfo.getBoresightParAngle().asRadians()
690  cd = wcs.getCdMatrix()
691  if wcs.isFlipped:
692  cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
693  else:
694  cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
695  rotAngle = (cdAngle + parAngle)*radians
696  return rotAngle
697 
698 
699 def wavelengthGenerator(filterInfo, dcrNumSubfilters):
700  """Iterate over the wavelength endpoints of subfilters.
701 
702  Parameters
703  ----------
704  filterInfo : `lsst.afw.image.Filter`
705  The filter definition, set in the current instruments' obs package.
706  dcrNumSubfilters : `int`
707  Number of sub-filters used to model chromatic effects within a band.
708 
709  Yields
710  ------
711  `tuple` of two `float`
712  The next set of wavelength endpoints for a subfilter, in nm.
713  """
714  lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
715  lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
716  wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
717  for wl in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=False):
718  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:668
def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, regularizationWidth=2)
Definition: dcrModel.py:404
def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, kwargs)
Definition: dcrModel.py:567
def __getitem__(self, subfilter)
Definition: dcrModel.py:157
def calculateNoiseCutoff(self, image, statsCtrl, bufferSize, convergenceMaskPlanes="DETECTED", mask=None, bbox=None)
Definition: dcrModel.py:489
def __init__(self, modelImages, filterInfo=None, psf=None, mask=None, variance=None)
Definition: dcrModel.py:53
def wavelengthGenerator(filterInfo, dcrNumSubfilters)
Definition: dcrModel.py:699
def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2)
Definition: dcrModel.py:525
def buildMatchedExposure(self, exposure=None, visitInfo=None, bbox=None, wcs=None, mask=None)
Definition: dcrModel.py:349
def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, kwargs)
Definition: dcrModel.py:106
def getReferenceImage(self, bbox=None)
Definition: dcrModel.py:261
def assign(self, dcrSubModel, bbox=None)
Definition: dcrModel.py:277
def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False)
Definition: dcrModel.py:614
def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor, regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED")
Definition: dcrModel.py:430
def conditionDcrModel(self, modelImages, bbox, gain=1.)
Definition: dcrModel.py:383
def buildMatchedTemplate(self, exposure=None, order=3, visitInfo=None, bbox=None, wcs=None, mask=None, splitSubfilters=False)
Definition: dcrModel.py:302