22from __future__
import annotations
26from typing
import TYPE_CHECKING, Iterable
32from lsst.daf.butler
import DataCoordinate, DeferredDatasetHandle
36 CloughTocher2DInterpolateTask,
49 PipelineTaskConnections,
52from lsst.pipe.base.connectionTypes
import Input, Output
57from .coaddInputRecorder
import CoaddInputRecorderTask
66 "MakeDirectWarpConfig",
73 """Inputs passed to `MakeDirectWarpTask.run` for a single detector.
76 exposure_or_handle: ExposureF | DeferredDatasetHandle | InMemoryDatasetHandle
77 """Detector image with initial calibration objects, or a deferred-load
81 data_id: DataCoordinate
82 """Butler data ID for this detector."""
84 background_revert: BackgroundList |
None =
None
85 """Background model to restore in (i.e. add to) the image."""
87 background_apply: BackgroundList |
None =
None
88 """Background model to apply to (i.e. subtract from) the image."""
92 """Get the exposure object, loading it if necessary."""
99 """Apply (subtract) the `background_apply` to the exposure in-place.
104 Raised if `background_apply` is None.
107 raise RuntimeError(
"No background to apply")
114 """Revert (add) the `background_revert` from the exposure in-place.
119 Raised if `background_revert` is None.
122 raise RuntimeError(
"No background to revert")
130 PipelineTaskConnections,
131 dimensions=(
"tract",
"patch",
"skymap",
"instrument",
"visit"),
136 """Connections for MakeWarpTask"""
139 doc=
"Input exposures to be interpolated and resampled onto a SkyMap "
142 storageClass=
"ExposureF",
143 dimensions=(
"instrument",
"visit",
"detector"),
147 background_revert_list = Input(
148 doc=
"Background to be reverted (i.e., added back to the calexp). "
149 "This connection is used only if doRevertOldBackground=False.",
150 name=
"calexpBackground",
151 storageClass=
"Background",
152 dimensions=(
"instrument",
"visit",
"detector"),
155 background_apply_list = Input(
156 doc=
"Background to be applied (subtracted from the calexp). "
157 "This is used only if doApplyNewBackground=True.",
159 storageClass=
"Background",
160 dimensions=(
"instrument",
"visit",
"detector"),
163 visit_summary = Input(
164 doc=
"Input visit-summary catalog with updated calibration objects.",
165 name=
"finalVisitSummary",
166 storageClass=
"ExposureCatalog",
167 dimensions=(
"instrument",
"visit"),
170 doc=
"Input definition of geometry/bbox and projection/wcs for warps.",
171 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
172 storageClass=
"SkyMap",
173 dimensions=(
"skymap",),
177 doc=
"Output direct warped exposure produced by resampling calexps "
178 "onto the skyMap patch geometry.",
179 name=
"{coaddName}Coadd_directWarp",
180 storageClass=
"ExposureF",
181 dimensions=(
"tract",
"patch",
"skymap",
"instrument",
"visit"),
183 masked_fraction_warp = Output(
184 doc=
"Output masked fraction warped exposure.",
185 name=
"{coaddName}Coadd_directWarp_maskedFraction",
186 storageClass=
"ImageF",
187 dimensions=(
"tract",
"patch",
"skymap",
"instrument",
"visit"),
190 def __init__(self, *, config=None):
191 super().__init__(config=config)
195 if not config.doRevertOldBackground:
196 del self.background_revert_list
197 if not config.doApplyNewBackground:
198 del self.background_apply_list
200 if not config.doWarpMaskedFraction:
201 del self.masked_fraction_warp
205 for n
in range(config.numberOfNoiseRealizations):
207 doc=f
"Output direct warped noise exposure ({n})",
208 name=f
"{config.connections.coaddName}Coadd_directWarp_noise{n}",
210 storageClass=
"MaskedImageF",
211 dimensions=(
"tract",
"patch",
"skymap",
"instrument",
"visit"),
213 setattr(self, f
"noise_warp{n}", noise_warp)
216class MakeDirectWarpConfig(
218 pipelineConnections=MakeDirectWarpConnections,
220 """Configuration for the MakeDirectWarpTask.
222 The config fields are as similar as possible to the corresponding fields in
227 The config fields are in camelCase to match the fields in the earlier
228 version of the makeWarp task as closely as possible.
231 MAX_NUMBER_OF_NOISE_REALIZATIONS = 3
233 numberOfNoiseRealizations is defined as a RangeField to prevent from
234 making multiple output connections and blowing up the memory usage by
235 accident. An upper bound of 3 is based on the best guess of the maximum
236 number of noise realizations that will be used for metadetection.
239 numberOfNoiseRealizations = RangeField[int](
240 doc=
"Number of noise realizations to simulate and persist.",
243 max=MAX_NUMBER_OF_NOISE_REALIZATIONS,
246 seedOffset = Field[int](
247 doc=
"Offset to the seed used for the noise realization. This can be "
248 "used to create a different noise realization if the default ones "
249 "are catastrophic, or for testing sensitivity to the noise.",
252 useMedianVariance = Field[bool](
253 doc=
"Use the median of variance plane in the input calexp to generate "
254 "noise realizations? If False, per-pixel variance will be used.",
257 doRevertOldBackground = Field[bool](
258 doc=
"Revert the old backgrounds from the `background_revert_list` "
262 doApplyNewBackground = Field[bool](
263 doc=
"Apply the new backgrounds from the `background_apply_list` "
267 useVisitSummaryPsf = Field[bool](
268 doc=
"If True, use the PSF model and aperture corrections from the "
269 "'visit_summary' connection to make the warp. If False, use the "
270 "PSF model and aperture corrections from the 'calexp' connection.",
273 doSelectPreWarp = Field[bool](
274 doc=
"Select ccds before warping?",
278 doc=
"Image selection subtask.",
279 target=PsfWcsSelectImagesTask,
281 doPreWarpInterpolation = Field[bool](
282 doc=
"Interpolate over bad pixels before warping?",
286 doc=
"Interpolation task to use for pre-warping interpolation",
287 target=CloughTocher2DInterpolateTask,
290 doc=
"Subtask that helps fill CoaddInputs catalogs added to the final "
292 target=CoaddInputRecorderTask,
294 includeCalibVar = Field[bool](
295 doc=
"Add photometric calibration variance to warp variance plane?",
299 doc=
"Pad the patch boundary of the warp by these many pixels, so as to allow for PSF-matching later",
303 doc=
"Configuration for the warper that warps the image and noise",
304 dtype=Warper.ConfigClass,
306 doWarpMaskedFraction = Field[bool](
307 doc=
"Warp the masked fraction image?",
311 doc=
"Configuration for the warp that warps the mask fraction image",
312 dtype=Warper.ConfigClass,
315 doc=
"Configuration for CoaddPsf",
316 dtype=CoaddPsfConfig,
318 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
323 def bgSubtracted(self) -> bool:
324 return not self.doRevertOldBackground
327 def bgSubtracted(self, value: bool) ->
None:
328 self.doRevertOldBackground =
not value
331 def doApplySkyCorr(self) -> bool:
332 return self.doApplyNewBackground
334 @doApplySkyCorr.setter
335 def doApplySkyCorr(self, value: bool) ->
None:
336 self.doApplyNewBackground = value
338 def setDefaults(self) -> None:
339 super().setDefaults()
340 self.warper.warpingKernelName =
"lanczos3"
341 self.warper.cacheSize = 0
342 self.maskedFractionWarper.warpingKernelName =
"bilinear"
345class MakeDirectWarpTask(PipelineTask):
346 """Warp single-detector images onto a common projection.
348 This task iterates over multiple images (corresponding to different
349 detectors) from a single visit that overlap the target patch. Pixels that
350 receive no input from any detector are set to NaN in the output image, and
351 NO_DATA bit is set in the mask plane.
353 This differs from the standard `MakeWarp` Task in the following
356 1. No selection on ccds at the time of warping. This is done later during
357 the coaddition stage.
358 2. Interpolate over a set of masked pixels before warping.
359 3. Generate an image where each pixel denotes how much of the pixel is
361 4. Generate multiple noise warps with the same interpolation applied.
362 5. No option to produce a PSF-matched warp.
365 ConfigClass = MakeDirectWarpConfig
366 _DefaultName =
"makeDirectWarp"
368 def __init__(self, **kwargs):
369 super().__init__(**kwargs)
370 self.makeSubtask(
"inputRecorder")
371 self.makeSubtask(
"preWarpInterpolation")
372 if self.config.doSelectPreWarp:
373 self.makeSubtask(
"select")
375 self.warper = Warper.fromConfig(self.config.warper)
376 if self.config.doWarpMaskedFraction:
377 self.maskedFractionWarper = Warper.fromConfig(self.config.maskedFractionWarper)
379 def runQuantum(self, butlerQC, inputRefs, outputRefs):
382 inputs: Mapping[int, WarpDetectorInputs] = {}
383 for handle
in butlerQC.get(inputRefs.calexp_list):
385 exposure_or_handle=handle,
386 data_id=handle.dataId,
390 raise NoWorkFound(
"No input warps provided for co-addition")
395 for ref
in getattr(inputRefs,
"background_revert_list", []):
396 inputs[ref.dataId[
"detector"]].background_revert = butlerQC.get(ref)
397 for ref
in getattr(inputRefs,
"background_apply_list", []):
398 inputs[ref.dataId[
"detector"]].background_apply = butlerQC.get(ref)
400 visit_summary = butlerQC.get(inputRefs.visit_summary)
401 sky_map = butlerQC.get(inputRefs.sky_map)
403 quantumDataId = butlerQC.quantum.dataId
404 sky_info = makeSkyInfo(
406 tractId=quantumDataId[
"tract"],
407 patchId=quantumDataId[
"patch"],
410 results = self.run(inputs, sky_info, visit_summary=visit_summary)
411 butlerQC.put(results, outputRefs)
413 def _preselect_inputs(
415 inputs: Mapping[int, WarpDetectorInputs],
417 visit_summary: ExposureCatalog,
418 ) -> dict[int, WarpDetectorInputs]:
419 """Filter the list of inputs via a 'select' subtask.
423 inputs : ``~collections.abc.Mapping` [ `int`, `WarpDetectorInputs` ]
424 Per-detector input structs.
425 sky_info : `lsst.pipe.base.Struct`
426 Structure with information about the tract and patch.
427 visit_summary : `~lsst.afw.table.ExposureCatalog`
428 Record with updated calibration information for the full visit.
432 filtered_inputs : `dict` [ `int`, `WarpDetectorInputs` ]
433 Like ``inputs``, with rejected detectors dropped.
435 data_id_list, bbox_list, wcs_list = [], [], []
436 for detector_id, detector_inputs
in inputs.items():
437 row = visit_summary.find(detector_id)
439 raise RuntimeError(f
"Unexpectedly incomplete visit_summary: {detector_id=} is missing.")
440 data_id_list.append(detector_inputs.data_id)
441 bbox_list.append(row.getBBox())
442 wcs_list.append(row.getWcs())
444 cornerPosList =
Box2D(sky_info.bbox).getCorners()
445 coordList = [sky_info.wcs.pixelToSky(pos)
for pos
in cornerPosList]
447 good_indices = self.select.run(
450 visitSummary=visit_summary,
452 dataIds=data_id_list,
454 detector_ids = list(inputs.keys())
455 good_detector_ids = [detector_ids[i]
for i
in good_indices]
456 return {detector_id: inputs[detector_id]
for detector_id
in good_detector_ids}
458 def run(self, inputs: Mapping[int, WarpDetectorInputs], sky_info, visit_summary) -> Struct:
459 """Create a Warp dataset from inputs.
463 inputs : `Mapping` [ `int`, `WarpDetectorInputs` ]
464 Dictionary of input datasets, with the detector id being the key.
465 sky_info : `~lsst.pipe.base.Struct`
466 A Struct object containing wcs, bounding box, and other information
467 about the patches within the tract.
468 visit_summary : `~lsst.afw.table.ExposureCatalog` | None
469 Table of visit summary information. If provided, the visit summary
470 information will be used to update the calibration of the input
471 exposures. If None, the input exposures will be used as-is.
475 results : `~lsst.pipe.base.Struct`
476 A Struct object containing the warped exposure, noise exposure(s),
477 and masked fraction image.
480 if self.config.doSelectPreWarp:
481 inputs = self._preselect_inputs(inputs, sky_info, visit_summary=visit_summary)
483 raise NoWorkFound(
"No input warps remain after selection for co-addition")
485 sky_info.bbox.grow(self.config.border)
486 target_bbox, target_wcs = sky_info.bbox, sky_info.wcs
489 final_warp = ExposureF(target_bbox, target_wcs)
491 for _, warp_detector_input
in inputs.items():
492 visit_id = warp_detector_input.data_id[
"visit"]
500 final_warp = self._prepareEmptyExposure(sky_info)
501 final_masked_fraction_warp = self._prepareEmptyExposure(sky_info)
502 final_noise_warps = {
503 n_noise: self._prepareEmptyExposure(sky_info)
504 for n_noise
in range(self.config.numberOfNoiseRealizations)
509 inputRecorder = self.inputRecorder.makeCoaddTempExpRecorder(
514 for index, detector_inputs
in enumerate(inputs.values()):
516 "Warping exposure %d/%d for id=%s",
519 detector_inputs.data_id,
522 input_exposure = detector_inputs.exposure
524 seed = self.get_seed_from_data_id(detector_inputs.data_id)
525 rng = np.random.RandomState(seed + self.config.seedOffset)
528 noise_calexps = self.make_noise_exposures(input_exposure, rng)
530 warpedExposure = self.process(
535 destBBox=target_bbox,
538 if warpedExposure
is None:
540 "Skipping exposure %s because it could not be warped.", detector_inputs.data_id
544 if final_warp.photoCalib
is not None:
546 final_warp.photoCalib.getInstFluxAtZeroMagnitude()
547 / warpedExposure.photoCalib.getInstFluxAtZeroMagnitude()
552 self.log.debug(
"Scaling exposure %s by %f", detector_inputs.data_id, ratio)
553 warpedExposure.maskedImage *= ratio
556 nGood = copyGoodPixels(
557 final_warp.maskedImage,
558 warpedExposure.maskedImage,
559 final_warp.mask.getPlaneBitMask([
"NO_DATA"]),
562 if final_warp.photoCalib
is None and nGood > 0:
563 final_warp.setPhotoCalib(warpedExposure.photoCalib)
565 ccdId = self.config.idGenerator.apply(detector_inputs.data_id).catalog_id
566 inputRecorder.addCalExp(input_exposure, ccdId, nGood)
567 totalGoodPixels += nGood
569 if self.config.doWarpMaskedFraction:
571 if self.config.doPreWarpInterpolation:
572 badMaskPlanes = self.preWarpInterpolation.config.badMaskPlanes
575 masked_fraction_exp = self._get_bad_mask(input_exposure, badMaskPlanes)
577 masked_fraction_warp = self.maskedFractionWarper.warpExposure(
578 target_wcs, masked_fraction_exp, destBBox=target_bbox
582 final_masked_fraction_warp.maskedImage,
583 masked_fraction_warp.maskedImage,
584 final_masked_fraction_warp.mask.getPlaneBitMask([
"NO_DATA"]),
588 for n_noise
in range(self.config.numberOfNoiseRealizations):
589 noise_calexp = noise_calexps[n_noise]
590 noise_pseudo_inputs = dataclasses.replace(
592 exposure_or_handle=noise_calexp,
596 warpedNoise = self.process(
601 destBBox=target_bbox,
604 warpedNoise.maskedImage *= ratio
607 final_noise_warps[n_noise].maskedImage,
608 warpedNoise.maskedImage,
609 final_noise_warps[n_noise].mask.getPlaneBitMask([
"NO_DATA"]),
613 if totalGoodPixels == 0:
616 masked_fraction_warp=
None,
618 for noise_index
in range(self.config.numberOfNoiseRealizations):
619 setattr(results, f
"noise_warp{noise_index}",
None)
624 inputRecorder.finish(final_warp, totalGoodPixels)
627 inputRecorder.coaddInputs.ccds,
629 self.config.coaddPsf.makeControl(),
632 final_warp.setPsf(coaddPsf)
633 for _, warp_detector_input
in inputs.items():
634 final_warp.setFilter(warp_detector_input.exposure.getFilter())
635 final_warp.getInfo().setVisitInfo(warp_detector_input.exposure.getInfo().getVisitInfo())
642 if self.config.doWarpMaskedFraction:
643 results.masked_fraction_warp = final_masked_fraction_warp.image
645 for noise_index, noise_exposure
in final_noise_warps.items():
646 setattr(results, f
"noise_warp{noise_index}", noise_exposure.maskedImage)
652 detector_inputs: WarpDetectorInputs,
658 ) -> ExposureF |
None:
659 """Process an exposure.
661 There are three processing steps that are applied to the input:
663 1. Interpolate over bad pixels before warping.
664 2. Apply all calibrations from visit_summary to the exposure.
665 3. Warp the exposure to the target coordinate system.
669 detector_inputs : `WarpDetectorInputs`
670 The input exposure to be processed, along with any other
671 per-detector modifications.
672 target_wcs : `~lsst.afw.geom.SkyWcs`
673 The WCS of the target patch.
674 warper : `~lsst.afw.math.Warper`
675 The warper to use for warping the input exposure.
676 visit_summary : `~lsst.afw.table.ExposureCatalog` | None
677 Table of visit summary information. If not None, the visit_summary
678 information will be used to update the calibration of the input
679 exposures. Otherwise, the input exposures will be used as-is.
680 maxBBox : `~lsst.geom.Box2I` | None
681 Maximum bounding box of the warped exposure. If None, this is
682 determined automatically.
683 destBBox : `~lsst.geom.Box2I` | None
684 Exact bounding box of the warped exposure. If None, this is
685 determined automatically.
689 warped_exposure : `~lsst.afw.image.Exposure` | None
690 The processed and warped exposure, if all the calibrations could
691 be applied successfully. Otherwise, None.
694 input_exposure = detector_inputs.exposure
695 if self.config.doPreWarpInterpolation:
696 self.preWarpInterpolation.run(input_exposure.maskedImage)
698 success = self._apply_all_calibrations(
700 visit_summary=visit_summary,
701 includeScaleUncertainty=self.config.includeCalibVar,
707 with self.timer(
"warp"):
708 warped_exposure = warper.warpExposure(
717 return warped_exposure
719 def _apply_all_calibrations(
721 detector_inputs: WarpDetectorInputs,
723 visit_summary: ExposureCatalog |
None =
None,
724 includeScaleUncertainty: bool =
False,
726 """Apply all of the calibrations from visit_summary to the exposure.
728 Specifically, this method updates the following (if available) to the
729 input exposure in place from ``visit_summary``:
731 - Aperture correction map
732 - Photometric calibration
736 It also reverts and applies backgrounds in ``detector_inputs``.
740 detector_inputs : `WarpDetectorInputs`
741 The input exposure to be processed, along with any other
742 per-detector modifications.
743 visit_summary : `~lsst.afw.table.ExposureCatalog` | None
744 Table of visit summary information. If not None, the visit summary
745 information will be used to update the calibration of the input
746 exposures. Otherwise, the input exposures will be used as-is.
747 includeScaleUncertainty : bool
748 Whether to include the uncertainty on the calibration in the
749 resulting variance? Passed onto the `calibrateImage` method of the
750 PhotoCalib object attached to ``exp``.
755 True if all calibrations were successfully applied, False otherwise.
760 Raised if ``visit_summary`` is provided but does not contain a
761 record corresponding to ``detector_inputs``, or if one of the
762 background fields in ``detector_inputs`` is inconsistent with the
765 if self.config.doRevertOldBackground:
766 detector_inputs.revert_background()
767 elif detector_inputs.background_revert:
771 f
"doRevertOldBackground is False, but {detector_inputs.data_id} has a background_revert."
774 input_exposure = detector_inputs.exposure
775 if visit_summary
is not None:
776 detector = input_exposure.info.getDetector().getId()
777 row = visit_summary.find(detector)
781 "Unexpectedly incomplete visit_summary: detector = %s is missing. Skipping it.",
786 if photo_calib := row.getPhotoCalib():
787 input_exposure.setPhotoCalib(photo_calib)
790 "No photometric calibration found in visit summary for detector = %s. Skipping it.",
795 if wcs := row.getWcs():
796 input_exposure.setWcs(wcs)
798 self.log.info(
"No WCS found in visit summary for detector = %s. Skipping it.", detector)
801 if self.config.useVisitSummaryPsf:
802 if psf := row.getPsf():
803 input_exposure.setPsf(psf)
805 self.log.info(
"No PSF found in visit summary for detector = %s. Skipping it.", detector)
808 if apcorr_map := row.getApCorrMap():
809 input_exposure.setApCorrMap(apcorr_map)
812 "No aperture correction map found in visit summary for detector = %s. Skipping it",
817 elif visit_summary
is not None:
820 "useVisitSummaryPsf=True, but visit_summary is provided. "
823 if self.config.doApplyNewBackground:
824 detector_inputs.apply_background()
825 elif detector_inputs.background_apply:
829 f
"doApplyNewBackground is False, but {detector_inputs.data_id} has a background_apply."
834 photo_calib = input_exposure.photoCalib
835 input_exposure.maskedImage = photo_calib.calibrateImage(
836 input_exposure.maskedImage,
837 includeScaleUncertainty=includeScaleUncertainty,
839 input_exposure.maskedImage /= photo_calib.getCalibrationMean()
845 def _prepareEmptyExposure(cls, sky_info):
846 """Produce an empty exposure for a given patch.
850 sky_info : `lsst.pipe.base.Struct`
851 Struct from `~lsst.pipe.base.coaddBase.makeSkyInfo` with
852 geometric information about the patch.
856 exp : `lsst.afw.image.exposure.ExposureF`
857 An empty exposure for a given patch.
859 exp = ExposureF(sky_info.bbox, sky_info.wcs)
860 exp.getMaskedImage().set(np.nan, Mask.getPlaneBitMask(
"NO_DATA"), np.inf)
864 def compute_median_variance(mi: MaskedImage) -> float:
865 """Compute the median variance across the good pixels of a MaskedImage.
869 mi : `~lsst.afw.image.MaskedImage`
870 The input image on which to compute the median variance.
874 median_variance : `float`
875 Median variance of the input calexp.
879 return np.median(mi.variance.array[np.isfinite(mi.variance.array) & np.isfinite(mi.image.array)])
881 def get_seed_from_data_id(self, data_id) -> int:
882 """Get a seed value given a data_id.
884 This method generates a unique, reproducible pseudo-random number for
885 a data id. This is not affected by ordering of the input, or what
886 set of visits, ccds etc. are given.
888 This is implemented as a public method, so that simulations that
889 don't necessary deal with the middleware can mock up a ``data_id``
890 instance, or override this method with a different one to obtain a
891 seed value consistent with the pipeline task.
895 data_id : `~lsst.daf.butler.DataCoordinate`
896 Data identifier dictionary.
901 A unique seed for this data_id to seed a random number generator.
903 return self.config.idGenerator.apply(data_id).catalog_id
905 def make_noise_exposures(self, calexp: ExposureF, rng) -> dict[int, ExposureF]:
906 """Make pure noise realizations based on ``calexp``.
910 calexp : `~lsst.afw.image.ExposureF`
911 The input exposure on which to base the noise realizations.
912 rng : `np.random.RandomState`
913 Random number generator to use for the noise realizations.
917 noise_calexps : `dict` [`int`, `~lsst.afw.image.ExposureF`]
918 A mapping of integers ranging from 0 up to
919 config.numberOfNoiseRealizations to the corresponding
920 noise realization exposures.
926 if self.config.numberOfNoiseRealizations == 0:
929 if self.config.useMedianVariance:
930 variance = self.compute_median_variance(calexp.maskedImage)
932 variance = calexp.variance.array
934 for n_noise
in range(self.config.numberOfNoiseRealizations):
935 noise_calexp = calexp.clone()
936 noise_calexp.image.array[:, :] = rng.normal(
937 scale=np.sqrt(variance),
938 size=noise_calexp.image.array.shape,
940 noise_calexp.variance.array[:, :] = variance
941 noise_calexps[n_noise] = noise_calexp
946 def _get_bad_mask(cls, exp: ExposureF, badMaskPlanes: Iterable[str]) -> ExposureF:
947 """Get an Exposure of bad mask
951 exp: `lsst.afw.image.Exposure`
953 badMaskPlanes: `list` [`str`]
954 List of mask planes to be considered as bad.
958 bad_mask: `~lsst.afw.image.Exposure`
959 An Exposure with boolean array with True if inverse variance <= 0
960 or if any of the badMaskPlanes bits are set, and False otherwise.
963 bad_mask = exp.clone()
965 var = exp.variance.array
966 mask = exp.mask.array
968 bitMask = exp.mask.getPlaneBitMask(badMaskPlanes)
970 bad_mask.image.array[:, :] = (var < 0) | np.isinf(var) | ((mask & bitMask) != 0)
972 bad_mask.variance.array *= 0.0
A floating-point coordinate rectangle geometry.
CoaddPsf is the Psf derived to be used for non-PSF-matched Coadd images.