24 from scipy 
import ndimage
 
   29 __all__ = [
"DcrModel", 
"applyDcr", 
"calculateDcr", 
"calculateImageParallacticAngle"]
 
   33     """A model of the true sky after correcting chromatic effects. 
   37     dcrNumSubfilters : `int` 
   38         Number of sub-filters used to model chromatic effects within a band. 
   39     modelImages : `list` of `lsst.afw.image.Image` 
   40         A list of masked images, each containing the model for one subfilter 
   44     The ``DcrModel`` contains an estimate of the true sky, at a higher 
   45     wavelength resolution than the input observations. It can be forward- 
   46     modeled to produce Differential Chromatic Refraction (DCR) matched 
   47     templates for a given ``Exposure``, and provides utilities for conditioning 
   48     the model in ``dcrAssembleCoadd`` to avoid oscillating solutions between 
   49     iterations of forward modeling or between the subfilters of the model. 
   52     def __init__(self, modelImages, filterInfo=None, psf=None, mask=None, variance=None, photoCalib=None):
 
   62     def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None, photoCalib=None):
 
   63         """Initialize a DcrModel by dividing a coadd between the subfilters. 
   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         photoCalib : `lsst.afw.image.PhotoCalib`, optional 
   78             Calibration to convert instrumental flux and 
   79             flux error to nanoJansky. 
   83         dcrModel : `lsst.pipe.tasks.DcrModel` 
   84             Best fit model of the true sky after correcting chromatic effects. 
   89             If there are any unmasked NAN values in ``maskedImage``. 
   93         model = maskedImage.image.clone()
 
   94         mask = maskedImage.mask.clone()
 
  100         variance = maskedImage.variance.clone()
 
  101         variance /= dcrNumSubfilters
 
  102         model /= dcrNumSubfilters
 
  103         modelImages = [model, ]
 
  104         for subfilter 
in range(1, dcrNumSubfilters):
 
  105             modelImages.append(model.clone())
 
  106         return cls(modelImages, filterInfo, psf, mask, variance, photoCalib=photoCalib)
 
  109     def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, **kwargs):
 
  110         """Load an existing DcrModel from a Gen 2 repository. 
  114         dataRef : `lsst.daf.persistence.ButlerDataRef` 
  115             Data reference defining the patch for coaddition and the 
  117         datasetType : `str`, optional 
  118             Name of the DcrModel in the registry {"dcrCoadd", "dcrCoadd_sub"} 
  119         numSubfilters : `int` 
  120             Number of sub-filters used to model chromatic effects within a band. 
  122             Additional keyword arguments to pass to look up the model in the data registry. 
  123             Common keywords and their types include: ``tract``:`str`, ``patch``:`str`, 
  124             ``bbox``:`lsst.afw.geom.Box2I` 
  128         dcrModel : `lsst.pipe.tasks.DcrModel` 
  129             Best fit model of the true sky after correcting chromatic effects. 
  137         for subfilter 
in range(numSubfilters):
 
  138             dcrCoadd = dataRef.get(datasetType, subfilter=subfilter,
 
  139                                    numSubfilters=numSubfilters, **kwargs)
 
  140             if filterInfo 
is None:
 
  141                 filterInfo = dcrCoadd.getFilter()
 
  143                 psf = dcrCoadd.getPsf()
 
  147                 variance = dcrCoadd.variance
 
  148             if photoCalib 
is None:
 
  149                 photoCalib = dcrCoadd.getPhotoCalib()
 
  150             modelImages.append(dcrCoadd.image)
 
  151         return cls(modelImages, filterInfo, psf, mask, variance, photoCalib)
 
  155         """Load an existing DcrModel from a Gen 3 repository. 
  159         availableCoaddRefs : `dict` of `int` : `lsst.daf.butler.DeferredDatasetHandle` 
  160             Dictionary of spatially relevant retrieved coadd patches, 
  161             indexed by their sequential patch number. 
  165         dcrModel : `lsst.pipe.tasks.DcrModel` 
  166             Best fit model of the true sky after correcting chromatic effects. 
  173         modelImages = [
None]*len(availableCoaddRefs)
 
  175         for coaddRef 
in availableCoaddRefs:
 
  176             subfilter = coaddRef.dataId[
"subfilter"]
 
  177             dcrCoadd = coaddRef.get()
 
  178             if filterInfo 
is None:
 
  179                 filterInfo = dcrCoadd.getFilter()
 
  181                 psf = dcrCoadd.getPsf()
 
  185                 variance = dcrCoadd.variance
 
  186             if photoCalib 
is None:
 
  187                 photoCalib = dcrCoadd.getPhotoCalib()
 
  188             modelImages[subfilter] = dcrCoadd.image
 
  189         return cls(modelImages, filterInfo, psf, mask, variance, photoCalib)
 
  192         """Return the number of subfilters. 
  196         dcrNumSubfilters : `int` 
  197             The number of DCR subfilters in the model. 
  202         """Iterate over the subfilters of the DCR model. 
  207             Index of the current ``subfilter`` within the full band. 
  208             Negative indices are allowed, and count in reverse order 
  209             from the highest ``subfilter``. 
  213         modelImage : `lsst.afw.image.Image` 
  214             The DCR model for the given ``subfilter``. 
  219             If the requested ``subfilter`` is greater or equal to the number 
  220             of subfilters in the model. 
  222         if np.abs(subfilter) >= len(self):
 
  223             raise IndexError(
"subfilter out of bounds.")
 
  227         """Update the model image for one subfilter. 
  232             Index of the current subfilter within the full band. 
  233         maskedImage : `lsst.afw.image.Image` 
  234             The DCR model to set for the given ``subfilter``. 
  239             If the requested ``subfilter`` is greater or equal to the number 
  240             of subfilters in the model. 
  242             If the bounding box of the new image does not match. 
  244         if np.abs(subfilter) >= len(self):
 
  245             raise IndexError(
"subfilter out of bounds.")
 
  246         if maskedImage.getBBox() != self.
bbox:
 
  247             raise ValueError(
"The bounding box of a subfilter must not change.")
 
  252         """Return the filter of the model. 
  256         filter : `lsst.afw.image.Filter` 
  257             The filter definition, set in the current instruments' obs package. 
  263         """Return the psf of the model. 
  267         psf : `lsst.afw.detection.Psf` 
  268             Point spread function (PSF) of the model. 
  274         """Return the common bounding box of each subfilter image. 
  278         bbox : `lsst.afw.geom.Box2I` 
  279             Bounding box of the DCR model. 
  281         return self[0].getBBox()
 
  285         """Return the common mask of each subfilter image. 
  289         mask : `lsst.afw.image.Mask` 
  290             Mask plane of the DCR model. 
  296         """Return the common variance of each subfilter image. 
  300         variance : `lsst.afw.image.Image` 
  301             Variance plane of the DCR model. 
  306         """Calculate a reference image from the average of the subfilter images. 
  310         bbox : `lsst.afw.geom.Box2I`, optional 
  311             Sub-region of the coadd. Returns the entire image if `None`. 
  315         refImage : `numpy.ndarray` 
  316             The reference image with no chromatic effects applied. 
  318         bbox = bbox 
or self.
bbox 
  319         return np.mean([model[bbox].array 
for model 
in self], axis=0)
 
  321     def assign(self, dcrSubModel, bbox=None):
 
  322         """Update a sub-region of the ``DcrModel`` with new values. 
  326         dcrSubModel : `lsst.pipe.tasks.DcrModel` 
  327             New model of the true scene after correcting chromatic effects. 
  328         bbox : `lsst.afw.geom.Box2I`, optional 
  329             Sub-region of the coadd. 
  330             Defaults to the bounding box of ``dcrSubModel``. 
  335             If the new model has a different number of subfilters. 
  337         if len(dcrSubModel) != len(self):
 
  338             raise ValueError(
"The number of DCR subfilters must be the same " 
  339                              "between the old and new models.")
 
  340         bbox = bbox 
or self.
bbox 
  341         for model, subModel 
in zip(self, dcrSubModel):
 
  342             model.assign(subModel[bbox], bbox)
 
  345                              visitInfo=None, bbox=None, wcs=None, mask=None,
 
  346                              splitSubfilters=True, splitThreshold=0., amplifyModel=1.):
 
  347         """Create a DCR-matched template image for an exposure. 
  351         exposure : `lsst.afw.image.Exposure`, optional 
  352             The input exposure to build a matched template for. 
  353             May be omitted if all of the metadata is supplied separately 
  354         order : `int`, optional 
  355             Interpolation order of the DCR shift. 
  356         visitInfo : `lsst.afw.image.VisitInfo`, optional 
  357             Metadata for the exposure. Ignored if ``exposure`` is set. 
  358         bbox : `lsst.afw.geom.Box2I`, optional 
  359             Sub-region of the coadd. Ignored if ``exposure`` is set. 
  360         wcs : `lsst.afw.geom.SkyWcs`, optional 
  361             Coordinate system definition (wcs) for the exposure. 
  362             Ignored if ``exposure`` is set. 
  363         mask : `lsst.afw.image.Mask`, optional 
  364             reference mask to use for the template image. 
  365         splitSubfilters : `bool`, optional 
  366             Calculate DCR for two evenly-spaced wavelengths in each subfilter, 
  367             instead of at the midpoint. Default: True 
  368         splitThreshold : `float`, optional 
  369             Minimum DCR difference within a subfilter required to use ``splitSubfilters`` 
  370         amplifyModel : `float`, optional 
  371             Multiplication factor to amplify differences between model planes. 
  372             Used to speed convergence of iterative forward modeling. 
  376         templateImage : `lsst.afw.image.ImageF` 
  377             The DCR-matched template 
  382             If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set. 
  385             raise ValueError(
"'filterInfo' must be set for the DcrModel in order to calculate DCR.")
 
  386         if exposure 
is not None:
 
  387             visitInfo = exposure.getInfo().getVisitInfo()
 
  388             bbox = exposure.getBBox()
 
  389             wcs = exposure.getInfo().getWcs()
 
  390         elif visitInfo 
is None or bbox 
is None or wcs 
is None:
 
  391             raise ValueError(
"Either exposure or visitInfo, bbox, and wcs must be set.")
 
  392         dcrShift = 
calculateDcr(visitInfo, wcs, self.
filter, len(self), splitSubfilters=splitSubfilters)
 
  393         templateImage = afwImage.ImageF(bbox)
 
  395         for subfilter, dcr 
in enumerate(dcrShift):
 
  397                 model = (self[subfilter][bbox].array - refModel)*amplifyModel + refModel
 
  399                 model = self[subfilter][bbox].array
 
  400             templateImage.array += 
applyDcr(model, dcr, splitSubfilters=splitSubfilters,
 
  401                                             splitThreshold=splitThreshold, order=order)
 
  405                              visitInfo=None, bbox=None, wcs=None, mask=None):
 
  406         """Wrapper to create an exposure from a template image. 
  410         exposure : `lsst.afw.image.Exposure`, optional 
  411             The input exposure to build a matched template for. 
  412             May be omitted if all of the metadata is supplied separately 
  413         visitInfo : `lsst.afw.image.VisitInfo`, optional 
  414             Metadata for the exposure. Ignored if ``exposure`` is set. 
  415         bbox : `lsst.afw.geom.Box2I`, optional 
  416             Sub-region of the coadd. Ignored if ``exposure`` is set. 
  417         wcs : `lsst.afw.geom.SkyWcs`, optional 
  418             Coordinate system definition (wcs) for the exposure. 
  419             Ignored if ``exposure`` is set. 
  420         mask : `lsst.afw.image.Mask`, optional 
  421             reference mask to use for the template image. 
  425         templateExposure : `lsst.afw.image.exposureF` 
  426             The DCR-matched template 
  429             bbox = exposure.getBBox()
 
  431                                                   bbox=bbox, wcs=wcs, mask=mask)
 
  432         maskedImage = afwImage.MaskedImageF(bbox)
 
  433         maskedImage.image = templateImage[bbox]
 
  434         maskedImage.mask = self.
mask[bbox]
 
  435         maskedImage.variance = self.
variance[bbox]
 
  439         templateExposure = afwImage.ExposureF(bbox, wcs)
 
  440         templateExposure.setMaskedImage(maskedImage[bbox])
 
  441         templateExposure.setPsf(self.
psf)
 
  442         templateExposure.setFilter(self.
filter)
 
  444             raise RuntimeError(
"No PhotoCalib set for the DcrModel. " 
  445                                "If the DcrModel was created from a masked image" 
  446                                " you must also specify the photoCalib.")
 
  447         templateExposure.setPhotoCalib(self.
photoCalib)
 
  448         return templateExposure
 
  451         """Average two iterations' solutions to reduce oscillations. 
  455         modelImages : `list` of `lsst.afw.image.Image` 
  456             The new DCR model images from the current iteration. 
  457             The values will be modified in place. 
  458         bbox : `lsst.afw.geom.Box2I` 
  459             Sub-region of the coadd 
  460         gain : `float`, optional 
  461             Relative weight to give the new solution when updating the model. 
  462             Defaults to 1.0, which gives equal weight to both solutions. 
  465         for model, newModel 
in zip(self, modelImages):
 
  467             newModel += model[bbox]
 
  468             newModel /= 1. + gain
 
  471                             regularizationWidth=2):
 
  472         """Restrict large variations in the model between iterations. 
  477             Index of the current subfilter within the full band. 
  478         newModel : `lsst.afw.image.Image` 
  479             The new DCR model for one subfilter from the current iteration. 
  480             Values in ``newModel`` that are extreme compared with the last 
  481             iteration are modified in place. 
  482         bbox : `lsst.afw.geom.Box2I` 
  484         regularizationFactor : `float` 
  485             Maximum relative change of the model allowed between iterations. 
  486         regularizationWidth : int, optional 
  487             Minimum radius of a region to include in regularization, in pixels. 
  489         refImage = self[subfilter][bbox].array
 
  490         highThreshold = np.abs(refImage)*regularizationFactor
 
  491         lowThreshold = refImage/regularizationFactor
 
  492         newImage = newModel.array
 
  494                                   regularizationWidth=regularizationWidth)
 
  497                             regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
 
  498         """Restrict large variations in the model between subfilters. 
  502         modelImages : `list` of `lsst.afw.image.Image` 
  503             The new DCR model images from the current iteration. 
  504             The values will be modified in place. 
  505         bbox : `lsst.afw.geom.Box2I` 
  507         statsCtrl : `lsst.afw.math.StatisticsControl` 
  508             Statistics control object for coaddition. 
  509         regularizationFactor : `float` 
  510             Maximum relative change of the model allowed between subfilters. 
  511         regularizationWidth : `int`, optional 
  512             Minimum radius of a region to include in regularization, in pixels. 
  513         mask : `lsst.afw.image.Mask`, optional 
  514             Optional alternate mask 
  515         convergenceMaskPlanes : `list` of `str`, or `str`, optional 
  516             Mask planes to use to calculate convergence. 
  520         This implementation of frequency regularization restricts each subfilter 
  521         image to be a smoothly-varying function times a reference image. 
  525         maxDiff = np.sqrt(regularizationFactor)
 
  526         noiseLevel = self.
calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
 
  528         badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
 
  529         if np.sum(~badPixels) == 0:
 
  532         referenceImage[badPixels] = 0.
 
  533         filterWidth = regularizationWidth
 
  534         fwhm = 2.*filterWidth
 
  537         smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth, mode=
'constant')
 
  540         smoothRef += 3.*noiseLevel
 
  542         lowThreshold = smoothRef/maxDiff
 
  543         highThreshold = smoothRef*maxDiff
 
  544         for model 
in modelImages:
 
  546                                       highThreshold=highThreshold,
 
  547                                       lowThreshold=lowThreshold,
 
  548                                       regularizationWidth=regularizationWidth)
 
  549             smoothModel = ndimage.filters.gaussian_filter(model.array, filterWidth, mode=
'constant')
 
  550             smoothModel += 3.*noiseLevel
 
  551             relativeModel = smoothModel/smoothRef
 
  554             relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/alpha)
 
  555             relativeModel += alpha*(relativeModel - relativeModel2)
 
  556             model.array = relativeModel*referenceImage
 
  559                              convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
 
  560         """Helper function to calculate the background noise level of an image. 
  564         image : `lsst.afw.image.Image` 
  565             The input image to evaluate the background noise properties. 
  566         statsCtrl : `lsst.afw.math.StatisticsControl` 
  567             Statistics control object for coaddition. 
  569             Number of additional pixels to exclude 
  570             from the edges of the bounding box. 
  571         convergenceMaskPlanes : `list` of `str`, or `str` 
  572             Mask planes to use to calculate convergence. 
  573         mask : `lsst.afw.image.Mask`, Optional 
  574             Optional alternate mask 
  575         bbox : `lsst.afw.geom.Box2I`, optional 
  576             Sub-region of the masked image to calculate the noise level over. 
  580         noiseCutoff : `float` 
  581             The threshold value to treat pixels as noise in an image.. 
  586             mask = self.
mask[bbox]
 
  588         bboxShrink.grow(-bufferSize)
 
  589         convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
 
  591         backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
 
  592         noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
 
  596         """Restrict image values to be between upper and lower limits. 
  598         This method flags all pixels in an image that are outside of the given 
  599         threshold values. The threshold values are taken from a reference image, 
  600         so noisy pixels are likely to get flagged. In order to exclude those 
  601         noisy pixels, the array of flags is eroded and dilated, which removes 
  602         isolated pixels outside of the thresholds from the list of pixels to be 
  603         modified. Pixels that remain flagged after this operation have their 
  604         values set to the appropriate upper or lower threshold value. 
  608         image : `numpy.ndarray` 
  609             The image to apply the thresholds to. 
  610             The values will be modified in place. 
  611         highThreshold : `numpy.ndarray`, optional 
  612             Array of upper limit values for each pixel of ``image``. 
  613         lowThreshold : `numpy.ndarray`, optional 
  614             Array of lower limit values for each pixel of ``image``. 
  615         regularizationWidth : `int`, optional 
  616             Minimum radius of a region to include in regularization, in pixels. 
  621         filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
 
  623         if highThreshold 
is not None:
 
  624             highPixels = image > highThreshold
 
  625             if regularizationWidth > 0:
 
  627                 highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
 
  628             image[highPixels] = highThreshold[highPixels]
 
  629         if lowThreshold 
is not None:
 
  630             lowPixels = image < lowThreshold
 
  631             if regularizationWidth > 0:
 
  633                 lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
 
  634             image[lowPixels] = lowThreshold[lowPixels]
 
  637 def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0.,
 
  638              doPrefilter=True, order=3):
 
  639     """Shift an image along the X and Y directions. 
  643     image : `numpy.ndarray` 
  644         The input image to shift. 
  646         Shift calculated with ``calculateDcr``. 
  647         Uses numpy axes ordering (Y, X). 
  648         If ``splitSubfilters`` is set, each element is itself a `tuple` 
  649         of two `float`, corresponding to the DCR shift at the two wavelengths. 
  650         Otherwise, each element is a `float` corresponding to the DCR shift at 
  651         the effective wavelength of the subfilter. 
  652     useInverse : `bool`, optional 
  653         Apply the shift in the opposite direction. Default: False 
  654     splitSubfilters : `bool`, optional 
  655         Calculate DCR for two evenly-spaced wavelengths in each subfilter, 
  656         instead of at the midpoint. Default: False 
  657     splitThreshold : `float`, optional 
  658         Minimum DCR difference within a subfilter required to use ``splitSubfilters`` 
  659     doPrefilter : `bool`, optional 
  660         Spline filter the image before shifting, if set. Filtering is required, 
  661         so only set to False if the image is already filtered. 
  662         Filtering takes ~20% of the time of shifting, so if `applyDcr` will be 
  663         called repeatedly on the same image it is more efficient to precalculate 
  665     order : `int`, optional 
  666         The order of the spline interpolation, default is 3. 
  670     shiftedImage : `numpy.ndarray` 
  671         A copy of the input image with the specified shift applied. 
  674         prefilteredImage = ndimage.spline_filter(image, order=order)
 
  676         prefilteredImage = image
 
  678         shiftAmp = np.max(np.abs([_dcr0 - _dcr1 
for _dcr0, _dcr1 
in zip(dcr[0], dcr[1])]))
 
  679         if shiftAmp >= splitThreshold:
 
  681                 shift = [-1.*s 
for s 
in dcr[0]]
 
  682                 shift1 = [-1.*s 
for s 
in dcr[1]]
 
  686             shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=
False, order=order)
 
  687             shiftedImage += ndimage.shift(prefilteredImage, shift1, prefilter=
False, order=order)
 
  693             dcr = (np.mean(dcr[0]), np.mean(dcr[1]))
 
  695         shift = [-1.*s 
for s 
in dcr]
 
  698     shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=
False, order=order)
 
  702 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
 
  703     """Calculate the shift in pixels of an exposure due to DCR. 
  707     visitInfo : `lsst.afw.image.VisitInfo` 
  708         Metadata for the exposure. 
  709     wcs : `lsst.afw.geom.SkyWcs` 
  710         Coordinate system definition (wcs) for the exposure. 
  711     filterInfo : `lsst.afw.image.Filter` 
  712         The filter definition, set in the current instruments' obs package. 
  713     dcrNumSubfilters : `int` 
  714         Number of sub-filters used to model chromatic effects within a band. 
  715     splitSubfilters : `bool`, optional 
  716         Calculate DCR for two evenly-spaced wavelengths in each subfilter, 
  717         instead of at the midpoint. Default: False 
  721     dcrShift : `tuple` of two `float` 
  722         The 2D shift due to DCR, in pixels. 
  723         Uses numpy axes ordering (Y, X). 
  727     weight = [0.75, 0.25]
 
  728     lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
 
  732                                                  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
 
  733                                                  observatory=visitInfo.getObservatory(),
 
  734                                                  weather=visitInfo.getWeather())
 
  736                                                  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
 
  737                                                  observatory=visitInfo.getObservatory(),
 
  738                                                  weather=visitInfo.getWeather())
 
  740             diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
 
  741             diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
 
  742             diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
 
  743                               diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
 
  744             shiftX = [diffRefractPix*np.sin(rotation.asRadians()) 
for diffRefractPix 
in diffRefractArr]
 
  745             shiftY = [diffRefractPix*np.cos(rotation.asRadians()) 
for diffRefractPix 
in diffRefractArr]
 
  746             dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1])))
 
  748             diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
 
  749             diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
 
  750             shiftX = diffRefractPix*np.sin(rotation.asRadians())
 
  751             shiftY = diffRefractPix*np.cos(rotation.asRadians())
 
  752             dcrShift.append((shiftY, shiftX))
 
  757     """Calculate the total sky rotation angle of an exposure. 
  761     visitInfo : `lsst.afw.image.VisitInfo` 
  762         Metadata for the exposure. 
  763     wcs : `lsst.afw.geom.SkyWcs` 
  764         Coordinate system definition (wcs) for the exposure. 
  769         The rotation of the image axis, East from North. 
  770         Equal to the parallactic angle plus any additional rotation of the 
  772         A rotation angle of 0 degrees is defined with 
  773         North along the +y axis and East along the +x axis. 
  774         A rotation angle of 90 degrees is defined with 
  775         North along the +x axis and East along the -y axis. 
  777     parAngle = visitInfo.getBoresightParAngle().asRadians()
 
  778     cd = wcs.getCdMatrix()
 
  780         cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
 
  781         rotAngle = (cdAngle + parAngle)*geom.radians
 
  783         cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
 
  784         rotAngle = (cdAngle - parAngle)*geom.radians
 
  789     """Iterate over the wavelength endpoints of subfilters. 
  793     filterInfo : `lsst.afw.image.Filter` 
  794         The filter definition, set in the current instruments' obs package. 
  795     dcrNumSubfilters : `int` 
  796         Number of sub-filters used to model chromatic effects within a band. 
  800     `tuple` of two `float` 
  801         The next set of wavelength endpoints for a subfilter, in nm. 
  803     lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
 
  804     lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
 
  805     wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
 
  806     for wl 
in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=
False):
 
  807         yield (wl, wl + wlStep)