LSST Applications 28.0.0,g1653933729+a8ce1bb630,g1a997c3884+a8ce1bb630,g28da252d5a+5bd70b7e6d,g2bbee38e9b+638fca75ac,g2bc492864f+638fca75ac,g3156d2b45e+07302053f8,g347aa1857d+638fca75ac,g35bb328faa+a8ce1bb630,g3a166c0a6a+638fca75ac,g3e281a1b8c+7bbb0b2507,g4005a62e65+17cd334064,g414038480c+5b5cd4fff3,g41af890bb2+4ffae9de63,g4e1a3235cc+0f1912dca3,g6249c6f860+3c3976f90c,g80478fca09+46aba80bd6,g82479be7b0+77990446f6,g858d7b2824+78ba4d1ce1,g89c8672015+f667a5183b,g9125e01d80+a8ce1bb630,ga5288a1d22+2a6264e9ca,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc22bb204ba+78ba4d1ce1,gc28159a63d+638fca75ac,gcf0d15dbbd+32ddb6096f,gd6b7c0dfd1+3e339405e9,gda3e153d99+78ba4d1ce1,gda6a2b7d83+32ddb6096f,gdaeeff99f8+1711a396fd,gdd5a9049c5+b18c39e5e3,ge2409df99d+a5e4577cdc,ge33fd446bb+78ba4d1ce1,ge79ae78c31+638fca75ac,gf0baf85859+64e8883e75,gf5289d68f6+e1b046a8d7,gfa443fc69c+91d9ed1ecf,gfda6b12a05+8419469a56
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.geom import Box2D
32from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig, WarpedPsf
34 CloughTocher2DInterpolateTask,
35)
36from lsst.meas.base import DetectorVisitIdGeneratorConfig
37from lsst.pex.config import (
38 ConfigField,
39 ConfigurableField,
40 Field,
41 RangeField,
42)
43from lsst.pipe.base import (
44 NoWorkFound,
45 PipelineTask,
46 PipelineTaskConfig,
47 PipelineTaskConnections,
48 Struct,
49)
50from lsst.pipe.base.connectionTypes import Input, Output
51from lsst.pipe.tasks.coaddBase import makeSkyInfo
52from lsst.pipe.tasks.selectImages import PsfWcsSelectImagesTask
53from lsst.skymap import BaseSkyMap
54
55from .coaddInputRecorder import CoaddInputRecorderTask
56
57if TYPE_CHECKING:
58 from lsst.afw.image import MaskedImage
59 from lsst.afw.table import Exposure, ExposureCatalog
60
61
62__all__ = (
63 "MakeDirectWarpConfig",
64 "MakeDirectWarpTask",
65)
66
67
69 PipelineTaskConnections,
70 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
71 defaultTemplates={
72 "coaddName": "deep",
73 "calexpType": "",
74 },
75):
76 """Connections for MakeWarpTask"""
77
78 calexp_list = Input(
79 doc="Input exposures to be interpolated and resampled onto a SkyMap "
80 "projection/patch.",
81 name="{calexpType}calexp",
82 storageClass="ExposureF",
83 dimensions=("instrument", "visit", "detector"),
84 multiple=True,
85 deferLoad=True,
86 )
87 background_revert_list = Input(
88 doc="Background to be reverted (i.e., added back to the calexp). "
89 "This connection is used only if doRevertOldBackground=False.",
90 name="calexpBackground",
91 storageClass="Background",
92 dimensions=("instrument", "visit", "detector"),
93 multiple=True,
94 )
95 background_apply_list = Input(
96 doc="Background to be applied (subtracted from the calexp). "
97 "This is used only if doApplyNewBackground=True.",
98 name="skyCorr",
99 storageClass="Background",
100 dimensions=("instrument", "visit", "detector"),
101 multiple=True,
102 )
103 visit_summary = Input(
104 doc="Input visit-summary catalog with updated calibration objects.",
105 name="finalVisitSummary",
106 storageClass="ExposureCatalog",
107 dimensions=("instrument", "visit"),
108 )
109 sky_map = Input(
110 doc="Input definition of geometry/bbox and projection/wcs for warps.",
111 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
112 storageClass="SkyMap",
113 dimensions=("skymap",),
114 )
115 # Declare all possible outputs (except noise, which is configurable)
116 warp = Output(
117 doc="Output direct warped exposure produced by resampling calexps "
118 "onto the skyMap patch geometry.",
119 name="{coaddName}Coadd_directWarp",
120 storageClass="ExposureF",
121 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
122 )
123 masked_fraction_warp = Output(
124 doc="Output masked fraction warped exposure.",
125 name="{coaddName}Coadd_directWarp_maskedFraction",
126 storageClass="ImageF",
127 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
128 )
129
130 def __init__(self, *, config=None):
131 super().__init__(config=config)
132 if not config:
133 return
134
135 if not config.doRevertOldBackground:
136 del self.background_revert_list
137 if not config.doApplyNewBackground:
138 del self.background_apply_list
139
140 if not config.doWarpMaskedFraction:
141 del self.masked_fraction_warp
142
143 # Dynamically set output connections for noise images, depending on the
144 # number of noise realization specified in the config.
145 for n in range(config.numberOfNoiseRealizations):
146 noise_warp = Output(
147 doc=f"Output direct warped noise exposure ({n})",
148 name=f"{config.connections.coaddName}Coadd_directWarp_noise{n}",
149 # Store it as a MaskedImage to preserve the variance plane.
150 storageClass="MaskedImageF",
151 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
152 )
153 setattr(self, f"noise_warp{n}", noise_warp)
154
155
156class MakeDirectWarpConfig(
157 PipelineTaskConfig,
158 pipelineConnections=MakeDirectWarpConnections,
159):
160 """Configuration for the MakeDirectWarpTask.
161
162 The config fields are as similar as possible to the corresponding fields in
163 MakeWarpConfig.
164
165 Notes
166 -----
167 The config fields are in camelCase to match the fields in the earlier
168 version of the makeWarp task as closely as possible.
169 """
170
171 MAX_NUMBER_OF_NOISE_REALIZATIONS = 3
172 """
173 numberOfNoiseRealizations is defined as a RangeField to prevent from
174 making multiple output connections and blowing up the memory usage by
175 accident. An upper bound of 3 is based on the best guess of the maximum
176 number of noise realizations that will be used for metadetection.
177 """
178
179 numberOfNoiseRealizations = RangeField[int](
180 doc="Number of noise realizations to simulate and persist.",
181 default=0,
182 min=0,
183 max=MAX_NUMBER_OF_NOISE_REALIZATIONS,
184 inclusiveMax=True,
185 )
186 seedOffset = Field[int](
187 doc="Offset to the seed used for the noise realization. This can be "
188 "used to create a different noise realization if the default ones "
189 "are catastrophic, or for testing sensitivity to the noise.",
190 default=0,
191 )
192 useMedianVariance = Field[bool](
193 doc="Use the median of variance plane in the input calexp to generate "
194 "noise realizations? If False, per-pixel variance will be used.",
195 default=True,
196 )
197 doRevertOldBackground = Field[bool](
198 doc="Revert the old backgrounds from the `background_revert_list` "
199 "connection?",
200 default=False,
201 )
202 doApplyNewBackground = Field[bool](
203 doc="Apply the new backgrounds from the `background_apply_list` "
204 "connection?",
205 default=False,
206 )
207 useVisitSummaryPsf = Field[bool](
208 doc="If True, use the PSF model and aperture corrections from the "
209 "'visit_summary' connection to make the warp. If False, use the "
210 "PSF model and aperture corrections from the 'calexp' connection.",
211 default=True,
212 )
213 doSelectPreWarp = Field[bool](
214 doc="Select ccds before warping?",
215 default=True,
216 )
217 select = ConfigurableField(
218 doc="Image selection subtask.",
219 target=PsfWcsSelectImagesTask,
220 )
221 doPreWarpInterpolation = Field[bool](
222 doc="Interpolate over bad pixels before warping?",
223 default=False,
224 )
225 preWarpInterpolation = ConfigurableField(
226 doc="Interpolation task to use for pre-warping interpolation",
227 target=CloughTocher2DInterpolateTask,
228 )
229 inputRecorder = ConfigurableField(
230 doc="Subtask that helps fill CoaddInputs catalogs added to the final "
231 "coadd",
232 target=CoaddInputRecorderTask,
233 )
234 includeCalibVar = Field[bool](
235 doc="Add photometric calibration variance to warp variance plane?",
236 default=False,
237 )
238 border = Field[int](
239 doc="Pad the patch boundary of the warp by these many pixels, so as to allow for PSF-matching later",
240 default=256,
241 )
242 warper = ConfigField(
243 doc="Configuration for the warper that warps the image and noise",
244 dtype=Warper.ConfigClass,
245 )
246 doWarpMaskedFraction = Field[bool](
247 doc="Warp the masked fraction image?",
248 default=False,
249 )
250 maskedFractionWarper = ConfigField(
251 doc="Configuration for the warp that warps the mask fraction image",
252 dtype=Warper.ConfigClass,
253 )
254 coaddPsf = ConfigField(
255 doc="Configuration for CoaddPsf",
256 dtype=CoaddPsfConfig,
257 )
258 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
259
260 # Use bgSubtracted and doApplySkyCorr to match the old MakeWarpConfig,
261 # but as properties instead of config fields.
262 @property
263 def bgSubtracted(self) -> bool:
264 return not self.doRevertOldBackground
265
266 @bgSubtracted.setter
267 def bgSubtracted(self, value: bool) -> None:
268 self.doRevertOldBackground = ~value
269
270 @property
271 def doApplySkyCorr(self) -> bool:
272 return self.doApplyNewBackground
273
274 @doApplySkyCorr.setter
275 def doApplySkyCorr(self, value: bool) -> None:
276 self.doApplyNewBackground = value
277
278 def setDefaults(self) -> None:
279 super().setDefaults()
280 self.warper.warpingKernelName = "lanczos3"
281 self.warper.cacheSize = 0
282 self.maskedFractionWarper.warpingKernelName = "bilinear"
283
284
285class MakeDirectWarpTask(PipelineTask):
286 """Warp single-detector images onto a common projection.
287
288 This task iterates over multiple images (corresponding to different
289 detectors) from a single visit that overlap the target patch. Pixels that
290 receive no input from any detector are set to NaN in the output image, and
291 NO_DATA bit is set in the mask plane.
292
293 This differs from the standard `MakeWarp` Task in the following
294 ways:
295
296 1. No selection on ccds at the time of warping. This is done later during
297 the coaddition stage.
298 2. Interpolate over a set of masked pixels before warping.
299 3. Generate an image where each pixel denotes how much of the pixel is
300 masked.
301 4. Generate multiple noise warps with the same interpolation applied.
302 5. No option to produce a PSF-matched warp.
303 """
304
305 ConfigClass = MakeDirectWarpConfig
306 _DefaultName = "makeDirectWarp"
307
308 def __init__(self, **kwargs):
309 super().__init__(**kwargs)
310 self.makeSubtask("inputRecorder")
311 self.makeSubtask("preWarpInterpolation")
312 if self.config.doSelectPreWarp:
313 self.makeSubtask("select")
314
315 self.warper = Warper.fromConfig(self.config.warper)
316 if self.config.doWarpMaskedFraction:
317 self.maskedFractionWarper = Warper.fromConfig(self.config.maskedFractionWarper)
318
319 def runQuantum(self, butlerQC, inputRefs, outputRefs):
320 # Docstring inherited.
321
322 # Read in all inputs.
323 inputs = butlerQC.get(inputRefs)
324
325 if not inputs["calexp_list"]:
326 raise NoWorkFound("No input warps provided for co-addition")
327
328 sky_map = inputs.pop("sky_map")
329
330 quantumDataId = butlerQC.quantum.dataId
331 sky_info = makeSkyInfo(
332 sky_map,
333 tractId=quantumDataId["tract"],
334 patchId=quantumDataId["patch"],
335 )
336
337 results = self.run(inputs, sky_info)
338 butlerQC.put(results, outputRefs)
339
340 def _preselect_inputs(self, inputs, sky_info):
341 dataIdList = [ref.dataId for ref in inputs["calexp_list"]]
342 visit_summary = inputs["visit_summary"]
343
344 bboxList, wcsList = [], []
345 for dataId in dataIdList:
346 row = visit_summary.find(dataId["detector"])
347 if row is None:
348 raise RuntimeError(f"Unexpectedly incomplete visit_summary: {dataId=} is missing.")
349 bboxList.append(row.getBBox())
350 wcsList.append(row.getWcs())
351
352 cornerPosList = Box2D(sky_info.bbox).getCorners()
353 coordList = [sky_info.wcs.pixelToSky(pos) for pos in cornerPosList]
354
355 goodIndices = self.select.run(
356 **inputs,
357 bboxList=bboxList,
358 wcsList=wcsList,
359 visitSummary=visit_summary,
360 coordList=coordList,
361 dataIds=dataIdList,
362 )
363 inputs = self._filterInputs(indices=goodIndices, inputs=inputs)
364
365 return inputs
366
367 def run(self, inputs, sky_info, **kwargs):
368 """Create a Warp dataset from inputs.
369
370 Parameters
371 ----------
372 inputs : `Mapping`
373 Dictionary of input datasets. It must have a list of input calexps
374 under the key "calexp_list". Other supported keys are
375 "background_revert_list" and "background_apply_list", corresponding
376 to the old and the new backgrounds to be reverted and applied to
377 the calexps. They must be in the same order as the calexps.
378 sky_info : `~lsst.pipe.base.Struct`
379 A Struct object containing wcs, bounding box, and other information
380 about the patches within the tract.
381 visit_summary : `~lsst.afw.table.ExposureCatalog` | None
382 Table of visit summary information. If provided, the visit summary
383 information will be used to update the calibration of the input
384 exposures. If None, the input exposures will be used as-is.
385
386 Returns
387 -------
388 results : `~lsst.pipe.base.Struct`
389 A Struct object containing the warped exposure, noise exposure(s),
390 and masked fraction image.
391 """
392
393 if self.config.doSelectPreWarp:
394 inputs = self._preselect_inputs(inputs, sky_info)
395 if not inputs["calexp_list"]:
396 raise NoWorkFound("No input warps remain after selection for co-addition")
397
398 sky_info.bbox.grow(self.config.border)
399 target_bbox, target_wcs = sky_info.bbox, sky_info.wcs
400
401 visit_summary = inputs["visit_summary"] if self.config.useVisitSummaryPsf else None
402
403 # Initialize the objects that will hold the warp.
404 final_warp = ExposureF(target_bbox, target_wcs)
405
406 exposures = inputs["calexp_list"]
407 background_revert_list = inputs.get("background_revert_list", [None] * len(exposures))
408 background_apply_list = inputs.get("background_apply_list", [None] * len(exposures))
409
410 visit_id = exposures[0].dataId["visit"]
411
412 # The warpExposure routine is expensive, and we do not want to call
413 # it twice (i.e., a second time for PSF-matched warps). We do not
414 # want to hold all the warped exposures in memory at once either.
415 # So we create empty exposure(s) to accumulate the warps of each type,
416 # and then process each detector serially.
417 final_warp = self._prepareEmptyExposure(sky_info)
418 final_masked_fraction_warp = self._prepareEmptyExposure(sky_info)
419 final_noise_warps = {
420 n_noise: self._prepareEmptyExposure(sky_info)
421 for n_noise in range(self.config.numberOfNoiseRealizations)
422 }
423
424 # We need a few bookkeeping variables only for the science coadd.
425 totalGoodPixels = 0
426 inputRecorder = self.inputRecorder.makeCoaddTempExpRecorder(
427 visit_id,
428 len(exposures),
429 )
430
431 for index, (calexp_ref, old_background, new_background) in enumerate(
432 zip(exposures, background_revert_list, background_apply_list, strict=True)
433 ):
434 dataId = calexp_ref.dataId
435 self.log.debug(
436 "Warping exposure %d/%d for id=%s",
437 index + 1,
438 len(exposures),
439 dataId,
440 )
441 calexp = calexp_ref.get()
442
443 # Generate noise image(s) in-situ.
444 seed = self.get_seed_from_data_id(dataId)
445 rng = np.random.RandomState(seed + self.config.seedOffset)
446
447 # Generate noise images in-situ.
448 noise_calexps = self.make_noise_exposures(calexp, rng)
449
450 # Warp the PSF before processing nad overwriting exposure.
451 xyTransform = makeWcsPairTransform(calexp.getWcs(), target_wcs)
452 psfWarped = WarpedPsf(calexp.getPsf(), xyTransform)
453
454 warpedExposure = self.process(
455 calexp,
456 target_wcs,
457 self.warper,
458 old_background,
459 new_background,
460 visit_summary,
461 destBBox=target_bbox,
462 )
463 warpedExposure.setPsf(psfWarped)
464
465 if final_warp.photoCalib is not None:
466 ratio = (
467 final_warp.photoCalib.getInstFluxAtZeroMagnitude()
468 / warpedExposure.photoCalib.getInstFluxAtZeroMagnitude()
469 )
470 else:
471 ratio = 1
472
473 self.log.debug("Scaling exposure %s by %f", dataId, ratio)
474 warpedExposure.maskedImage *= ratio
475
476 # Accumulate the partial warps in an online fashion.
477 nGood = copyGoodPixels(
478 final_warp.maskedImage,
479 warpedExposure.maskedImage,
480 final_warp.mask.getPlaneBitMask(["NO_DATA"]),
481 )
482
483 if final_warp.photoCalib is None and nGood > 0:
484 final_warp.setPhotoCalib(warpedExposure.photoCalib)
485
486 ccdId = self.config.idGenerator.apply(dataId).catalog_id
487 inputRecorder.addCalExp(calexp, ccdId, nGood)
488 totalGoodPixels += nGood
489
490 if self.config.doWarpMaskedFraction:
491 # Obtain the masked fraction exposure and warp it.
492 if self.config.doPreWarpInterpolation:
493 badMaskPlanes = self.preWarpInterpolation.config.badMaskPlanes
494 else:
495 badMaskPlanes = []
496 masked_fraction_exp = self._get_bad_mask(calexp, badMaskPlanes)
497
498 masked_fraction_warp = self.maskedFractionWarper.warpExposure(
499 target_wcs, masked_fraction_exp, destBBox=target_bbox
500 )
501
502 copyGoodPixels(
503 final_masked_fraction_warp.maskedImage,
504 masked_fraction_warp.maskedImage,
505 final_masked_fraction_warp.mask.getPlaneBitMask(["NO_DATA"]),
506 )
507
508 # Process and accumulate noise images.
509 for n_noise in range(self.config.numberOfNoiseRealizations):
510 noise_calexp = noise_calexps[n_noise]
511 warpedNoise = self.process(
512 noise_calexp,
513 target_wcs,
514 self.warper,
515 old_background,
516 new_background,
517 visit_summary,
518 destBBox=target_bbox,
519 )
520
521 warpedNoise.maskedImage *= ratio
522
523 copyGoodPixels(
524 final_noise_warps[n_noise].maskedImage,
525 warpedNoise.maskedImage,
526 final_noise_warps[n_noise].mask.getPlaneBitMask(["NO_DATA"]),
527 )
528
529 # If there are no good pixels, return a Struct filled with None.
530 if totalGoodPixels == 0:
531 results = Struct(
532 warp=None,
533 masked_fraction_warp=None,
534 )
535 for noise_index in range(self.config.numberOfNoiseRealizations):
536 setattr(results, f"noise_warp{noise_index}", None)
537
538 return results
539
540 # Finish the inputRecorder and add the coaddPsf to the final warp.
541 inputRecorder.finish(final_warp, totalGoodPixels)
542
543 coaddPsf = CoaddPsf(
544 inputRecorder.coaddInputs.ccds,
545 sky_info.wcs,
546 self.config.coaddPsf.makeControl(),
547 )
548
549 final_warp.setPsf(coaddPsf)
550 final_warp.setFilter(calexp.getFilter())
551 final_warp.getInfo().setVisitInfo(calexp.getInfo().getVisitInfo())
552
553 results = Struct(
554 warp=final_warp,
555 )
556
557 if self.config.doWarpMaskedFraction:
558 results.masked_fraction_warp = final_masked_fraction_warp.image
559
560 for noise_index, noise_exposure in final_noise_warps.items():
561 setattr(results, f"noise_warp{noise_index}", noise_exposure.maskedImage)
562
563 return results
564
565 def process(
566 self,
567 exposure,
568 target_wcs,
569 warper,
570 old_background=None,
571 new_background=None,
572 visit_summary=None,
573 maxBBox=None,
574 destBBox=None,
575 ):
576 """Process an exposure.
577
578 There are three processing steps that are applied to the input:
579
580 1. Interpolate over bad pixels before warping.
581 2. Apply all calibrations from visit_summary to the exposure.
582 3. Warp the exposure to the target coordinate system.
583
584 Parameters
585 ----------
586 exposure : `~lsst.afw.image.Exposure`
587 The input exposure to be processed.
588 target_wcs : `~lsst.afw.geom.SkyWcs`
589 The WCS of the target patch.
590 warper : `~lsst.afw.math.Warper`
591 The warper to use for warping the input exposure.
592 old_background : `~lsst.afw.image.Background` | None
593 The old background to be added back into the calexp.
594 new_background : `~lsst.afw.image.Background` | None
595 The new background to be subtracted from the calexp.
596 visit_summary : `~lsst.afw.table.ExposureCatalog` | None
597 Table of visit summary information. If not None, the visit_summary
598 information will be used to update the calibration of the input
599 exposures. Otherwise, the input exposures will be used as-is.
600 maxBBox : `~lsst.geom.Box2I` | None
601 Maximum bounding box of the warped exposure. If None, this is
602 determined automatically.
603 destBBox : `~lsst.geom.Box2I` | None
604 Exact bounding box of the warped exposure. If None, this is
605 determined automatically.
606
607 Returns
608 -------
609 warped_exposure : `~lsst.afw.image.Exposure`
610 The processed and warped exposure.
611 """
612
613 if self.config.doPreWarpInterpolation:
614 self.preWarpInterpolation.run(exposure.maskedImage)
615
616 self._apply_all_calibrations(
617 exposure,
618 old_background,
619 new_background,
620 logger=self.log,
621 visit_summary=visit_summary,
622 includeScaleUncertainty=self.config.includeCalibVar,
623 )
624 with self.timer("warp"):
625 warped_exposure = warper.warpExposure(
626 target_wcs,
627 exposure,
628 maxBBox=maxBBox,
629 destBBox=destBBox,
630 )
631
632 # Potentially a post-warp interpolation here? Relies on DM-38630.
633
634 return warped_exposure
635
636 @staticmethod
637 def _filterInputs(indices, inputs):
638 """Filter inputs by their indices.
639
640 This method down-selects the list entries in ``inputs`` dictionary by
641 keeping only those items in the lists that correspond to ``indices``.
642 This is intended to select input visits that go into the warps.
643
644 Parameters
645 ----------
646 indices : `list` [`int`]
647 inputs : `dict`
648 A dictionary of input connections to be passed to run.
649
650 Returns
651 -------
652 inputs : `dict`
653 Task inputs with their lists filtered by indices.
654 """
655 for key in inputs.keys():
656 # Only down-select on list inputs
657 if isinstance(inputs[key], list):
658 inputs[key] = [inputs[key][ind] for ind in indices]
659
660 return inputs
661
662 def _apply_all_calibrations(
663 self,
664 exp: Exposure,
665 old_background,
666 new_background,
667 logger,
668 visit_summary: ExposureCatalog | None = None,
669 includeScaleUncertainty: bool = False,
670 ) -> None:
671 """Apply all of the calibrations from visit_summary to the exposure.
672
673 Specifically, this method updates the following (if available) to the
674 input exposure ``exp`` in place from ``visit_summary``:
675
676 - Aperture correction map
677 - Photometric calibration
678 - PSF
679 - WCS
680
681 Parameters
682 ----------
683 exp : `~lsst.afw.image.Exposure`
684 Exposure to be updated.
685 old_background : `~lsst.afw.image.Exposure`
686 Exposure corresponding to the old background, to be added back.
687 new_background : `~lsst.afw.image.Exposure`
688 Exposure corresponding to the new background, to be subtracted.
689 logger : `logging.Logger`
690 Logger object from the caller Task to write the logs onto.
691 visit_summary : `~lsst.afw.table.ExposureCatalog` | None
692 Table of visit summary information. If not None, the visit summary
693 information will be used to update the calibration of the input
694 exposures. Otherwise, the input exposures will be used as-is.
695 includeScaleUncertainty : bool
696 Whether to include the uncertainty on the calibration in the
697 resulting variance? Passed onto the `calibrateImage` method of the
698 PhotoCalib object attached to ``exp``.
699
700 Raises
701 ------
702 RuntimeError
703 Raised if ``visit_summary`` is provided but does not contain a
704 record corresponding to ``exp``.
705 """
706 if old_background:
707 exp.maskedImage += old_background.getImage()
708
709 if self.config.useVisitSummaryPsf:
710 detector = exp.info.getDetector().getId()
711 row = visit_summary.find(detector)
712
713 if row is None:
714 raise RuntimeError(f"Unexpectedly incomplete visit_summary: {detector=} is missing.")
715
716 if photo_calib := row.getPhotoCalib():
717 exp.setPhotoCalib(photo_calib)
718 else:
719 logger.warning(
720 "No photometric calibration found in visit summary for detector = %s.",
721 detector,
722 )
723
724 if wcs := row.getWcs():
725 exp.setWcs(wcs)
726 else:
727 logger.warning("No WCS found in visit summary for detector = %s.", detector)
728
729 if psf := row.getPsf():
730 exp.setPsf(psf)
731 else:
732 logger.warning("No PSF found in visit summary for detector = %s.", detector)
733
734 if apcorr_map := row.getApCorrMap():
735 exp.setApCorrMap(apcorr_map)
736 else:
737 logger.warning(
738 "No aperture correction map found in visit summary for detector = %s.",
739 detector,
740 )
741
742 if new_background:
743 exp.maskedImage -= new_background.getImage()
744
745 # Calibrate the (masked) image.
746 # This should likely happen even if visit_summary is None.
747 photo_calib = exp.photoCalib
748 exp.maskedImage = photo_calib.calibrateImage(
749 exp.maskedImage, includeScaleUncertainty=includeScaleUncertainty
750 )
751 exp.maskedImage /= photo_calib.getCalibrationMean()
752
753 # This method is copied from makeWarp.py
754 @classmethod
755 def _prepareEmptyExposure(cls, sky_info):
756 """Produce an empty exposure for a given patch.
757
758 Parameters
759 ----------
760 sky_info : `lsst.pipe.base.Struct`
761 Struct from `~lsst.pipe.base.coaddBase.makeSkyInfo` with
762 geometric information about the patch.
763
764 Returns
765 -------
766 exp : `lsst.afw.image.exposure.ExposureF`
767 An empty exposure for a given patch.
768 """
769 exp = ExposureF(sky_info.bbox, sky_info.wcs)
770 exp.getMaskedImage().set(np.nan, Mask.getPlaneBitMask("NO_DATA"), np.inf)
771 return exp
772
773 @staticmethod
774 def compute_median_variance(mi: MaskedImage) -> float:
775 """Compute the median variance across the good pixels of a MaskedImage.
776
777 Parameters
778 ----------
779 mi : `~lsst.afw.image.MaskedImage`
780 The input image on which to compute the median variance.
781
782 Returns
783 -------
784 median_variance : `float`
785 Median variance of the input calexp.
786 """
787 # Shouldn't this exclude pixels that are masked, to be safe?
788 # This is implemented as it was in descwl_coadd.
789 return np.median(mi.variance.array[np.isfinite(mi.variance.array) & np.isfinite(mi.image.array)])
790
791 def get_seed_from_data_id(self, data_id) -> int:
792 """Get a seed value given a data_id.
793
794 This method generates a unique, reproducible pseudo-random number for
795 a data id. This is not affected by ordering of the input, or what
796 set of visits, ccds etc. are given.
797
798 This is implemented as a public method, so that simulations that
799 don't necessary deal with the middleware can mock up a ``data_id``
800 instance, or override this method with a different one to obtain a
801 seed value consistent with the pipeline task.
802
803 Parameters
804 ----------
805 data_id : `~lsst.daf.butler.DataCoordinate`
806 Data identifier dictionary.
807
808 Returns
809 -------
810 seed : `int`
811 A unique seed for this data_id to seed a random number generator.
812 """
813 return self.config.idGenerator.apply(data_id).catalog_id
814
815 def make_noise_exposures(self, calexp: ExposureF, rng) -> dict[int, ExposureF]:
816 """Make pure noise realizations based on ``calexp``.
817
818 Parameters
819 ----------
820 calexp : `~lsst.afw.image.ExposureF`
821 The input exposure on which to base the noise realizations.
822 rng : `np.random.RandomState`
823 Random number generator to use for the noise realizations.
824
825 Returns
826 -------
827 noise_calexps : `dict` [`int`, `~lsst.afw.image.ExposureF`]
828 A mapping of integers ranging from 0 up to
829 config.numberOfNoiseRealizations to the corresponding
830 noise realization exposures.
831 """
832 noise_calexps = {}
833
834 # If no noise exposures are requested, return the empty dictionary
835 # without any further computations.
836 if self.config.numberOfNoiseRealizations == 0:
837 return noise_calexps
838
839 if self.config.useMedianVariance:
840 variance = self.compute_median_variance(calexp.maskedImage)
841 else:
842 variance = calexp.variance.array
843
844 for n_noise in range(self.config.numberOfNoiseRealizations):
845 noise_calexp = calexp.clone()
846 noise_calexp.image.array[:, :] = rng.normal(
847 scale=np.sqrt(variance),
848 size=noise_calexp.image.array.shape,
849 )
850 noise_calexp.variance.array[:, :] = variance
851 noise_calexps[n_noise] = noise_calexp
852
853 return noise_calexps
854
855 @classmethod
856 def _get_bad_mask(cls, exp: ExposureF, badMaskPlanes: Iterable[str]) -> ExposureF:
857 """Get an Exposure of bad mask
858
859 Parameters
860 ----------
861 exp: `lsst.afw.image.Exposure`
862 The exposure data.
863 badMaskPlanes: `list` [`str`]
864 List of mask planes to be considered as bad.
865
866 Returns
867 -------
868 bad_mask: `~lsst.afw.image.Exposure`
869 An Exposure with boolean array with True if inverse variance <= 0
870 or if any of the badMaskPlanes bits are set, and False otherwise.
871 """
872
873 bad_mask = exp.clone()
874
875 var = exp.variance.array
876 mask = exp.mask.array
877
878 bitMask = exp.mask.getPlaneBitMask(badMaskPlanes)
879
880 bad_mask.image.array[:, :] = (var < 0) | np.isinf(var) | ((mask & bitMask) != 0)
881
882 bad_mask.variance.array *= 0.0
883
884 return bad_mask
A floating-point coordinate rectangle geometry.
Definition Box.h:413
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