LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
make_psf_matched_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
24__all__ = (
25 "growValidPolygons",
26 "MakePsfMatchedWarpConfig",
27 "MakePsfMatchedWarpConnections",
28 "MakePsfMatchedWarpTask",
29)
30
31from typing import TYPE_CHECKING
32
33import lsst.geom as geom
34import numpy as np
35import warnings
36
37from lsst.afw.geom import Polygon, makeWcsPairTransform, SinglePolygonException
38from lsst.coadd.utils import copyGoodPixels
39from lsst.ip.diffim import ModelPsfMatchTask
40from lsst.meas.algorithms import GaussianPsfFactory, WarpedPsf
41from lsst.pex.config import ConfigurableField
42from lsst.pipe.base import (
43 PipelineTask,
44 PipelineTaskConfig,
45 PipelineTaskConnections,
46 Struct,
47)
48from lsst.pipe.base.connectionTypes import Input, Output
49from lsst.pipe.tasks.coaddBase import makeSkyInfo
50from lsst.skymap import BaseSkyMap
51from lsst.utils.timer import timeMethod
52
53if TYPE_CHECKING:
54 from lsst.afw.image import Exposure
55
56
58 PipelineTaskConnections,
59 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
60 defaultTemplates={
61 "coaddName": "deep",
62 },
63):
64 """Connections for MakePsfMatchedWarpTask"""
65 sky_map = Input(
66 doc="Input definition of geometry/bbox and projection/wcs for warps.",
67 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
68 storageClass="SkyMap",
69 dimensions=("skymap",),
70 )
71
72 direct_warp = Input(
73 doc="Direct warped exposure produced by resampling calexps onto the skyMap patch geometry",
74 name="{coaddName}Coadd_directWarp",
75 storageClass="ExposureF",
76 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
77 )
78
79 psf_matched_warp = Output(
80 doc=(
81 "Output PSF-Matched warped exposure, produced by resampling ",
82 "calexps onto the skyMap patch geometry and PSF-matching to a model PSF.",
83 ),
84 name="{coaddName}Coadd_psfMatchedWarp",
85 storageClass="ExposureF",
86 dimensions=("tract", "patch", "skymap", "visit", "instrument"),
87 )
88
89
90class MakePsfMatchedWarpConfig(
91 PipelineTaskConfig,
92 pipelineConnections=MakePsfMatchedWarpConnections,
93):
94 """Config for MakePsfMatchedWarpTask."""
95
96 modelPsf = GaussianPsfFactory.makeField(doc="Model Psf factory")
97 psfMatch = ConfigurableField(
98 target=ModelPsfMatchTask,
99 doc="Task to warp and PSF-match calexp",
100 )
101
102 def setDefaults(self):
103 super().setDefaults()
104 self.psfMatch.kernel["AL"].alardSigGauss = [1.0, 2.0, 4.5]
105 self.modelPsf.defaultFwhm = 7.7
106
107
108class MakePsfMatchedWarpTask(PipelineTask):
109 ConfigClass = MakePsfMatchedWarpConfig
110 _DefaultName = "makePsfMatchedWarp"
111
112 def __init__(self, **kwargs):
113 super().__init__(**kwargs)
114 self.makeSubtask("psfMatch")
115
116 def runQuantum(self, butlerQC, inputRefs, outputRefs):
117 # Docstring inherited.
118
119 # Read in all inputs.
120 inputs = butlerQC.get(inputRefs)
121
122 sky_map = inputs.pop("sky_map")
123
124 quantumDataId = butlerQC.quantum.dataId
125 sky_info = makeSkyInfo(
126 sky_map,
127 tractId=quantumDataId["tract"],
128 patchId=quantumDataId["patch"],
129 )
130
131 results = self.run(inputs["direct_warp"], sky_info.bbox)
132 butlerQC.put(results, outputRefs)
133
134 @timeMethod
135 def run(self, direct_warp: Exposure, bbox: geom.Box2I):
136 """Make a PSF-matched warp from a direct warp.
137
138 Each individual detector from the direct warp is isolated, one at a
139 time, and PSF-matched to the same model PSF. The PSF-matched images are
140 then added back together to form the final PSF-matched warp. The bulk
141 of the work is done by the `psfMatchTask`.
142
143 Notes
144 -----
145 Pixels that receive no inputs are set to NaN, for e.g, chip gaps. This
146 violates LSST algorithms group policy.
147
148 Parameters
149 ----------
150 direct_warp : `lsst.afw.image.Exposure`
151 Direct warp to be PSF-matched.
152
153 Returns
154 -------
155 struct : `lsst.pipe.base.Struct`
156 Struct containing the PSF-matched warp under the attribute
157 `psf_matched_warp`.
158 """
159 modelPsf = self.config.modelPsf.apply()
160
161 # Prepare the output exposure. We clone the input image to keep the
162 # metadata, but reset the image and variance plans.
163 exposure_psf_matched = direct_warp[bbox].clone()
164 exposure_psf_matched.image.array[:, :] = np.nan
165 exposure_psf_matched.variance.array[:, :] = np.inf
166 exposure_psf_matched.setPsf(modelPsf)
167
168 bit_mask = direct_warp.mask.getPlaneBitMask("NO_DATA")
169 total_good_pixels = 0 # Total number of pixels copied to output.
170
171 for row in direct_warp.info.getCoaddInputs().ccds:
172 transform = makeWcsPairTransform(row.wcs, direct_warp.wcs)
173 warp_psf = WarpedPsf(row.getPsf(), transform)
174
175 if (src_polygon := row.validPolygon) is None:
176 # Calculate the polygon for this detector.
177 src_polygon = Polygon(
178 [geom.Point2D(corner) for corner in row.getBBox().getCorners()]
179 )
180 self.log.debug("Polygon for detector=%d is calculated as %s",
181 row["ccd"],
182 src_polygon
183 )
184 else:
185 self.log.debug("Polygon for detector=%d is read from the input calexp as %s",
186 row["ccd"],
187 src_polygon
188 )
189
190 try:
191 destination_polygon = src_polygon.transform(transform).intersectionSingle(
192 geom.Box2D(direct_warp.getBBox())
193 )
194 except SinglePolygonException:
195 self.log.info(
196 "Skipping CCD %d as its polygon does not intersect the direct warp",
197 row["ccd"],
198 )
199 continue
200
201 # Compute the minimum possible bounding box that overlaps the CCD.
202 # First find the intersection polygon between the per-detector warp
203 # and the warp bounding box.
204 bbox = geom.Box2I()
205 for corner in destination_polygon.getVertices():
206 bbox.include(geom.Point2I(corner))
207 bbox.clip(direct_warp.getBBox()) # Additional safeguard
208
209 # Because the direct warps are larger, it is possible that after
210 # clipping, `bbox` lies outside PSF-matched warp's bounding box.
211 if not bbox.overlaps(exposure_psf_matched.getBBox()):
212 self.log.debug(
213 "Skipping CCD %d as its bbox %s does not overlap the PSF-matched warp",
214 row["ccd"],
215 bbox,
216 )
217 continue
218
219 self.log.debug("PSF-matching CCD %d with bbox %s", row["ccd"], bbox)
220
221 ccd_mask_array = ~(destination_polygon.createImage(bbox).array <= 0)
222
223 # Clone the subimage, set the PSF to the model and reset the planes
224 # outside the detector.
225 temp_warp = direct_warp[bbox].clone()
226 temp_warp.setPsf(warp_psf)
227 temp_warp.image.array *= ccd_mask_array
228 temp_warp.mask.array |= (~ccd_mask_array) * bit_mask
229 # We intend to divide by zero outside the detector to set the
230 # per-pixel variance values to infinity. Suppress the warning.
231 with warnings.catch_warnings():
232 warnings.filterwarnings("ignore", message="divide by zero", category=RuntimeWarning)
233 temp_warp.variance.array /= ccd_mask_array
234
235 temp_psf_matched = self.psfMatch.run(temp_warp, modelPsf).psfMatchedExposure
236 del temp_warp
237
238 # Set pixels outside the intersection polygon to NO_DATA.
239 temp_psf_matched.maskedImage[bbox].mask.array |= (~ccd_mask_array) * bit_mask
240
241 # Clip the bbox to the PSF-matched warp bounding box.
242 bbox.clip(exposure_psf_matched.getBBox())
243
244 num_good_pixels = copyGoodPixels(
245 exposure_psf_matched.maskedImage[bbox],
246 temp_psf_matched.maskedImage[bbox],
247 bit_mask,
248 )
249
250 del temp_psf_matched
251
252 self.log.info(
253 "Copied %d pixels from CCD %d to exposure_psf_matched", num_good_pixels, row["ccd"],
254 )
255 total_good_pixels += num_good_pixels
256
257 self.log.info("Total number of good pixels = %d", total_good_pixels)
258
259 if total_good_pixels > 0:
260 growValidPolygons(
261 exposure_psf_matched.info.getCoaddInputs(),
262 -self.config.psfMatch.kernel.active.kernelSize // 2
263 )
264
265 return Struct(psf_matched_warp=exposure_psf_matched)
266 else:
267 return Struct(psf_matched_warp=None)
268
269
270# Note that this is implemented as a free-floating function to enable reuse in
271# makeWarp.py without creating any relationships between the two classes.
272# This may be converted to a method after makeWarp.py is removed altogether.
273def growValidPolygons(coaddInputs, growBy: int) -> None:
274 """Grow coaddInputs' ccds' ValidPolygons in place.
275
276 Either modify each ccd's validPolygon in place, or if CoaddInputs
277 does not have a validPolygon, create one from its bbox.
278
279 Parameters
280 ----------
281 coaddInputs : `lsst.afw.image.coaddInputs`
282 CoaddInputs object containing the ccds to grow the valid polygons of.
283 growBy : `int`
284 The value to grow the valid polygons by.
285
286 Notes
287 -----
288 Negative values for ``growBy`` can shrink the polygons.
289 """
290 for ccd in coaddInputs.ccds:
291 polyOrig = ccd.getValidPolygon()
292 validPolyBBox = polyOrig.getBBox() if polyOrig else ccd.getBBox()
293 validPolyBBox.grow(growBy)
294 if polyOrig:
295 validPolygon = polyOrig.intersectionSingle(validPolyBBox)
296 else:
297 validPolygon = Polygon(geom.Box2D(validPolyBBox))
298
299 ccd.validPolygon = validPolygon
Cartesian polygons.
Definition Polygon.h:59
A floating-point coordinate rectangle geometry.
Definition Box.h:413
An integer coordinate rectangle.
Definition Box.h:55
A Psf class that maps an arbitrary Psf through a coordinate transformation.
Definition WarpedPsf.h:52