lsst.ip.diffim  16.0-18-geff7d6b+4
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 from lsst.afw.geom import AffineTransform
28 from lsst.afw.geom import makeTransform
29 import lsst.afw.image as afwImage
30 import lsst.afw.math as afwMath
31 from lsst.geom import radians
32 
33 __all__ = ["DcrModel", "applyDcr", "calculateDcr", "calculateImageParallacticAngle"]
34 
35 
36 class DcrModel:
37  """A model of the true sky after correcting chromatic effects.
38 
39  Attributes
40  ----------
41  dcrNumSubfilters : `int`
42  Number of sub-filters used to model chromatic effects within a band.
43  filterInfo : `lsst.afw.image.Filter`
44  The filter definition, set in the current instruments' obs package.
45  modelImages : `list` of `lsst.afw.image.MaskedImage`
46  A list of masked images, each containing the model for one subfilter
47 
48  Parameters
49  ----------
50  modelImages : `list` of `lsst.afw.image.MaskedImage`
51  A list of masked images, each containing the model for one subfilter.
52  filterInfo : `lsst.afw.image.Filter`, optional
53  The filter definition, set in the current instruments' obs package.
54  Required for any calculation of DCR, including making matched templates.
55 
56  Notes
57  -----
58  The ``DcrModel`` contains an estimate of the true sky, at a higher
59  wavelength resolution than the input observations. It can be forward-
60  modeled to produce Differential Chromatic Refraction (DCR) matched
61  templates for a given ``Exposure``, and provides utilities for conditioning
62  the model in ``dcrAssembleCoadd`` to avoid oscillating solutions between
63  iterations of forward modeling or between the subfilters of the model.
64  """
65 
66  def __init__(self, modelImages, filterInfo=None, psf=None):
67  self.dcrNumSubfilters = len(modelImages)
68  self.modelImages = modelImages
69  self._filter = filterInfo
70  self._psf = psf
71 
72  @classmethod
73  def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None):
74  """Initialize a DcrModel by dividing a coadd between the subfilters.
75 
76  Parameters
77  ----------
78  maskedImage : `lsst.afw.image.MaskedImage`
79  Input coadded image to divide equally between the subfilters.
80  dcrNumSubfilters : `int`
81  Number of sub-filters used to model chromatic effects within a band.
82  filterInfo : `lsst.afw.image.Filter`, optional
83  The filter definition, set in the current instruments' obs package.
84  Required for any calculation of DCR, including making matched templates.
85  psf : `lsst.afw.detection.Psf`, optional
86  Point spread function (PSF) of the model.
87  Required if the ``DcrModel`` will be persisted.
88 
89  Returns
90  -------
91  dcrModel : `lsst.pipe.tasks.DcrModel`
92  Best fit model of the true sky after correcting chromatic effects.
93  """
94  # NANs will potentially contaminate the entire image,
95  # depending on the shift or convolution type used.
96  model = maskedImage.clone()
97  badPixels = np.isnan(model.image.array) | np.isnan(model.variance.array)
98  model.image.array[badPixels] = 0.
99  model.variance.array[badPixels] = 0.
100  model.image.array /= dcrNumSubfilters
101  # We divide the variance by N and not N**2 because we will assume each
102  # subfilter is independent. That means that the significance of
103  # detected sources will be lower by a factor of sqrt(N) in the
104  # subfilter images, but we will recover it when we combine the
105  # subfilter images to construct matched templates.
106  model.variance.array /= dcrNumSubfilters
107  model.mask.array[badPixels] = model.mask.getPlaneBitMask("NO_DATA")
108  modelImages = [model, ]
109  for subfilter in range(1, dcrNumSubfilters):
110  modelImages.append(model.clone())
111  return cls(modelImages, filterInfo, psf)
112 
113  @classmethod
114  def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, **kwargs):
115  """Load an existing DcrModel from a repository.
116 
117  Parameters
118  ----------
119  dataRef : `lsst.daf.persistence.ButlerDataRef`
120  Data reference defining the patch for coaddition and the
121  reference Warp
122  datasetType : `str`, optional
123  Name of the DcrModel in the registry {"dcrCoadd", "dcrCoadd_sub"}
124  numSubfilters : `int`
125  Number of sub-filters used to model chromatic effects within a band.
126  **kwargs
127  Additional keyword arguments to pass to look up the model in the data registry.
128  Common keywords and their types include: ``tract``:`str`, ``patch``:`str`,
129  ``bbox``:`lsst.afw.geom.Box2I`
130 
131  Returns
132  -------
133  dcrModel : `lsst.pipe.tasks.DcrModel`
134  Best fit model of the true sky after correcting chromatic effects.
135  """
136  modelImages = []
137  filterInfo = None
138  psf = None
139  for subfilter in range(numSubfilters):
140  dcrCoadd = dataRef.get(datasetType, subfilter=subfilter,
141  numSubfilters=numSubfilters, **kwargs)
142  if filterInfo is None:
143  filterInfo = dcrCoadd.getFilter()
144  if psf is None:
145  psf = dcrCoadd.getPsf()
146  modelImages.append(dcrCoadd.maskedImage)
147  return cls(modelImages, filterInfo, psf)
148 
149  def __len__(self):
150  """Return the number of subfilters.
151 
152  Returns
153  -------
154  dcrNumSubfilters : `int`
155  The number of DCR subfilters in the model.
156  """
157  return self.dcrNumSubfilters
158 
159  def __getitem__(self, subfilter):
160  """Iterate over the subfilters of the DCR model.
161 
162  Parameters
163  ----------
164  subfilter : `int`
165  Index of the current ``subfilter`` within the full band.
166  Negative indices are allowed, and count in reverse order
167  from the highest ``subfilter``.
168 
169  Returns
170  -------
171  modelImage : `lsst.afw.image.MaskedImage`
172  The DCR model for the given ``subfilter``.
173 
174  Raises
175  ------
176  IndexError
177  If the requested ``subfilter`` is greater or equal to the number
178  of subfilters in the model.
179  """
180  if np.abs(subfilter) >= len(self):
181  raise IndexError("subfilter out of bounds.")
182  return self.modelImages[subfilter]
183 
184  def __setitem__(self, subfilter, maskedImage):
185  """Update the model image for one subfilter.
186 
187  Parameters
188  ----------
189  subfilter : `int`
190  Index of the current subfilter within the full band.
191  maskedImage : `lsst.afw.image.MaskedImage`
192  The DCR model to set for the given ``subfilter``.
193 
194  Raises
195  ------
196  IndexError
197  If the requested ``subfilter`` is greater or equal to the number
198  of subfilters in the model.
199  ValueError
200  If the bounding box of the new image does not match.
201  """
202  if np.abs(subfilter) >= len(self):
203  raise IndexError("subfilter out of bounds.")
204  if maskedImage.getBBox() != self.bbox:
205  raise ValueError("The bounding box of a subfilter must not change.")
206  self.modelImages[subfilter] = maskedImage
207 
208  @property
209  def filter(self):
210  """Return the filter of the model.
211 
212  Returns
213  -------
214  filter : `lsst.afw.image.Filter`
215  The filter definition, set in the current instruments' obs package.
216  """
217  return self._filter
218 
219  @property
220  def psf(self):
221  """Return the psf of the model.
222 
223  Returns
224  -------
225  psf : `lsst.afw.detection.Psf`
226  Point spread function (PSF) of the model.
227  """
228  return self._psf
229 
230  @property
231  def bbox(self):
232  """Return the common bounding box of each subfilter image.
233 
234  Returns
235  -------
236  bbox : `lsst.afw.geom.Box2I`
237  Bounding box of the DCR model.
238  """
239  return self[0].getBBox()
240 
241  @property
242  def mask(self):
243  """Return the common mask of each subfilter image.
244 
245  Returns
246  -------
247  bbox : `lsst.afw.image.Mask`
248  Mask plane of the DCR model.
249  """
250  return self[0].mask
251 
252  def getReferenceImage(self, bbox=None):
253  """Create a simple template from the DCR model.
254 
255  Parameters
256  ----------
257  bbox : `lsst.afw.geom.Box2I`, optional
258  Sub-region of the coadd. Returns the entire image if `None`.
259 
260  Returns
261  -------
262  templateImage : `numpy.ndarray`
263  The template with no chromatic effects applied.
264  """
265  bbox = bbox or self.bbox
266  return np.mean([model[bbox].image.array for model in self], axis=0)
267 
268  def assign(self, dcrSubModel, bbox=None):
269  """Update a sub-region of the ``DcrModel`` with new values.
270 
271  Parameters
272  ----------
273  dcrSubModel : `lsst.pipe.tasks.DcrModel`
274  New model of the true scene after correcting chromatic effects.
275  bbox : `lsst.afw.geom.Box2I`, optional
276  Sub-region of the coadd.
277  Defaults to the bounding box of ``dcrSubModel``.
278 
279  Raises
280  ------
281  ValueError
282  If the new model has a different number of subfilters.
283  """
284  if len(dcrSubModel) != len(self):
285  raise ValueError("The number of DCR subfilters must be the same "
286  "between the old and new models.")
287  bbox = bbox or self.bbox
288  for model, subModel in zip(self, dcrSubModel):
289  model.assign(subModel[bbox], bbox)
290 
291  def buildMatchedTemplate(self, exposure=None, warpCtrl=None,
292  visitInfo=None, bbox=None, wcs=None, mask=None,
293  splitSubfilters=False):
294  """Create a DCR-matched template image for an exposure.
295 
296  Parameters
297  ----------
298  exposure : `lsst.afw.image.Exposure`, optional
299  The input exposure to build a matched template for.
300  May be omitted if all of the metadata is supplied separately
301  warpCtrl : `lsst.afw.Math.WarpingControl`, optional
302  Configuration settings for warping an image.
303  If not set, defaults to a lanczos3 warping kernel for the image,
304  and a bilinear kernel for the mask
305  visitInfo : `lsst.afw.image.VisitInfo`, optional
306  Metadata for the exposure. Ignored if ``exposure`` is set.
307  bbox : `lsst.afw.geom.Box2I`, optional
308  Sub-region of the coadd. Ignored if ``exposure`` is set.
309  wcs : `lsst.afw.geom.SkyWcs`, optional
310  Coordinate system definition (wcs) for the exposure.
311  Ignored if ``exposure`` is set.
312  mask : `lsst.afw.image.Mask`, optional
313  reference mask to use for the template image.
314  splitSubfilters : `bool`, optional
315  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
316  instead of at the midpoint. Default: False
317 
318  Returns
319  -------
320  templateImage : `lsst.afw.image.maskedImageF`
321  The DCR-matched template
322 
323  Raises
324  ------
325  ValueError
326  If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set.
327  """
328  if self.filter is None:
329  raise ValueError("'filterInfo' must be set for the DcrModel in order to calculate DCR.")
330  if exposure is not None:
331  visitInfo = exposure.getInfo().getVisitInfo()
332  bbox = exposure.getBBox()
333  wcs = exposure.getInfo().getWcs()
334  elif visitInfo is None or bbox is None or wcs is None:
335  raise ValueError("Either exposure or visitInfo, bbox, and wcs must be set.")
336  if warpCtrl is None:
337  # Turn off the warping cache, since we set the linear interpolation length to the entire subregion
338  # This warper is only used for applying DCR shifts, which are assumed to be uniform across a patch
339  warpCtrl = afwMath.WarpingControl("lanczos3", "bilinear",
340  cacheSize=0, interpLength=max(bbox.getDimensions()))
341 
342  dcrShift = calculateDcr(visitInfo, wcs, self.filter, len(self), splitSubfilters=splitSubfilters)
343  templateImage = afwImage.MaskedImageF(bbox)
344  for subfilter, dcr in enumerate(dcrShift):
345  templateImage += applyDcr(self[subfilter][bbox], dcr, warpCtrl, splitSubfilters=splitSubfilters)
346  if mask is not None:
347  templateImage.setMask(mask[bbox])
348  return templateImage
349 
350  def buildMatchedExposure(self, exposure=None, warpCtrl=None,
351  visitInfo=None, bbox=None, wcs=None, mask=None):
352  """Wrapper to create an exposure from a template image.
353 
354  Parameters
355  ----------
356  exposure : `lsst.afw.image.Exposure`, optional
357  The input exposure to build a matched template for.
358  May be omitted if all of the metadata is supplied separately
359  warpCtrl : `lsst.afw.Math.WarpingControl`
360  Configuration settings for warping an image
361  visitInfo : `lsst.afw.image.VisitInfo`, optional
362  Metadata for the exposure. Ignored if ``exposure`` is set.
363  bbox : `lsst.afw.geom.Box2I`, optional
364  Sub-region of the coadd. Ignored if ``exposure`` is set.
365  wcs : `lsst.afw.geom.SkyWcs`, optional
366  Coordinate system definition (wcs) for the exposure.
367  Ignored if ``exposure`` is set.
368  mask : `lsst.afw.image.Mask`, optional
369  reference mask to use for the template image.
370 
371  Returns
372  -------
373  templateExposure : `lsst.afw.image.exposureF`
374  The DCR-matched template
375  """
376  templateImage = self.buildMatchedTemplate(exposure, warpCtrl, visitInfo, bbox, wcs, mask)
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.MaskedImage`
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 image and variance planes.
398  # Note that ``newModel *= gain`` would multiply the variance by ``gain**2``
399  for model, newModel in zip(self, modelImages):
400  newModel.image *= gain
401  newModel.image += model[bbox].image
402  newModel.image /= 1. + gain
403  newModel.variance *= gain
404  newModel.variance += model[bbox].variance
405  newModel.variance /= 1. + gain
406 
407  def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor,
408  regularizationWidth=2):
409  """Restrict large variations in the model between iterations.
410 
411  Parameters
412  ----------
413  subfilter : `int`
414  Index of the current subfilter within the full band.
415  newModel : `lsst.afw.image.MaskedImage`
416  The new DCR model for one subfilter from the current iteration.
417  Values in ``newModel`` that are extreme compared with the last
418  iteration are modified in place.
419  bbox : `lsst.afw.geom.Box2I`
420  Sub-region to coadd
421  regularizationFactor : `float`
422  Maximum relative change of the model allowed between iterations.
423  regularizationWidth : int, optional
424  Minimum radius of a region to include in regularization, in pixels.
425  """
426  refImage = self[subfilter][bbox].image.array
427  highThreshold = np.abs(refImage)*regularizationFactor
428  lowThreshold = refImage/regularizationFactor
429  self.applyImageThresholds(newModel, highThreshold=highThreshold, lowThreshold=lowThreshold,
430  regularizationWidth=regularizationWidth)
431 
432  def regularizeModelFreq(self, modelImages, bbox, regularizationFactor,
433  regularizationWidth=2):
434  """Restrict large variations in the model between subfilters.
435 
436  Parameters
437  ----------
438  modelImages : `list` of `lsst.afw.image.MaskedImage`
439  The new DCR model images from the current iteration.
440  The values will be modified in place.
441  bbox : `lsst.afw.geom.Box2I`
442  Sub-region to coadd
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  """
448  # ``regularizationFactor`` is the maximum change between subfilter images, so the maximum difference
449  # between one subfilter image and the average will be the square root of that.
450  maxDiff = np.sqrt(regularizationFactor)
451  refImage = self.getReferenceImage(bbox)
452 
453  for model in modelImages:
454  highThreshold = np.abs(refImage)*maxDiff
455  lowThreshold = refImage/maxDiff
456  self.applyImageThresholds(model[bbox], highThreshold=highThreshold, lowThreshold=lowThreshold,
457  regularizationWidth=regularizationWidth)
458 
459  def calculateNoiseCutoff(self, maskedImage, statsCtrl, bufferSize,
460  convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
461  """Helper function to calculate the background noise level of an image.
462 
463  Parameters
464  ----------
465  maskedImage : `lsst.afw.image.MaskedImage`
466  The input image to evaluate the background noise properties.
467  statsCtrl : `lsst.afw.math.StatisticsControl`
468  Statistics control object for coaddition.
469  bufferSize : `int`
470  Number of additional pixels to exclude
471  from the edges of the bounding box.
472  convergenceMaskPlanes : `list` of `str`, or `str`
473  Mask planes to use to calculate convergence.
474  mask : `lsst.afw.image.Mask`, Optional
475  Optional alternate mask
476  bbox : `lsst.afw.geom.Box2I`, optional
477  Sub-region of the masked image to calculate the noise level over.
478 
479  Returns
480  -------
481  noiseCutoff : `float`
482  The threshold value to treat pixels as noise in an image..
483  """
484  if bbox is None:
485  bbox = self.bbox
486  if mask is None:
487  mask = maskedImage[bbox].mask
488  bboxShrink = afwGeom.Box2I(bbox)
489  bboxShrink.grow(-bufferSize)
490  convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
491 
492  backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
493  noiseCutoff = np.std(maskedImage[bboxShrink].image.array[backgroundPixels])
494  return noiseCutoff
495 
496  def applyImageThresholds(self, maskedImage, highThreshold=None, lowThreshold=None,
497  regularizationWidth=2):
498  """Restrict image values to be between upper and lower limits.
499 
500  This method flags all pixels in an image that are outside of the given
501  threshold values. The threshold values are taken from a reference image,
502  so noisy pixels are likely to get flagged. In order to exclude those
503  noisy pixels, the array of flags is eroded and dilated, which removes
504  isolated pixels outside of the thresholds from the list of pixels to be
505  modified. Pixels that remain flagged after this operation have their
506  values set to the appropriate upper or lower threshold value.
507 
508  Parameters
509  ----------
510  maskedImage : `lsst.afw.image.MaskedImage`
511  The image to apply the thresholds to.
512  The image plane values will be modified in place.
513  highThreshold : `numpy.ndarray`, optional
514  Array of upper limit values for each pixel of ``maskedImage``.
515  lowThreshold : `numpy.ndarray`, optional
516  Array of lower limit values for each pixel of ``maskedImage``.
517  regularizationWidth : `int`, optional
518  Minimum radius of a region to include in regularization, in pixels.
519  """
520  # Generate the structure for binary erosion and dilation, which is used to remove noise-like pixels.
521  # Groups of pixels with a radius smaller than ``regularizationWidth``
522  # will be excluded from regularization.
523  image = maskedImage.image.array
524  filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
525  regularizationWidth)
526  if highThreshold is not None:
527  highPixels = image > highThreshold
528  if regularizationWidth > 0:
529  # Erode and dilate ``highPixels`` to exclude noisy pixels.
530  highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
531  image[highPixels] = highThreshold[highPixels]
532  if lowThreshold is not None:
533  lowPixels = image < lowThreshold
534  if regularizationWidth > 0:
535  # Erode and dilate ``lowPixels`` to exclude noisy pixels.
536  lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
537  image[lowPixels] = lowThreshold[lowPixels]
538 
539 
540 def applyDcr(maskedImage, dcr, warpCtrl, bbox=None, useInverse=False, splitSubfilters=False):
541  """Shift a masked image.
542 
543  Parameters
544  ----------
545  maskedImage : `lsst.afw.image.MaskedImage`
546  The input masked image to shift.
547  dcr : `lsst.afw.geom.Extent2I`
548  Shift calculated with ``calculateDcr``.
549  warpCtrl : `lsst.afw.math.WarpingControl`
550  Configuration settings for warping an image
551  bbox : `lsst.afw.geom.Box2I`, optional
552  Sub-region of the masked image to shift.
553  Shifts the entire image if None (Default).
554  useInverse : `bool`, optional
555  Use the reverse of ``dcr`` for the shift. Default: False
556  splitSubfilters : `bool`, optional
557  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
558  instead of at the midpoint. Default: False
559 
560  Returns
561  -------
562  shiftedImage : `lsst.afw.image.maskedImageF`
563  A masked image, with the pixels within the bounding box shifted.
564  """
565  padValue = afwImage.pixel.SinglePixelF(0., maskedImage.mask.getPlaneBitMask("NO_DATA"), 0)
566  if bbox is None:
567  bbox = maskedImage.getBBox()
568  if splitSubfilters:
569  shiftedImage = afwImage.MaskedImageF(bbox)
570  transform0 = makeTransform(AffineTransform((-1.0 if useInverse else 1.0)*dcr[0]))
571  afwMath.warpImage(shiftedImage, maskedImage[bbox],
572  transform0, warpCtrl, padValue=padValue)
573  shiftedImage1 = afwImage.MaskedImageF(bbox)
574  transform1 = makeTransform(AffineTransform((-1.0 if useInverse else 1.0)*dcr[1]))
575  afwMath.warpImage(shiftedImage1, maskedImage[bbox],
576  transform1, warpCtrl, padValue=padValue)
577  shiftedImage += shiftedImage1
578  shiftedImage /= 2.
579  else:
580  shiftedImage = afwImage.MaskedImageF(bbox)
581  transform = makeTransform(AffineTransform((-1.0 if useInverse else 1.0)*dcr))
582  afwMath.warpImage(shiftedImage, maskedImage[bbox],
583  transform, warpCtrl, padValue=padValue)
584  return shiftedImage
585 
586 
587 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
588  """Calculate the shift in pixels of an exposure due to DCR.
589 
590  Parameters
591  ----------
592  visitInfo : `lsst.afw.image.VisitInfo`
593  Metadata for the exposure.
594  wcs : `lsst.afw.geom.SkyWcs`
595  Coordinate system definition (wcs) for the exposure.
596  filterInfo : `lsst.afw.image.Filter`
597  The filter definition, set in the current instruments' obs package.
598  dcrNumSubfilters : `int`
599  Number of sub-filters used to model chromatic effects within a band.
600  splitSubfilters : `bool`, optional
601  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
602  instead of at the midpoint. Default: False
603 
604  Returns
605  -------
606  dcrShift : `lsst.afw.geom.Extent2I`
607  The 2D shift due to DCR, in pixels.
608  """
609  rotation = calculateImageParallacticAngle(visitInfo, wcs)
610  dcrShift = []
611  weight = [0.75, 0.25]
612  lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
613  for wl0, wl1 in wavelengthGenerator(filterInfo, dcrNumSubfilters):
614  # Note that diffRefractAmp can be negative, since it's relative to the midpoint of the full band
615  diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=lambdaEff,
616  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
617  observatory=visitInfo.getObservatory(),
618  weather=visitInfo.getWeather())
619  diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=lambdaEff,
620  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
621  observatory=visitInfo.getObservatory(),
622  weather=visitInfo.getWeather())
623  if splitSubfilters:
624  diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
625  diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
626  diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
627  diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
628  shiftX = [diffRefractPix*np.sin(rotation.asRadians()) for diffRefractPix in diffRefractArr]
629  shiftY = [diffRefractPix*np.cos(rotation.asRadians()) for diffRefractPix in diffRefractArr]
630  dcrShift.append((afwGeom.Extent2D(shiftX[0], shiftY[0]), afwGeom.Extent2D(shiftX[1], shiftY[1])))
631  else:
632  diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
633  diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
634  shiftX = diffRefractPix*np.sin(rotation.asRadians())
635  shiftY = diffRefractPix*np.cos(rotation.asRadians())
636  dcrShift.append(afwGeom.Extent2D(shiftX, shiftY))
637  return dcrShift
638 
639 
640 def calculateImageParallacticAngle(visitInfo, wcs):
641  """Calculate the total sky rotation angle of an exposure.
642 
643  Parameters
644  ----------
645  visitInfo : `lsst.afw.image.VisitInfo`
646  Metadata for the exposure.
647  wcs : `lsst.afw.geom.SkyWcs`
648  Coordinate system definition (wcs) for the exposure.
649 
650  Returns
651  -------
652  `lsst.geom.Angle`
653  The rotation of the image axis, East from North.
654  Equal to the parallactic angle plus any additional rotation of the
655  coordinate system.
656  A rotation angle of 0 degrees is defined with
657  North along the +y axis and East along the +x axis.
658  A rotation angle of 90 degrees is defined with
659  North along the +x axis and East along the -y axis.
660  """
661  parAngle = visitInfo.getBoresightParAngle().asRadians()
662  cd = wcs.getCdMatrix()
663  if wcs.isFlipped:
664  cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
665  else:
666  cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
667  rotAngle = (cdAngle + parAngle)*radians
668  return rotAngle
669 
670 
671 def wavelengthGenerator(filterInfo, dcrNumSubfilters):
672  """Iterate over the wavelength endpoints of subfilters.
673 
674  Parameters
675  ----------
676  filterInfo : `lsst.afw.image.Filter`
677  The filter definition, set in the current instruments' obs package.
678  dcrNumSubfilters : `int`
679  Number of sub-filters used to model chromatic effects within a band.
680 
681  Yields
682  ------
683  `tuple` of two `float`
684  The next set of wavelength endpoints for a subfilter, in nm.
685  """
686  lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
687  lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
688  wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
689  for wl in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=False):
690  yield (wl, wl + wlStep)
def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None)
Definition: dcrModel.py:73
def calculateNoiseCutoff(self, maskedImage, statsCtrl, bufferSize, convergenceMaskPlanes="DETECTED", mask=None, bbox=None)
Definition: dcrModel.py:460
def __setitem__(self, subfilter, maskedImage)
Definition: dcrModel.py:184
def applyImageThresholds(self, maskedImage, highThreshold=None, lowThreshold=None, regularizationWidth=2)
Definition: dcrModel.py:497
def calculateImageParallacticAngle(visitInfo, wcs)
Definition: dcrModel.py:640
int warpImage(DestImageT &destImage, SrcImageT const &srcImage, geom::TransformPoint2ToPoint2 const &srcToDest, WarpingControl const &control, typename DestImageT::SinglePixel padValue=lsst::afw::math::edgePixel< DestImageT >(typename lsst::afw::image::detail::image_traits< DestImageT >::image_category()))
def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, regularizationWidth=2)
Definition: dcrModel.py:408
def regularizeModelFreq(self, modelImages, bbox, regularizationFactor, regularizationWidth=2)
Definition: dcrModel.py:433
def __getitem__(self, subfilter)
Definition: dcrModel.py:159
def __init__(self, modelImages, filterInfo=None, psf=None)
Definition: dcrModel.py:66
def wavelengthGenerator(filterInfo, dcrNumSubfilters)
Definition: dcrModel.py:671
def buildMatchedTemplate(self, exposure=None, warpCtrl=None, visitInfo=None, bbox=None, wcs=None, mask=None, splitSubfilters=False)
Definition: dcrModel.py:293
def buildMatchedExposure(self, exposure=None, warpCtrl=None, visitInfo=None, bbox=None, wcs=None, mask=None)
Definition: dcrModel.py:351
def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, kwargs)
Definition: dcrModel.py:114
def getReferenceImage(self, bbox=None)
Definition: dcrModel.py:252
def applyDcr(maskedImage, dcr, warpCtrl, bbox=None, useInverse=False, splitSubfilters=False)
Definition: dcrModel.py:540
def assign(self, dcrSubModel, bbox=None)
Definition: dcrModel.py:268
def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False)
Definition: dcrModel.py:587
def conditionDcrModel(self, modelImages, bbox, gain=1.)
Definition: dcrModel.py:383