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