LSST Applications g06d8191974+de063e15a7,g180d380827+d0b6459378,g2079a07aa2+86d27d4dc4,g2305ad1205+f1ae3263cc,g29320951ab+5752d78b6e,g2bbee38e9b+85cf0a37e7,g337abbeb29+85cf0a37e7,g33d1c0ed96+85cf0a37e7,g3a166c0a6a+85cf0a37e7,g3ddfee87b4+b5254b9343,g48712c4677+9ea88d309d,g487adcacf7+05f7dba17f,g50ff169b8f+96c6868917,g52b1c1532d+585e252eca,g591dd9f2cf+48904e3942,g64a986408d+de063e15a7,g858d7b2824+de063e15a7,g864b0138d7+33ab2bc355,g8a8a8dda67+585e252eca,g99cad8db69+4508353287,g9c22b2923f+53520f316c,g9ddcbc5298+9a081db1e4,ga1e77700b3+15fc3df1f7,gb0e22166c9+60f28cb32d,gba4ed39666+c2a2e4ac27,gbb8dafda3b+ccb7f83a87,gc120e1dc64+6caf640b9b,gc28159a63d+85cf0a37e7,gc3e9b769f7+548c5e05a3,gcf0d15dbbd+b5254b9343,gdaeeff99f8+f9a426f77a,ge6526c86ff+515b6c9330,ge79ae78c31+85cf0a37e7,gee10cc3b42+585e252eca,gff1a9f87cc+de063e15a7,w.2024.17
LSST Data Management Base Package
Loading...
Searching...
No Matches
make_direct_warp.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22from __future__ import annotations
23
24from typing import TYPE_CHECKING, Iterable
25
26import numpy as np
27from lsst.afw.geom import makeWcsPairTransform
28from lsst.afw.image import ExposureF, Mask
29from lsst.afw.math import Warper
30from lsst.coadd.utils import copyGoodPixels
31from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig, WarpedPsf
33 CloughTocher2DInterpolateTask,
34)
35from lsst.meas.base import DetectorVisitIdGeneratorConfig
36from lsst.pex.config import (
37 ConfigField,
38 ConfigurableField,
39 Field,
40 RangeField,
41)
42from lsst.pipe.base import (
43 NoWorkFound,
44 PipelineTask,
45 PipelineTaskConfig,
46 PipelineTaskConnections,
47 Struct,
48)
49from lsst.pipe.base.connectionTypes import Input, Output
50from lsst.pipe.tasks.coaddBase import makeSkyInfo
51from lsst.skymap import BaseSkyMap
52
53from .coaddInputRecorder import CoaddInputRecorderTask
54
55if TYPE_CHECKING:
56 from lsst.afw.image import MaskedImage
57 from lsst.afw.table import Exposure, ExposureCatalog
58
59
60__all__ = (
61 "MakeDirectWarpConfig",
62 "MakeDirectWarpTask",
63)
64
65
67 PipelineTaskConnections,
68 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
69 defaultTemplates={
70 "coaddName": "deep",
71 "calexpType": "",
72 },
73):
74 """Connections for MakeWarpTask"""
75
76 calexp_list = Input(
77 doc="Input exposures to be interpolated and resampled onto a SkyMap "
78 "projection/patch.",
79 name="{calexpType}calexp",
80 storageClass="ExposureF",
81 dimensions=("instrument", "visit", "detector"),
82 multiple=True,
83 deferLoad=True,
84 )
85 background_revert_list = Input(
86 doc="Background to be reverted (i.e., added back to the calexp). "
87 "This connection is used only if doRevertOldBackground=False.",
88 name="calexpBackground",
89 storageClass="Background",
90 dimensions=("instrument", "visit", "detector"),
91 multiple=True,
92 )
93 background_apply_list = Input(
94 doc="Background to be applied (subtracted from the calexp). "
95 "This is used only if doApplyNewBackground=True.",
96 name="skyCorr",
97 storageClass="Background",
98 dimensions=("instrument", "visit", "detector"),
99 multiple=True,
100 )
101 visit_summary = Input(
102 doc="Input visit-summary catalog with updated calibration objects.",
103 name="finalVisitSummary",
104 storageClass="ExposureCatalog",
105 dimensions=("instrument", "visit"),
106 )
107 sky_map = Input(
108 doc="Input definition of geometry/bbox and projection/wcs for warps.",
109 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
110 storageClass="SkyMap",
111 dimensions=("skymap",),
112 )
113 # Declare all possible outputs (except noise, which is configurable)
114 warp = Output(
115 doc="Output direct warped exposure produced by resampling calexps "
116 "onto the skyMap patch geometry.",
117 name="{coaddName}Coadd_directWarp",
118 storageClass="ExposureF",
119 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
120 )
121 masked_fraction_warp = Output(
122 doc="Output masked fraction warped exposure.",
123 name="{coaddName}Coadd_directWarp_maskedFraction",
124 storageClass="ImageF",
125 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
126 )
127
128 def __init__(self, *, config=None):
129 super().__init__(config=config)
130 if not config:
131 return
132
133 if not config.doRevertOldBackground:
134 del self.background_revert_list
135 if not config.doApplyNewBackground:
136 del self.background_apply_list
137
138 # Dynamically set output connections for noise images, depending on the
139 # number of noise realization specified in the config.
140 for n in range(config.numberOfNoiseRealizations):
141 noise_warp = Output(
142 doc=f"Output direct warped noise exposure ({n})",
143 name=f"{config.connections.coaddName}Coadd_directWarp_noise{n}",
144 # Store it as a MaskedImage to preserve the variance plane.
145 storageClass="MaskedImageF",
146 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
147 )
148 setattr(self, f"noise_warp{n}", noise_warp)
149
150
151class MakeDirectWarpConfig(
152 PipelineTaskConfig,
153 pipelineConnections=MakeDirectWarpConnections,
154):
155 """Configuration for the MakeDirectWarpTask.
156
157 The config fields are as similar as possible to the corresponding fields in
158 MakeWarpConfig.
159
160 Notes
161 -----
162 The config fields are in camelCase to match the fields in the earlier
163 version of the makeWarp task as closely as possible.
164 """
165
166 MAX_NUMBER_OF_NOISE_REALIZATIONS = 3
167 """
168 numberOfNoiseRealizations is defined as a RangeField to prevent from
169 making multiple output connections and blowing up the memory usage by
170 accident. An upper bound of 3 is based on the best guess of the maximum
171 number of noise realizations that will be used for metadetection.
172 """
173
174 numberOfNoiseRealizations = RangeField[int](
175 doc="Number of noise realizations to simulate and persist.",
176 default=1,
177 min=0,
178 max=MAX_NUMBER_OF_NOISE_REALIZATIONS,
179 inclusiveMax=True,
180 )
181 seedOffset = Field[int](
182 doc="Offset to the seed used for the noise realization. This can be "
183 "used to create a different noise realization if the default ones "
184 "are catastrophic, or for testing sensitivity to the noise.",
185 default=0,
186 )
187 useMedianVariance = Field[bool](
188 doc="Use the median of variance plane in the input calexp to generate "
189 "noise realizations? If False, per-pixel variance will be used.",
190 default=True,
191 )
192 doRevertOldBackground = Field[bool](
193 doc="Revert the old backgrounds from the `background_revert_list` "
194 "connection?",
195 default=True,
196 )
197 doApplyNewBackground = Field[bool](
198 doc="Apply the new backgrounds from the `background_apply_list` "
199 "connection?",
200 default=False,
201 )
202 useVisitSummaryPsf = Field[bool](
203 doc="If True, use the PSF model and aperture corrections from the "
204 "'visit_summary' connection. If False, use the PSF model and "
205 "aperture corrections from the 'calexp' connection.",
206 default=True,
207 )
208 doPreWarpInterpolation = Field[bool](
209 doc="Interpolate over bad pixels before warping?",
210 default=True,
211 )
212 preWarpInterpolation = ConfigurableField(
213 doc="Interpolation task to use for pre-warping interpolation",
214 target=CloughTocher2DInterpolateTask,
215 )
216 inputRecorder = ConfigurableField(
217 doc="Subtask that helps fill CoaddInputs catalogs added to the final "
218 "coadd",
219 target=CoaddInputRecorderTask,
220 )
221 includeCalibVar = Field[bool](
222 doc="Add photometric calibration variance to warp variance plane?",
223 default=False,
224 )
225 matchingKernelSize = Field[int](
226 doc="Size in pixels of matching kernel. Must be odd.",
227 default=21,
228 check=lambda x: x % 2 == 1,
229 )
230 warper = ConfigField(
231 doc="Configuration for the warper that warps the image and noise",
232 dtype=Warper.ConfigClass,
233 )
234 maskedFractionWarper = ConfigField(
235 doc="Configuration for the warp that warps the mask fraction image",
236 dtype=Warper.ConfigClass,
237 )
238 coaddPsf = ConfigField(
239 doc="Configuration for CoaddPsf",
240 dtype=CoaddPsfConfig,
241 )
242 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
243
244 # Use bgSubtracted and doApplySkyCorr to match the old MakeWarpConfig,
245 # but as properties instead of config fields.
246 @property
247 def bgSubtracted(self) -> bool:
248 return not self.doRevertOldBackground
249
250 @bgSubtracted.setter
251 def bgSubtracted(self, value: bool) -> None:
252 self.doRevertOldBackground = ~value
253
254 @property
255 def doApplySkyCorr(self) -> bool:
256 return self.doApplyNewBackground
257
258 @doApplySkyCorr.setter
259 def doApplySkyCorr(self, value: bool) -> None:
260 self.doApplyNewBackground = value
261
262 def setDefaults(self) -> None:
263 super().setDefaults()
264 self.warper.warpingKernelName = "lanczos3"
265 self.maskedFractionWarper.warpingKernelName = "bilinear"
266
267
268class MakeDirectWarpTask(PipelineTask):
269 """Warp single-detector images onto a common projection.
270
271 This task iterates over multiple images (corresponding to different
272 detectors) from a single visit that overlap the target patch. Pixels that
273 receive no input from any detector are set to NaN in the output image, and
274 NO_DATA bit is set in the mask plane.
275
276 This differs from the standard `MakeWarp` Task in the following
277 ways:
278
279 1. No selection on ccds at the time of warping. This is done later during
280 the coaddition stage.
281 2. Interpolate over a set of masked pixels before warping.
282 3. Generate an image where each pixel denotes how much of the pixel is
283 masked.
284 4. Generate multiple noise warps with the same interpolation applied.
285 5. No option to produce a PSF-matched warp.
286 """
287
288 ConfigClass = MakeDirectWarpConfig
289 _DefaultName = "makeDirectWarp"
290
291 def __init__(self, **kwargs):
292 super().__init__(**kwargs)
293 self.makeSubtask("inputRecorder")
294 self.makeSubtask("preWarpInterpolation")
295
296 self.warper = Warper.fromConfig(self.config.warper)
297 self.maskedFractionWarper = Warper.fromConfig(self.config.maskedFractionWarper)
298
299 def runQuantum(self, butlerQC, inputRefs, outputRefs):
300 # Docstring inherited.
301
302 # Read in all inputs.
303 inputs = butlerQC.get(inputRefs)
304
305 if not inputs["calexp_list"]:
306 raise NoWorkFound("No input warps provided for co-addition")
307
308 sky_map = inputs.pop("sky_map")
309
310 quantumDataId = butlerQC.quantum.dataId
311 sky_info = makeSkyInfo(
312 sky_map,
313 tractId=quantumDataId["tract"],
314 patchId=quantumDataId["patch"],
315 )
316
317 visit_summary = inputs["visit_summary"] if self.config.useVisitSummaryPsf else None
318
319 results = self.run(inputs, sky_info, visit_summary)
320 butlerQC.put(results, outputRefs)
321
322 def run(self, inputs, sky_info, visit_summary):
323 """Create a Warp dataset from inputs.
324
325 Parameters
326 ----------
327 inputs : `Mapping`
328 Dictionary of input datasets. It must have a list of input calexps
329 under the key "calexp_list". Other supported keys are
330 "background_revert_list" and "background_apply_list", corresponding
331 to the old and the new backgrounds to be reverted and applied to
332 the calexps. They must be in the same order as the calexps.
333 sky_info : `~lsst.pipe.base.Struct`
334 A Struct object containing wcs, bounding box, and other information
335 about the patches within the tract.
336 visit_summary : `~lsst.afw.table.ExposureCatalog` | None
337 Table of visit summary information. If provided, the visit summary
338 information will be used to update the calibration of the input
339 exposures. If None, the input exposures will be used as-is.
340
341 Returns
342 -------
343 results : `~lsst.pipe.base.Struct`
344 A Struct object containing the warped exposure, noise exposure(s),
345 and masked fraction image.
346 """
347 target_bbox, target_wcs = sky_info.bbox, sky_info.wcs
348
349 # Initialize the objects that will hold the warp.
350 final_warp = ExposureF(target_bbox, target_wcs)
351
352 exposures = inputs["calexp_list"]
353 background_revert_list = inputs.get("background_revert_list", [None] * len(exposures))
354 background_apply_list = inputs.get("background_apply_list", [None] * len(exposures))
355
356 visit_id = exposures[0].dataId["visit"]
357
358 # The warpExposure routine is expensive, and we do not want to call
359 # it twice (i.e., a second time for PSF-matched warps). We do not
360 # want to hold all the warped exposures in memory at once either.
361 # So we create empty exposure(s) to accumulate the warps of each type,
362 # and then process each detector serially.
363 final_warp = self._prepareEmptyExposure(sky_info)
364 final_masked_fraction_warp = self._prepareEmptyExposure(sky_info)
365 final_noise_warps = {
366 n_noise: self._prepareEmptyExposure(sky_info)
367 for n_noise in range(self.config.numberOfNoiseRealizations)
368 }
369
370 # We need a few bookkeeping variables only for the science coadd.
371 totalGoodPixels = 0
372 inputRecorder = self.inputRecorder.makeCoaddTempExpRecorder(
373 visit_id,
374 len(exposures),
375 )
376
377 for index, (calexp_ref, old_background, new_background) in enumerate(
378 zip(exposures, background_revert_list, background_apply_list, strict=True)
379 ):
380 dataId = calexp_ref.dataId
381 self.log.debug(
382 "Warping exposure %d/%d for id=%s",
383 index + 1,
384 len(exposures),
385 dataId,
386 )
387 calexp = calexp_ref.get()
388
389 # Generate noise image(s) in-situ.
390 seed = self.get_seed_from_data_id(dataId)
391 rng = np.random.RandomState(seed + self.config.seedOffset)
392
393 # Generate noise images in-situ.
394 noise_calexps = self.make_noise_exposures(calexp, rng)
395
396 # Warp the PSF before processing nad overwriting exposure.
397 xyTransform = makeWcsPairTransform(calexp.getWcs(), target_wcs)
398 psfWarped = WarpedPsf(calexp.getPsf(), xyTransform)
399
400 warpedExposure = self.process(
401 calexp,
402 target_wcs,
403 self.warper,
404 old_background,
405 new_background,
406 visit_summary,
407 destBBox=target_bbox,
408 )
409 warpedExposure.setPsf(psfWarped)
410
411 # Accumulate the partial warps in an online fashion.
412 nGood = copyGoodPixels(
413 final_warp.maskedImage,
414 warpedExposure.maskedImage,
415 final_warp.mask.getPlaneBitMask(["NO_DATA"]),
416 )
417 ccdId = self.config.idGenerator.apply(dataId).catalog_id
418 inputRecorder.addCalExp(calexp, ccdId, nGood)
419 totalGoodPixels += nGood
420
421 # Obtain the masked fraction exposure and warp it.
422 if self.config.doPreWarpInterpolation:
423 badMaskPlanes = self.preWarpInterpolation.config.badMaskPlanes
424 else:
425 badMaskPlanes = []
426 masked_fraction_exp = self._get_bad_mask(calexp, badMaskPlanes)
427 masked_fraction_warp = self.maskedFractionWarper.warpExposure(
428 target_wcs, masked_fraction_exp, destBBox=target_bbox
429 )
430 copyGoodPixels(
431 final_masked_fraction_warp.maskedImage,
432 masked_fraction_warp.maskedImage,
433 final_masked_fraction_warp.mask.getPlaneBitMask(["NO_DATA"]),
434 )
435
436 # Process and accumulate noise images.
437 for n_noise in range(self.config.numberOfNoiseRealizations):
438 noise_calexp = noise_calexps[n_noise]
439 warpedNoise = self.process(
440 noise_calexp,
441 target_wcs,
442 self.warper,
443 old_background,
444 new_background,
445 visit_summary,
446 destBBox=target_bbox,
447 )
448 copyGoodPixels(
449 final_noise_warps[n_noise].maskedImage,
450 warpedNoise.maskedImage,
451 final_noise_warps[n_noise].mask.getPlaneBitMask(["NO_DATA"]),
452 )
453
454 # Finish the inputRecorder and add the coaddPsf to the final warp.
455 if totalGoodPixels > 0:
456 inputRecorder.finish(final_warp, totalGoodPixels)
457
458 coaddPsf = CoaddPsf(
459 inputRecorder.coaddInputs.ccds,
460 sky_info.wcs,
461 self.config.coaddPsf.makeControl(),
462 )
463
464 final_warp.setPsf(coaddPsf)
465 final_warp.setFilter(calexp.getFilter())
466 final_warp.getInfo().setVisitInfo(calexp.getInfo().getVisitInfo())
467
468 results = Struct(
469 warp=final_warp,
470 masked_fraction_warp=final_masked_fraction_warp.image,
471 )
472
473 for noise_index, noise_exposure in final_noise_warps.items():
474 setattr(results, f"noise_warp{noise_index}", noise_exposure.maskedImage)
475
476 return results
477
478 def process(
479 self,
480 exposure,
481 target_wcs,
482 warper,
483 old_background=None,
484 new_background=None,
485 visit_summary=None,
486 maxBBox=None,
487 destBBox=None,
488 ):
489 """Process an exposure.
490
491 There are three processing steps that are applied to the input:
492
493 1. Interpolate over bad pixels before warping.
494 2. Apply all calibrations from visit_summary to the exposure.
495 3. Warp the exposure to the target coordinate system.
496
497 Parameters
498 ----------
499 exposure : `~lsst.afw.image.Exposure`
500 The input exposure to be processed.
501 target_wcs : `~lsst.afw.geom.SkyWcs`
502 The WCS of the target patch.
503 warper : `~lsst.afw.math.Warper`
504 The warper to use for warping the input exposure.
505 old_background : `~lsst.afw.image.Background` | None
506 The old background to be added back into the calexp.
507 new_background : `~lsst.afw.image.Background` | None
508 The new background to be subtracted from the calexp.
509 visit_summary : `~lsst.afw.table.ExposureCatalog` | None
510 Table of visit summary information. If not None, the visit_summary
511 information will be used to update the calibration of the input
512 exposures. Otherwise, the input exposures will be used as-is.
513 maxBBox : `~lsst.geom.Box2I` | None
514 Maximum bounding box of the warped exposure. If None, this is
515 determined automatically.
516 destBBox : `~lsst.geom.Box2I` | None
517 Exact bounding box of the warped exposure. If None, this is
518 determined automatically.
519
520 Returns
521 -------
522 warped_exposure : `~lsst.afw.image.Exposure`
523 The processed and warped exposure.
524 """
525
526 if self.config.doPreWarpInterpolation:
527 self.preWarpInterpolation.run(exposure.maskedImage)
528
529 self._apply_all_calibrations(
530 exposure,
531 old_background,
532 new_background,
533 logger=self.log,
534 visit_summary=visit_summary,
535 includeScaleUncertainty=self.config.includeCalibVar,
536 )
537 with self.timer("warp"):
538 warped_exposure = warper.warpExposure(
539 target_wcs,
540 exposure,
541 maxBBox=maxBBox,
542 destBBox=destBBox,
543 )
544
545 # Potentially a post-warp interpolation here? Relies on DM-38630.
546
547 return warped_exposure
548
549 @staticmethod
550 def _apply_all_calibrations(
551 exp: Exposure,
552 old_background,
553 new_background,
554 logger,
555 visit_summary: ExposureCatalog | None = None,
556 includeScaleUncertainty: bool = False,
557 ) -> None:
558 """Apply all of the calibrations from visit_summary to the exposure.
559
560 Specifically, this method updates the following (if available) to the
561 input exposure ``exp`` in place from ``visit_summary``:
562
563 - Aperture correction map
564 - Photometric calibration
565 - PSF
566 - WCS
567
568 Parameters
569 ----------
570 exp : `~lsst.afw.image.Exposure`
571 Exposure to be updated.
572 old_background : `~lsst.afw.image.Exposure`
573 Exposure corresponding to the old background, to be added back.
574 new_background : `~lsst.afw.image.Exposure`
575 Exposure corresponding to the new background, to be subtracted.
576 logger : `logging.Logger`
577 Logger object from the caller Task to write the logs onto.
578 visit_summary : `~lsst.afw.table.ExposureCatalog` | None
579 Table of visit summary information. If not None, the visit summary
580 information will be used to update the calibration of the input
581 exposures. Otherwise, the input exposures will be used as-is.
582 includeScaleUncertainty : bool
583 Whether to include the uncertainty on the calibration in the
584 resulting variance? Passed onto the `calibrateImage` method of the
585 PhotoCalib object attached to ``exp``.
586
587 Raises
588 ------
589 RuntimeError
590 Raised if ``visit_summary`` is provided but does not contain a
591 record corresponding to ``exp``.
592 """
593 if not visit_summary:
594 logger.debug("No visit summary provided.")
595 else:
596 logger.debug("Updating calibration from visit summary.")
597
598 if old_background:
599 exp.maskedImage += old_background.getImage()
600
601 if visit_summary:
602 detector = exp.info.getDetector().getId()
603 row = visit_summary.find(detector)
604
605 if row is None:
606 raise RuntimeError(f"Unexpectedly incomplete visit_summary: {detector=} is missing.")
607
608 if photo_calib := row.getPhotoCalib():
609 exp.setPhotoCalib(photo_calib)
610 else:
611 logger.warning(
612 "No photometric calibration found in visit summary for detector = %s.",
613 detector,
614 )
615
616 if wcs := row.getWcs():
617 exp.setWcs(wcs)
618 else:
619 logger.warning("No WCS found in visit summary for detector = %s.", detector)
620
621 if psf := row.getPsf():
622 exp.setPsf(psf)
623 else:
624 logger.warning("No PSF found in visit summary for detector = %s.", detector)
625
626 if apcorr_map := row.getApCorrMap():
627 exp.setApCorrMap(apcorr_map)
628 else:
629 logger.warning(
630 "No aperture correction map found in visit summary for detector = %s.",
631 detector,
632 )
633
634 if new_background:
635 exp.maskedImage -= new_background.getImage()
636
637 # Calibrate the (masked) image.
638 # This should likely happen even if visit_summary is None.
639 photo_calib = exp.getPhotoCalib()
640 exp.maskedImage = photo_calib.calibrateImage(
641 exp.maskedImage, includeScaleUncertainty=includeScaleUncertainty
642 )
643 exp.maskedImage /= photo_calib.getCalibrationMean()
644
645 # This method is copied from makeWarp.py
646 @classmethod
647 def _prepareEmptyExposure(cls, sky_info):
648 """Produce an empty exposure for a given patch.
649
650 Parameters
651 ----------
652 sky_info : `lsst.pipe.base.Struct`
653 Struct from `~lsst.pipe.base.coaddBase.makeSkyInfo` with
654 geometric information about the patch.
655
656 Returns
657 -------
658 exp : `lsst.afw.image.exposure.ExposureF`
659 An empty exposure for a given patch.
660 """
661 exp = ExposureF(sky_info.bbox, sky_info.wcs)
662 exp.getMaskedImage().set(np.nan, Mask.getPlaneBitMask("NO_DATA"), np.inf)
663 return exp
664
665 @staticmethod
666 def compute_median_variance(mi: MaskedImage) -> float:
667 """Compute the median variance across the good pixels of a MaskedImage.
668
669 Parameters
670 ----------
671 mi : `~lsst.afw.image.MaskedImage`
672 The input image on which to compute the median variance.
673
674 Returns
675 -------
676 median_variance : `float`
677 Median variance of the input calexp.
678 """
679 # Shouldn't this exclude pixels that are masked, to be safe?
680 # This is implemented as it was in descwl_coadd.
681 return np.median(mi.variance.array[np.isfinite(mi.variance.array) & np.isfinite(mi.image.array)])
682
683 def get_seed_from_data_id(self, data_id) -> int:
684 """Get a seed value given a data_id.
685
686 This method generates a unique, reproducible pseudo-random number for
687 a data id. This is not affected by ordering of the input, or what
688 set of visits, ccds etc. are given.
689
690 This is implemented as a public method, so that simulations that
691 don't necessary deal with the middleware can mock up a ``data_id``
692 instance, or override this method with a different one to obtain a
693 seed value consistent with the pipeline task.
694
695 Parameters
696 ----------
697 data_id : `~lsst.daf.butler.DataCoordinate`
698 Data identifier dictionary.
699
700 Returns
701 -------
702 seed : `int`
703 A unique seed for this data_id to seed a random number generator.
704 """
705 return self.config.idGenerator.apply(data_id).catalog_id
706
707 def make_noise_exposures(self, calexp: ExposureF, rng) -> dict[int, ExposureF]:
708 """Make pure noise realizations based on ``calexp``.
709
710 Parameters
711 ----------
712 calexp : `~lsst.afw.image.ExposureF`
713 The input exposure on which to base the noise realizations.
714 rng : `np.random.RandomState`
715 Random number generator to use for the noise realizations.
716
717 Returns
718 -------
719 noise_calexps : `dict` [`int`, `~lsst.afw.image.ExposureF`]
720 A mapping of integers ranging from 0 up to
721 config.numberOfNoiseRealizations to the corresponding
722 noise realization exposures.
723 """
724 noise_calexps = {}
725
726 # If no noise exposures are requested, return the empty dictionary
727 # without any further computations.
728 if self.config.numberOfNoiseRealizations == 0:
729 return noise_calexps
730
731 if self.config.useMedianVariance:
732 variance = self.compute_median_variance(calexp.maskedImage)
733 else:
734 variance = calexp.variance.array
735
736 for n_noise in range(self.config.numberOfNoiseRealizations):
737 noise_calexp = calexp.clone()
738 noise_calexp.image.array[:, :] = rng.normal(
739 scale=np.sqrt(variance),
740 size=noise_calexp.image.array.shape,
741 )
742 noise_calexp.variance.array[:, :] = variance
743 noise_calexps[n_noise] = noise_calexp
744
745 return noise_calexps
746
747 @classmethod
748 def _get_bad_mask(cls, exp: ExposureF, badMaskPlanes: Iterable[str]) -> ExposureF:
749 """Get an Exposure of bad mask
750
751 Parameters
752 ----------
753 exp: `lsst.afw.image.Exposure`
754 The exposure data.
755 badMaskPlanes: `list` [`str`]
756 List of mask planes to be considered as bad.
757
758 Returns
759 -------
760 bad_mask: `~lsst.afw.image.Exposure`
761 An Exposure with boolean array with True if inverse variance <= 0
762 or if any of the badMaskPlanes bits are set, and False otherwise.
763 """
764
765 bad_mask = exp.clone()
766
767 var = exp.variance.array
768 mask = exp.mask.array
769
770 bitMask = exp.mask.getPlaneBitMask(badMaskPlanes)
771
772 bad_mask.image.array[:, :] = (var < 0) | np.isinf(var) | ((mask & bitMask) != 0)
773
774 bad_mask.variance.array *= 0.0
775
776 return bad_mask
CoaddPsf is the Psf derived to be used for non-PSF-matched Coadd images.
Definition CoaddPsf.h:58
A Psf class that maps an arbitrary Psf through a coordinate transformation.
Definition WarpedPsf.h:52
daf::base::PropertySet * set
Definition fits.cc:931