24from 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.
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, effectiveWavelength, bandwidth, filterLabel=None, psf=None,
53 bbox=None, wcs=None, mask=None, variance=None, photoCalib=None):
60 self.
_bbox_bbox = bbox
62 self.
_mask_mask = mask
67 def fromImage(cls, maskedImage, dcrNumSubfilters, effectiveWavelength, bandwidth,
68 wcs=None, filterLabel=None, psf=None, photoCalib=None):
69 """Initialize a DcrModel by dividing a coadd between the subfilters.
74 Input coadded image to divide equally between the subfilters.
75 dcrNumSubfilters : `int`
76 Number of sub-filters used to model chromatic effects within a
78 effectiveWavelength : `float`
79 The effective wavelengths of the current filter, in nanometers.
81 The bandwidth of the current filter,
in nanometers.
83 Coordinate system definition (wcs)
for the exposure.
85 The filter label, set
in the current instruments
' obs package.
86 Required for any calculation of DCR, including making matched
89 Point spread function (PSF) of the model.
90 Required
if the ``DcrModel`` will be persisted.
92 Calibration to convert instrumental flux
and
93 flux error to nanoJansky.
97 dcrModel : `lsst.pipe.tasks.DcrModel`
98 Best fit model of the true sky after correcting chromatic effects.
102 model = maskedImage.image.clone()
103 mask = maskedImage.mask.clone()
104 bbox = maskedImage.getBBox()
110 variance = maskedImage.variance.clone()
111 variance /= dcrNumSubfilters
112 model /= dcrNumSubfilters
113 modelImages = [model, ]
114 for subfilter
in range(1, dcrNumSubfilters):
115 modelImages.append(model.clone())
116 return cls(modelImages, effectiveWavelength, bandwidth,
117 filterLabel=filterLabel, psf=psf, bbox=bbox, wcs=wcs,
118 mask=mask, variance=variance, photoCalib=photoCalib)
121 def fromQuantum(cls, availableCoaddRefs, effectiveWavelength, bandwidth):
122 """Load an existing DcrModel from a Gen 3 repository.
126 availableCoaddRefs : `dict` [`int`, `lsst.daf.butler.DeferredDatasetHandle`]
127 Dictionary of spatially relevant retrieved coadd patches,
128 indexed by their sequential patch number.
129 effectiveWavelength : `float`
130 The effective wavelengths of the current filter, in nanometers.
132 The bandwidth of the current filter,
in nanometers.
136 dcrModel : `lsst.pipe.tasks.DcrModel`
137 Best fit model of the true sky after correcting chromatic effects.
146 modelImages = [
None]*len(availableCoaddRefs)
148 for coaddRef
in availableCoaddRefs:
149 subfilter = coaddRef.dataId[
"subfilter"]
150 dcrCoadd = coaddRef.get()
151 if filterLabel
is None:
152 filterLabel = dcrCoadd.getFilterLabel()
154 psf = dcrCoadd.getPsf()
156 bbox = dcrCoadd.getBBox()
162 variance = dcrCoadd.variance
163 if photoCalib
is None:
164 photoCalib = dcrCoadd.getPhotoCalib()
165 modelImages[subfilter] = dcrCoadd.image
166 return cls(modelImages, effectiveWavelength, bandwidth, filterLabel,
167 psf, bbox, wcs, mask, variance, photoCalib)
170 """Return the number of subfilters.
174 dcrNumSubfilters : `int`
175 The number of DCR subfilters in the model.
180 """Iterate over the subfilters of the DCR model.
185 Index of the current ``subfilter`` within the full band.
186 Negative indices are allowed, and count
in reverse order
187 from the highest ``subfilter``.
192 The DCR model
for the given ``subfilter``.
197 If the requested ``subfilter``
is greater
or equal to the number
198 of subfilters
in the model.
200 if np.abs(subfilter) >= len(self):
201 raise IndexError(
"subfilter out of bounds.")
205 """Update the model image for one subfilter.
210 Index of the current subfilter within the full band.
212 The DCR model to set for the given ``subfilter``.
217 If the requested ``subfilter``
is greater
or equal to the number
218 of subfilters
in the model.
220 If the bounding box of the new image does
not match.
222 if np.abs(subfilter) >= len(self):
223 raise IndexError(
"subfilter out of bounds.")
224 if maskedImage.getBBox() != self.
bboxbbox:
225 raise ValueError(
"The bounding box of a subfilter must not change.")
226 self.
modelImagesmodelImages[subfilter] = maskedImage
230 """Return the effective wavelength of the model.
234 effectiveWavelength : `float`
235 The effective wavelength of the current filter, in nanometers.
241 """Return the filter label for the model.
246 The filter used for the input observations.
252 """Return the bandwidth of the model.
257 The bandwidth of the current filter, in nanometers.
263 """Return the psf of the model.
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.
_bbox_bbox
285 """Return the WCS of each subfilter image.
290 Coordinate system definition (wcs) for the exposure.
296 """Return the common mask of each subfilter image.
301 Mask plane of the DCR model.
303 return self.
_mask_mask
307 """Return the common variance of each subfilter image.
312 Variance plane of the DCR model.
317 """Calculate a reference image from the average of the subfilter
322 bbox : `lsst.afw.geom.Box2I`, optional
323 Sub-region of the coadd. Returns the entire image if `
None`.
327 refImage : `numpy.ndarray`
328 The reference image
with no chromatic effects applied.
330 bbox = bbox or self.
bboxbbox
331 return np.mean([model[bbox].array
for model
in self], axis=0)
333 def assign(self, dcrSubModel, bbox=None):
334 """Update a sub-region of the ``DcrModel`` with new values.
338 dcrSubModel : `lsst.pipe.tasks.DcrModel`
339 New model of the true scene after correcting chromatic effects.
340 bbox : `lsst.afw.geom.Box2I`, optional
341 Sub-region of the coadd.
342 Defaults to the bounding box of ``dcrSubModel``.
347 If the new model has a different number of subfilters.
349 if len(dcrSubModel) != len(self):
350 raise ValueError(
"The number of DCR subfilters must be the same "
351 "between the old and new models.")
352 bbox = bbox
or self.
bboxbbox
353 for model, subModel
in zip(self, dcrSubModel):
354 model.assign(subModel[bbox], bbox)
357 visitInfo=None, bbox=None, mask=None,
358 splitSubfilters=True, splitThreshold=0., amplifyModel=1.):
359 """Create a DCR-matched template image for an exposure.
364 The input exposure to build a matched template for.
365 May be omitted
if all of the metadata
is supplied separately
366 order : `int`, optional
367 Interpolation order of the DCR shift.
369 Metadata
for the exposure. Ignored
if ``exposure``
is set.
370 bbox : `lsst.afw.geom.Box2I`, optional
371 Sub-region of the coadd,
or use the entire coadd
if not supplied.
373 reference mask to use
for the template image.
374 splitSubfilters : `bool`, optional
375 Calculate DCR
for two evenly-spaced wavelengths
in each subfilter,
376 instead of at the midpoint. Default:
True
377 splitThreshold : `float`, optional
378 Minimum DCR difference within a subfilter required to use
380 amplifyModel : `float`, optional
381 Multiplication factor to amplify differences between model planes.
382 Used to speed convergence of iterative forward modeling.
386 templateImage : `lsst.afw.image.ImageF`
387 The DCR-matched template
392 If neither ``exposure``
or ``visitInfo`` are set.
395 raise ValueError(
"'effectiveWavelength' and 'bandwidth' must be set for the DcrModel in order "
397 if exposure
is not None:
398 visitInfo = exposure.getInfo().getVisitInfo()
399 elif visitInfo
is None:
400 raise ValueError(
"Either exposure or visitInfo must be set.")
404 splitSubfilters=splitSubfilters)
405 templateImage = afwImage.ImageF(bbox)
407 for subfilter, dcr
in enumerate(dcrShift):
408 if self[subfilter]
is None:
410 self.log.debug(
"Skipping missing DCR model subfilter %d", subfilter)
417 model = (self[subfilter][bbox].array - refModel)*amplifyModel + refModel
419 model = self[subfilter][bbox].array
420 templateImage.array +=
applyDcr(model, dcr, splitSubfilters=splitSubfilters,
421 splitThreshold=splitThreshold, order=order)
425 visitInfo=None, bbox=None, mask=None):
426 """Wrapper to create an exposure from a template image.
431 The input exposure to build a matched template for.
432 May be omitted
if all of the metadata
is supplied separately
434 Metadata
for the exposure. Ignored
if ``exposure``
is set.
435 bbox : `lsst.afw.geom.Box2I`, optional
436 Sub-region of the coadd,
or use the entire coadd
if not supplied.
438 reference mask to use
for the template image.
442 templateExposure : `lsst.afw.image.exposureF`
443 The DCR-matched template
448 If no `photcCalib`
is set.
452 templateImage = self.
buildMatchedTemplatebuildMatchedTemplate(exposure=exposure, visitInfo=visitInfo,
453 bbox=bbox, mask=mask)
454 maskedImage = afwImage.MaskedImageF(bbox)
455 maskedImage.image = templateImage[bbox]
456 maskedImage.mask = self.
maskmask[bbox]
457 maskedImage.variance = self.
variancevariance[bbox]
461 templateExposure = afwImage.ExposureF(bbox, self.
wcswcs)
462 templateExposure.setMaskedImage(maskedImage[bbox])
463 templateExposure.setPsf(self.
psfpsf)
464 templateExposure.setFilterLabel(self.
filterfilter)
466 raise RuntimeError(
"No PhotoCalib set for the DcrModel. "
467 "If the DcrModel was created from a masked image"
468 " you must also specify the photoCalib.")
469 templateExposure.setPhotoCalib(self.
photoCalibphotoCalib)
470 return templateExposure
473 """Average two iterations' solutions to reduce oscillations.
478 The new DCR model images from the current iteration.
479 The values will be modified
in place.
480 bbox : `lsst.afw.geom.Box2I`
481 Sub-region of the coadd
482 gain : `float`, optional
483 Relative weight to give the new solution when updating the model.
484 Defaults to 1.0, which gives equal weight to both solutions.
487 for model, newModel
in zip(self, modelImages):
489 newModel += model[bbox]
490 newModel /= 1. + gain
493 regularizationWidth=2):
494 """Restrict large variations in the model between iterations.
499 Index of the current subfilter within the full band.
501 The new DCR model for one subfilter
from the current iteration.
502 Values
in ``newModel`` that are extreme compared
with the last
503 iteration are modified
in place.
504 bbox : `lsst.afw.geom.Box2I`
506 regularizationFactor : `float`
507 Maximum relative change of the model allowed between iterations.
508 regularizationWidth : int, optional
509 Minimum radius of a region to include
in regularization,
in pixels.
511 refImage = self[subfilter][bbox].array
512 highThreshold = np.abs(refImage)*regularizationFactor
513 lowThreshold = refImage/regularizationFactor
514 newImage = newModel.array
515 self.applyImageThresholdsapplyImageThresholds(newImage, highThreshold=highThreshold, lowThreshold=lowThreshold,
516 regularizationWidth=regularizationWidth)
519 regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
520 """Restrict large variations in the model between subfilters.
525 The new DCR model images from the current iteration.
526 The values will be modified
in place.
527 bbox : `lsst.afw.geom.Box2I`
530 Statistics control object
for coaddition.
531 regularizationFactor : `float`
532 Maximum relative change of the model allowed between subfilters.
533 regularizationWidth : `int`, optional
534 Minimum radius of a region to include
in regularization,
in pixels.
536 Optional alternate mask
537 convergenceMaskPlanes : `list` of `str`,
or `str`, optional
538 Mask planes to use to calculate convergence.
542 This implementation of frequency regularization restricts each
543 subfilter image to be a smoothly-varying function times a reference
549 maxDiff = np.sqrt(regularizationFactor)
550 noiseLevel = self.
calculateNoiseCutoffcalculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
552 badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
553 if np.sum(~badPixels) == 0:
556 referenceImage[badPixels] = 0.
557 filterWidth = regularizationWidth
558 fwhm = 2.*filterWidth
562 smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth, mode=
'constant')
566 smoothRef += 3.*noiseLevel
568 lowThreshold = smoothRef/maxDiff
569 highThreshold = smoothRef*maxDiff
570 for model
in modelImages:
572 highThreshold=highThreshold,
573 lowThreshold=lowThreshold,
574 regularizationWidth=regularizationWidth)
575 smoothModel = ndimage.filters.gaussian_filter(model.array, filterWidth, mode=
'constant')
576 smoothModel += 3.*noiseLevel
577 relativeModel = smoothModel/smoothRef
580 relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/alpha)
581 relativeModel += alpha*(relativeModel - relativeModel2)
582 model.array = relativeModel*referenceImage
585 convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
586 """Helper function to calculate the background noise level of an image.
591 The input image to evaluate the background noise properties.
593 Statistics control object for coaddition.
595 Number of additional pixels to exclude
596 from the edges of the bounding box.
597 convergenceMaskPlanes : `list` of `str`,
or `str`
598 Mask planes to use to calculate convergence.
600 Optional alternate mask
601 bbox : `lsst.afw.geom.Box2I`, optional
602 Sub-region of the masked image to calculate the noise level over.
606 noiseCutoff : `float`
607 The threshold value to treat pixels
as noise
in an image..
612 mask = self.
maskmask[bbox]
614 bboxShrink.grow(-bufferSize)
615 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
617 backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
618 noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
622 """Restrict image values to be between upper and lower limits.
624 This method flags all pixels in an image that are outside of the given
625 threshold values. The threshold values are taken
from a reference
626 image, so noisy pixels are likely to get flagged. In order to exclude
627 those noisy pixels, the array of flags
is eroded
and dilated, which
628 removes isolated pixels outside of the thresholds
from the list of
629 pixels to be modified. Pixels that remain flagged after this operation
630 have their values set to the appropriate upper
or lower threshold
635 image : `numpy.ndarray`
636 The image to apply the thresholds to.
637 The values will be modified
in place.
638 highThreshold : `numpy.ndarray`, optional
639 Array of upper limit values
for each pixel of ``image``.
640 lowThreshold : `numpy.ndarray`, optional
641 Array of lower limit values
for each pixel of ``image``.
642 regularizationWidth : `int`, optional
643 Minimum radius of a region to include
in regularization,
in pixels.
648 filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
650 if highThreshold
is not None:
651 highPixels = image > highThreshold
652 if regularizationWidth > 0:
654 highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
655 image[highPixels] = highThreshold[highPixels]
656 if lowThreshold
is not None:
657 lowPixels = image < lowThreshold
658 if regularizationWidth > 0:
660 lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
661 image[lowPixels] = lowThreshold[lowPixels]
664def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0.,
665 doPrefilter=True, order=3):
666 """Shift an image along the X and Y directions.
670 image : `numpy.ndarray`
671 The input image to shift.
673 Shift calculated with ``calculateDcr``.
674 Uses numpy axes ordering (Y, X).
675 If ``splitSubfilters``
is set, each element
is itself a `tuple`
676 of two `float`, corresponding to the DCR shift at the two wavelengths.
677 Otherwise, each element
is a `float` corresponding to the DCR shift at
678 the effective wavelength of the subfilter.
679 useInverse : `bool`, optional
680 Apply the shift
in the opposite direction. Default:
False
681 splitSubfilters : `bool`, optional
682 Calculate DCR
for two evenly-spaced wavelengths
in each subfilter,
683 instead of at the midpoint. Default:
False
684 splitThreshold : `float`, optional
685 Minimum DCR difference within a subfilter required to use
687 doPrefilter : `bool`, optional
688 Spline filter the image before shifting,
if set. Filtering
is required,
689 so only set to
False if the image
is already filtered.
690 Filtering takes ~20% of the time of shifting, so
if `applyDcr` will be
691 called repeatedly on the same image it
is more efficient to
692 precalculate the filter.
693 order : `int`, optional
694 The order of the spline interpolation, default
is 3.
698 shiftedImage : `numpy.ndarray`
699 A copy of the input image
with the specified shift applied.
701 if doPrefilter
and order > 1:
702 prefilteredImage = ndimage.spline_filter(image, order=order)
704 prefilteredImage = image
706 shiftAmp = np.max(np.abs([_dcr0 - _dcr1
for _dcr0, _dcr1
in zip(dcr[0], dcr[1])]))
707 if shiftAmp >= splitThreshold:
709 shift = [-1.*s
for s
in dcr[0]]
710 shift1 = [-1.*s
for s
in dcr[1]]
714 shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=
False, order=order)
715 shiftedImage += ndimage.shift(prefilteredImage, shift1, prefilter=
False, order=order)
721 dcr = (np.mean(dcr[0]), np.mean(dcr[1]))
723 shift = [-1.*s
for s
in dcr]
726 shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=
False, order=order)
730def calculateDcr(visitInfo, wcs, effectiveWavelength, bandwidth, dcrNumSubfilters, splitSubfilters=False):
731 """Calculate the shift in pixels of an exposure due to DCR.
736 Metadata for the exposure.
738 Coordinate system definition (wcs)
for the exposure.
739 effectiveWavelength : `float`
740 The effective wavelengths of the current filter,
in nanometers.
742 The bandwidth of the current filter,
in nanometers.
743 dcrNumSubfilters : `int`
744 Number of sub-filters used to model chromatic effects within a band.
745 splitSubfilters : `bool`, optional
746 Calculate DCR
for two evenly-spaced wavelengths
in each subfilter,
747 instead of at the midpoint. Default:
False
751 dcrShift : `tuple` of two `float`
752 The 2D shift due to DCR,
in pixels.
753 Uses numpy axes ordering (Y, X).
757 weight = [0.75, 0.25]
761 diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=effectiveWavelength,
762 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
763 observatory=visitInfo.getObservatory(),
764 weather=visitInfo.getWeather())
765 diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=effectiveWavelength,
766 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
767 observatory=visitInfo.getObservatory(),
768 weather=visitInfo.getWeather())
770 diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
771 diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
772 diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
773 diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
774 shiftX = [diffRefractPix*np.sin(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
775 shiftY = [diffRefractPix*np.cos(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
776 dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1])))
778 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
779 diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
780 shiftX = diffRefractPix*np.sin(rotation.asRadians())
781 shiftY = diffRefractPix*np.cos(rotation.asRadians())
782 dcrShift.append((shiftY, shiftX))
787 """Calculate the total sky rotation angle of an exposure.
792 Metadata for the exposure.
794 Coordinate system definition (wcs)
for the exposure.
799 The rotation of the image axis, East
from North.
800 Equal to the parallactic angle plus any additional rotation of the
802 A rotation angle of 0 degrees
is defined
with
803 North along the +y axis
and East along the +x axis.
804 A rotation angle of 90 degrees
is defined
with
805 North along the +x axis
and East along the -y axis.
807 parAngle = visitInfo.getBoresightParAngle().asRadians()
808 cd = wcs.getCdMatrix()
810 cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
811 rotAngle = (cdAngle + parAngle)*geom.radians
813 cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
814 rotAngle = (cdAngle - parAngle)*geom.radians
819 """Iterate over the wavelength endpoints of subfilters.
823 effectiveWavelength : `float`
824 The effective wavelength of the current filter, in nanometers.
826 The bandwidth of the current filter,
in nanometers.
827 dcrNumSubfilters : `int`
828 Number of sub-filters used to model chromatic effects within a band.
832 `tuple` of two `float`
833 The next set of wavelength endpoints
for a subfilter,
in nanometers.
835 lambdaMin = effectiveWavelength - bandwidth/2
836 lambdaMax = effectiveWavelength + bandwidth/2
837 wlStep = bandwidth/dcrNumSubfilters
838 for wl
in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=
False):
839 yield (wl, wl + wlStep)
def fromImage(cls, maskedImage, dcrNumSubfilters, effectiveWavelength, bandwidth, wcs=None, filterLabel=None, psf=None, photoCalib=None)
def calculateNoiseCutoff(self, image, statsCtrl, bufferSize, convergenceMaskPlanes="DETECTED", mask=None, bbox=None)
def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2)
def __init__(self, modelImages, effectiveWavelength, bandwidth, filterLabel=None, psf=None, bbox=None, wcs=None, mask=None, variance=None, photoCalib=None)
def getReferenceImage(self, bbox=None)
def conditionDcrModel(self, modelImages, bbox, gain=1.)
def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, regularizationWidth=2)
def assign(self, dcrSubModel, bbox=None)
def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor, regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED")
def __setitem__(self, subfilter, maskedImage)
def buildMatchedExposure(self, exposure=None, visitInfo=None, bbox=None, mask=None)
def __getitem__(self, subfilter)
def fromQuantum(cls, availableCoaddRefs, effectiveWavelength, bandwidth)
def buildMatchedTemplate(self, exposure=None, order=3, visitInfo=None, bbox=None, mask=None, splitSubfilters=True, splitThreshold=0., amplifyModel=1.)
def effectiveWavelength(self)
def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0., doPrefilter=True, order=3)
def wavelengthGenerator(effectiveWavelength, bandwidth, dcrNumSubfilters)
def calculateImageParallacticAngle(visitInfo, wcs)
def calculateDcr(visitInfo, wcs, effectiveWavelength, bandwidth, dcrNumSubfilters, splitSubfilters=False)