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, effectiveWavelength, bandwidth, filterInfo=None, psf=None,
53 mask=None, variance=None, photoCalib=None):
65 def fromImage(cls, maskedImage, dcrNumSubfilters, effectiveWavelength, bandwidth,
66 filterInfo=None, psf=None, photoCalib=None):
67 """Initialize a DcrModel by dividing a coadd between the subfilters.
71 maskedImage : `lsst.afw.image.MaskedImage`
72 Input coadded image to divide equally between the subfilters.
73 dcrNumSubfilters : `int`
74 Number of sub-filters used to model chromatic effects within a
76 effectiveWavelength : `float`
77 The effective wavelengths of the current filter, in nanometers.
79 The bandwidth of the current filter, in nanometers.
80 filterInfo : `lsst.afw.image.Filter`, optional
81 The filter definition, set in the current instruments' obs package.
82 Note: this object will be changed in DM-21333.
83 Required for any calculation of DCR, including making matched
85 psf : `lsst.afw.detection.Psf`, optional
86 Point spread function (PSF) of the model.
87 Required if the ``DcrModel`` will be persisted.
88 photoCalib : `lsst.afw.image.PhotoCalib`, optional
89 Calibration to convert instrumental flux and
90 flux error to nanoJansky.
94 dcrModel : `lsst.pipe.tasks.DcrModel`
95 Best fit model of the true sky after correcting chromatic effects.
99 model = maskedImage.image.clone()
100 mask = maskedImage.mask.clone()
106 variance = maskedImage.variance.clone()
107 variance /= dcrNumSubfilters
108 model /= dcrNumSubfilters
109 modelImages = [model, ]
110 for subfilter
in range(1, dcrNumSubfilters):
111 modelImages.append(model.clone())
112 return cls(modelImages, effectiveWavelength, bandwidth,
113 filterInfo=filterInfo, psf=psf, mask=mask, variance=variance, photoCalib=photoCalib)
116 def fromDataRef(cls, dataRef, effectiveWavelength, bandwidth, datasetType="dcrCoadd", numSubfilters=None,
118 """Load an existing DcrModel from a Gen 2 repository.
122 dataRef : `lsst.daf.persistence.ButlerDataRef`
123 Data reference defining the patch for coaddition and the
125 effectiveWavelength : `float`
126 The effective wavelengths of the current filter, in nanometers.
128 The bandwidth of the current filter, in nanometers.
129 datasetType : `str`, optional
130 Name of the DcrModel in the registry {"dcrCoadd", "dcrCoadd_sub"}
131 numSubfilters : `int`
132 Number of sub-filters used to model chromatic effects within a
135 Additional keyword arguments to pass to look up the model in the
137 Common keywords and their types include: ``tract``:`str`,
138 ``patch``:`str`, ``bbox``:`lsst.afw.geom.Box2I`
142 dcrModel : `lsst.pipe.tasks.DcrModel`
143 Best fit model of the true sky after correcting chromatic effects.
151 if "subfilter" in kwargs:
152 kwargs.pop(
"subfilter")
153 for subfilter
in range(numSubfilters):
154 dcrCoadd = dataRef.get(datasetType, subfilter=subfilter,
155 numSubfilters=numSubfilters, **kwargs)
156 if filterInfo
is None:
157 filterInfo = dcrCoadd.getFilter()
159 psf = dcrCoadd.getPsf()
163 variance = dcrCoadd.variance
164 if photoCalib
is None:
165 photoCalib = dcrCoadd.getPhotoCalib()
166 modelImages.append(dcrCoadd.image)
167 return cls(modelImages, effectiveWavelength, bandwidth, filterInfo, psf, mask, variance, photoCalib)
170 def fromQuantum(cls, availableCoaddRefs, effectiveWavelength, bandwidth):
171 """Load an existing DcrModel from a Gen 3 repository.
175 availableCoaddRefs : `dict` of
176 `int` : `lsst.daf.butler.DeferredDatasetHandle`
177 Dictionary of spatially relevant retrieved coadd patches,
178 indexed by their sequential patch number.
179 effectiveWavelength : `float`
180 The effective wavelengths of the current filter, in nanometers.
182 The bandwidth of the current filter, in nanometers.
186 dcrModel : `lsst.pipe.tasks.DcrModel`
187 Best fit model of the true sky after correcting chromatic effects.
194 modelImages = [
None]*len(availableCoaddRefs)
196 for coaddRef
in availableCoaddRefs:
197 subfilter = coaddRef.dataId[
"subfilter"]
198 dcrCoadd = coaddRef.get()
199 if filterInfo
is None:
200 filterInfo = dcrCoadd.getFilter()
202 psf = dcrCoadd.getPsf()
206 variance = dcrCoadd.variance
207 if photoCalib
is None:
208 photoCalib = dcrCoadd.getPhotoCalib()
209 modelImages[subfilter] = dcrCoadd.image
210 return cls(modelImages, effectiveWavelength, bandwidth, filterInfo, psf, mask, variance, photoCalib)
213 """Return the number of subfilters.
217 dcrNumSubfilters : `int`
218 The number of DCR subfilters in the model.
223 """Iterate over the subfilters of the DCR model.
228 Index of the current ``subfilter`` within the full band.
229 Negative indices are allowed, and count in reverse order
230 from the highest ``subfilter``.
234 modelImage : `lsst.afw.image.Image`
235 The DCR model for the given ``subfilter``.
240 If the requested ``subfilter`` is greater or equal to the number
241 of subfilters in the model.
243 if np.abs(subfilter) >= len(self):
244 raise IndexError(
"subfilter out of bounds.")
248 """Update the model image for one subfilter.
253 Index of the current subfilter within the full band.
254 maskedImage : `lsst.afw.image.Image`
255 The DCR model to set for the given ``subfilter``.
260 If the requested ``subfilter`` is greater or equal to the number
261 of subfilters in the model.
263 If the bounding box of the new image does not match.
265 if np.abs(subfilter) >= len(self):
266 raise IndexError(
"subfilter out of bounds.")
267 if maskedImage.getBBox() != self.
bbox:
268 raise ValueError(
"The bounding box of a subfilter must not change.")
273 """Return the effective wavelength of the model.
277 effectiveWavelength : `float`
278 The effective wavelength of the current filter, in nanometers.
284 """Return the filter label for the model.
288 filterInfo : `lsst.afw.image.Filter`
289 The name of the filter used for the input observations.
290 Note: this object will be changed in DM-21333.
296 """Return the bandwidth of the model.
301 The bandwidth of the current filter, in nanometers.
307 """Return the psf of the model.
311 psf : `lsst.afw.detection.Psf`
312 Point spread function (PSF) of the model.
318 """Return the common bounding box of each subfilter image.
322 bbox : `lsst.afw.geom.Box2I`
323 Bounding box of the DCR model.
325 return self[0].getBBox()
329 """Return the common mask of each subfilter image.
333 mask : `lsst.afw.image.Mask`
334 Mask plane of the DCR model.
340 """Return the common variance of each subfilter image.
344 variance : `lsst.afw.image.Image`
345 Variance plane of the DCR model.
350 """Calculate a reference image from the average of the subfilter
355 bbox : `lsst.afw.geom.Box2I`, optional
356 Sub-region of the coadd. Returns the entire image if `None`.
360 refImage : `numpy.ndarray`
361 The reference image with no chromatic effects applied.
363 bbox = bbox
or self.
bbox
364 return np.mean([model[bbox].array
for model
in self], axis=0)
366 def assign(self, dcrSubModel, bbox=None):
367 """Update a sub-region of the ``DcrModel`` with new values.
371 dcrSubModel : `lsst.pipe.tasks.DcrModel`
372 New model of the true scene after correcting chromatic effects.
373 bbox : `lsst.afw.geom.Box2I`, optional
374 Sub-region of the coadd.
375 Defaults to the bounding box of ``dcrSubModel``.
380 If the new model has a different number of subfilters.
382 if len(dcrSubModel) != len(self):
383 raise ValueError(
"The number of DCR subfilters must be the same "
384 "between the old and new models.")
385 bbox = bbox
or self.
bbox
386 for model, subModel
in zip(self, dcrSubModel):
387 model.assign(subModel[bbox], bbox)
390 visitInfo=None, bbox=None, wcs=None, mask=None,
391 splitSubfilters=True, splitThreshold=0., amplifyModel=1.):
392 """Create a DCR-matched template image for an exposure.
396 exposure : `lsst.afw.image.Exposure`, optional
397 The input exposure to build a matched template for.
398 May be omitted if all of the metadata is supplied separately
399 order : `int`, optional
400 Interpolation order of the DCR shift.
401 visitInfo : `lsst.afw.image.VisitInfo`, optional
402 Metadata for the exposure. Ignored if ``exposure`` is set.
403 bbox : `lsst.afw.geom.Box2I`, optional
404 Sub-region of the coadd. Ignored if ``exposure`` is set.
405 wcs : `lsst.afw.geom.SkyWcs`, optional
406 Coordinate system definition (wcs) for the exposure.
407 Ignored if ``exposure`` is set.
408 mask : `lsst.afw.image.Mask`, optional
409 reference mask to use for the template image.
410 splitSubfilters : `bool`, optional
411 Calculate DCR for two evenly-spaced wavelengths in each subfilter,
412 instead of at the midpoint. Default: True
413 splitThreshold : `float`, optional
414 Minimum DCR difference within a subfilter required to use
416 amplifyModel : `float`, optional
417 Multiplication factor to amplify differences between model planes.
418 Used to speed convergence of iterative forward modeling.
422 templateImage : `lsst.afw.image.ImageF`
423 The DCR-matched template
428 If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and
432 raise ValueError(
"'effectiveWavelength' and 'bandwidth' must be set for the DcrModel in order "
434 if exposure
is not None:
435 visitInfo = exposure.getInfo().getVisitInfo()
436 bbox = exposure.getBBox()
437 wcs = exposure.getInfo().getWcs()
438 elif visitInfo
is None or bbox
is None or wcs
is None:
439 raise ValueError(
"Either exposure or visitInfo, bbox, and wcs must be set.")
441 splitSubfilters=splitSubfilters)
442 templateImage = afwImage.ImageF(bbox)
444 for subfilter, dcr
in enumerate(dcrShift):
446 model = (self[subfilter][bbox].array - refModel)*amplifyModel + refModel
448 model = self[subfilter][bbox].array
449 templateImage.array +=
applyDcr(model, dcr, splitSubfilters=splitSubfilters,
450 splitThreshold=splitThreshold, order=order)
454 visitInfo=None, bbox=None, wcs=None, mask=None):
455 """Wrapper to create an exposure from a template image.
459 exposure : `lsst.afw.image.Exposure`, optional
460 The input exposure to build a matched template for.
461 May be omitted if all of the metadata is supplied separately
462 visitInfo : `lsst.afw.image.VisitInfo`, optional
463 Metadata for the exposure. Ignored if ``exposure`` is set.
464 bbox : `lsst.afw.geom.Box2I`, optional
465 Sub-region of the coadd. Ignored if ``exposure`` is set.
466 wcs : `lsst.afw.geom.SkyWcs`, optional
467 Coordinate system definition (wcs) for the exposure.
468 Ignored if ``exposure`` is set.
469 mask : `lsst.afw.image.Mask`, optional
470 reference mask to use for the template image.
474 templateExposure : `lsst.afw.image.exposureF`
475 The DCR-matched template
480 If no `photcCalib` is set.
483 bbox = exposure.getBBox()
485 bbox=bbox, wcs=wcs, mask=mask)
486 maskedImage = afwImage.MaskedImageF(bbox)
487 maskedImage.image = templateImage[bbox]
488 maskedImage.mask = self.
mask[bbox]
489 maskedImage.variance = self.
variance[bbox]
493 templateExposure = afwImage.ExposureF(bbox, wcs)
494 templateExposure.setMaskedImage(maskedImage[bbox])
495 templateExposure.setPsf(self.
psf)
496 templateExposure.setFilter(self.filterInfo)
498 raise RuntimeError(
"No PhotoCalib set for the DcrModel. "
499 "If the DcrModel was created from a masked image"
500 " you must also specify the photoCalib.")
501 templateExposure.setPhotoCalib(self.
photoCalib)
502 return templateExposure
505 """Average two iterations' solutions to reduce oscillations.
509 modelImages : `list` of `lsst.afw.image.Image`
510 The new DCR model images from the current iteration.
511 The values will be modified in place.
512 bbox : `lsst.afw.geom.Box2I`
513 Sub-region of the coadd
514 gain : `float`, optional
515 Relative weight to give the new solution when updating the model.
516 Defaults to 1.0, which gives equal weight to both solutions.
519 for model, newModel
in zip(self, modelImages):
521 newModel += model[bbox]
522 newModel /= 1. + gain
525 regularizationWidth=2):
526 """Restrict large variations in the model between iterations.
531 Index of the current subfilter within the full band.
532 newModel : `lsst.afw.image.Image`
533 The new DCR model for one subfilter from the current iteration.
534 Values in ``newModel`` that are extreme compared with the last
535 iteration are modified in place.
536 bbox : `lsst.afw.geom.Box2I`
538 regularizationFactor : `float`
539 Maximum relative change of the model allowed between iterations.
540 regularizationWidth : int, optional
541 Minimum radius of a region to include in regularization, in pixels.
543 refImage = self[subfilter][bbox].array
544 highThreshold = np.abs(refImage)*regularizationFactor
545 lowThreshold = refImage/regularizationFactor
546 newImage = newModel.array
548 regularizationWidth=regularizationWidth)
551 regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
552 """Restrict large variations in the model between subfilters.
556 modelImages : `list` of `lsst.afw.image.Image`
557 The new DCR model images from the current iteration.
558 The values will be modified in place.
559 bbox : `lsst.afw.geom.Box2I`
561 statsCtrl : `lsst.afw.math.StatisticsControl`
562 Statistics control object for coaddition.
563 regularizationFactor : `float`
564 Maximum relative change of the model allowed between subfilters.
565 regularizationWidth : `int`, optional
566 Minimum radius of a region to include in regularization, in pixels.
567 mask : `lsst.afw.image.Mask`, optional
568 Optional alternate mask
569 convergenceMaskPlanes : `list` of `str`, or `str`, optional
570 Mask planes to use to calculate convergence.
574 This implementation of frequency regularization restricts each
575 subfilter image to be a smoothly-varying function times a reference
581 maxDiff = np.sqrt(regularizationFactor)
582 noiseLevel = self.
calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
584 badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
585 if np.sum(~badPixels) == 0:
588 referenceImage[badPixels] = 0.
589 filterWidth = regularizationWidth
590 fwhm = 2.*filterWidth
594 smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth, mode=
'constant')
598 smoothRef += 3.*noiseLevel
600 lowThreshold = smoothRef/maxDiff
601 highThreshold = smoothRef*maxDiff
602 for model
in modelImages:
604 highThreshold=highThreshold,
605 lowThreshold=lowThreshold,
606 regularizationWidth=regularizationWidth)
607 smoothModel = ndimage.filters.gaussian_filter(model.array, filterWidth, mode=
'constant')
608 smoothModel += 3.*noiseLevel
609 relativeModel = smoothModel/smoothRef
612 relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/alpha)
613 relativeModel += alpha*(relativeModel - relativeModel2)
614 model.array = relativeModel*referenceImage
617 convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
618 """Helper function to calculate the background noise level of an image.
622 image : `lsst.afw.image.Image`
623 The input image to evaluate the background noise properties.
624 statsCtrl : `lsst.afw.math.StatisticsControl`
625 Statistics control object for coaddition.
627 Number of additional pixels to exclude
628 from the edges of the bounding box.
629 convergenceMaskPlanes : `list` of `str`, or `str`
630 Mask planes to use to calculate convergence.
631 mask : `lsst.afw.image.Mask`, Optional
632 Optional alternate mask
633 bbox : `lsst.afw.geom.Box2I`, optional
634 Sub-region of the masked image to calculate the noise level over.
638 noiseCutoff : `float`
639 The threshold value to treat pixels as noise in an image..
644 mask = self.
mask[bbox]
646 bboxShrink.grow(-bufferSize)
647 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
649 backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
650 noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
654 """Restrict image values to be between upper and lower limits.
656 This method flags all pixels in an image that are outside of the given
657 threshold values. The threshold values are taken from a reference
658 image, so noisy pixels are likely to get flagged. In order to exclude
659 those noisy pixels, the array of flags is eroded and dilated, which
660 removes isolated pixels outside of the thresholds from the list of
661 pixels to be modified. Pixels that remain flagged after this operation
662 have their values set to the appropriate upper or lower threshold
667 image : `numpy.ndarray`
668 The image to apply the thresholds to.
669 The values will be modified in place.
670 highThreshold : `numpy.ndarray`, optional
671 Array of upper limit values for each pixel of ``image``.
672 lowThreshold : `numpy.ndarray`, optional
673 Array of lower limit values for each pixel of ``image``.
674 regularizationWidth : `int`, optional
675 Minimum radius of a region to include in regularization, in pixels.
680 filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
682 if highThreshold
is not None:
683 highPixels = image > highThreshold
684 if regularizationWidth > 0:
686 highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
687 image[highPixels] = highThreshold[highPixels]
688 if lowThreshold
is not None:
689 lowPixels = image < lowThreshold
690 if regularizationWidth > 0:
692 lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
693 image[lowPixels] = lowThreshold[lowPixels]
696 def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0.,
697 doPrefilter=True, order=3):
698 """Shift an image along the X and Y directions.
702 image : `numpy.ndarray`
703 The input image to shift.
705 Shift calculated with ``calculateDcr``.
706 Uses numpy axes ordering (Y, X).
707 If ``splitSubfilters`` is set, each element is itself a `tuple`
708 of two `float`, corresponding to the DCR shift at the two wavelengths.
709 Otherwise, each element is a `float` corresponding to the DCR shift at
710 the effective wavelength of the subfilter.
711 useInverse : `bool`, optional
712 Apply the shift in the opposite direction. Default: False
713 splitSubfilters : `bool`, optional
714 Calculate DCR for two evenly-spaced wavelengths in each subfilter,
715 instead of at the midpoint. Default: False
716 splitThreshold : `float`, optional
717 Minimum DCR difference within a subfilter required to use
719 doPrefilter : `bool`, optional
720 Spline filter the image before shifting, if set. Filtering is required,
721 so only set to False if the image is already filtered.
722 Filtering takes ~20% of the time of shifting, so if `applyDcr` will be
723 called repeatedly on the same image it is more efficient to
724 precalculate the filter.
725 order : `int`, optional
726 The order of the spline interpolation, default is 3.
730 shiftedImage : `numpy.ndarray`
731 A copy of the input image with the specified shift applied.
734 prefilteredImage = ndimage.spline_filter(image, order=order)
736 prefilteredImage = image
738 shiftAmp = np.max(np.abs([_dcr0 - _dcr1
for _dcr0, _dcr1
in zip(dcr[0], dcr[1])]))
739 if shiftAmp >= splitThreshold:
741 shift = [-1.*s
for s
in dcr[0]]
742 shift1 = [-1.*s
for s
in dcr[1]]
746 shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=
False, order=order)
747 shiftedImage += ndimage.shift(prefilteredImage, shift1, prefilter=
False, order=order)
753 dcr = (np.mean(dcr[0]), np.mean(dcr[1]))
755 shift = [-1.*s
for s
in dcr]
758 shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=
False, order=order)
762 def calculateDcr(visitInfo, wcs, effectiveWavelength, bandwidth, dcrNumSubfilters, splitSubfilters=False):
763 """Calculate the shift in pixels of an exposure due to DCR.
767 visitInfo : `lsst.afw.image.VisitInfo`
768 Metadata for the exposure.
769 wcs : `lsst.afw.geom.SkyWcs`
770 Coordinate system definition (wcs) for the exposure.
771 effectiveWavelength : `float`
772 The effective wavelengths of the current filter, in nanometers.
774 The bandwidth of the current filter, in nanometers.
775 dcrNumSubfilters : `int`
776 Number of sub-filters used to model chromatic effects within a band.
777 splitSubfilters : `bool`, optional
778 Calculate DCR for two evenly-spaced wavelengths in each subfilter,
779 instead of at the midpoint. Default: False
783 dcrShift : `tuple` of two `float`
784 The 2D shift due to DCR, in pixels.
785 Uses numpy axes ordering (Y, X).
789 weight = [0.75, 0.25]
794 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
795 observatory=visitInfo.getObservatory(),
796 weather=visitInfo.getWeather())
798 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
799 observatory=visitInfo.getObservatory(),
800 weather=visitInfo.getWeather())
802 diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
803 diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
804 diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
805 diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
806 shiftX = [diffRefractPix*np.sin(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
807 shiftY = [diffRefractPix*np.cos(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
808 dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1])))
810 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
811 diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
812 shiftX = diffRefractPix*np.sin(rotation.asRadians())
813 shiftY = diffRefractPix*np.cos(rotation.asRadians())
814 dcrShift.append((shiftY, shiftX))
819 """Calculate the total sky rotation angle of an exposure.
823 visitInfo : `lsst.afw.image.VisitInfo`
824 Metadata for the exposure.
825 wcs : `lsst.afw.geom.SkyWcs`
826 Coordinate system definition (wcs) for the exposure.
831 The rotation of the image axis, East from North.
832 Equal to the parallactic angle plus any additional rotation of the
834 A rotation angle of 0 degrees is defined with
835 North along the +y axis and East along the +x axis.
836 A rotation angle of 90 degrees is defined with
837 North along the +x axis and East along the -y axis.
839 parAngle = visitInfo.getBoresightParAngle().asRadians()
840 cd = wcs.getCdMatrix()
842 cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
843 rotAngle = (cdAngle + parAngle)*geom.radians
845 cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
846 rotAngle = (cdAngle - parAngle)*geom.radians
851 """Iterate over the wavelength endpoints of subfilters.
855 effectiveWavelength : `float`
856 The effective wavelength of the current filter, in nanometers.
858 The bandwidth of the current filter, in nanometers.
859 dcrNumSubfilters : `int`
860 Number of sub-filters used to model chromatic effects within a band.
864 `tuple` of two `float`
865 The next set of wavelength endpoints for a subfilter, in nanometers.
867 lambdaMin = effectiveWavelength - bandwidth/2
868 lambdaMax = effectiveWavelength + bandwidth/2
869 wlStep = bandwidth/dcrNumSubfilters
870 for wl
in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=
False):
871 yield (wl, wl + wlStep)