Coverage for python/lsst/cp/pipe/cpSkyTask.py: 56%
100 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-13 03:01 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-13 03:01 -0700
1# This file is part of cp_pipe.
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 <http://www.gnu.org/licenses/>.
21import numpy as np
23import lsst.pex.config as pexConfig
24import lsst.pipe.base as pipeBase
25import lsst.pipe.base.connectionTypes as cT
27from lsst.pipe.tasks.background import (FocalPlaneBackground, MaskObjectsTask, SkyMeasurementTask,
28 FocalPlaneBackgroundConfig)
29from lsst.daf.base import PropertyList
30from ._lookupStaticCalibration import lookupStaticCalibration
31from .cpCombine import CalibCombineTask
33__all__ = ['CpSkyImageTask', 'CpSkyImageConfig',
34 'CpSkyScaleMeasureTask', 'CpSkyScaleMeasureConfig',
35 'CpSkySubtractBackgroundTask', 'CpSkySubtractBackgroundConfig',
36 'CpSkyCombineTask', 'CpSkyCombineConfig']
39class CpSkyImageConnections(pipeBase.PipelineTaskConnections,
40 dimensions=("instrument", "physical_filter", "exposure", "detector")):
41 inputExp = cT.Input(
42 name="postISRCCD",
43 doc="Input pre-processed exposures to combine.",
44 storageClass="Exposure",
45 dimensions=("instrument", "exposure", "detector"),
46 )
47 camera = cT.PrerequisiteInput(
48 name="camera",
49 doc="Input camera to use for geometry.",
50 storageClass="Camera",
51 dimensions=("instrument",),
52 lookupFunction=lookupStaticCalibration,
53 isCalibration=True,
54 )
56 maskedExp = cT.Output(
57 name="cpSkyMaskedIsr",
58 doc="Output masked post-ISR exposure.",
59 storageClass="Exposure",
60 dimensions=("instrument", "exposure", "detector"),
61 )
62 maskedBkg = cT.Output(
63 name="cpSkyDetectorBackground",
64 doc="Initial background model from one image.",
65 storageClass="FocalPlaneBackground",
66 dimensions=("instrument", "exposure", "detector"),
67 )
70class CpSkyImageConfig(pipeBase.PipelineTaskConfig,
71 pipelineConnections=CpSkyImageConnections):
72 maskTask = pexConfig.ConfigurableField(
73 target=MaskObjectsTask,
74 doc="Object masker to use.",
75 )
77 maskThresh = pexConfig.Field(
78 dtype=float,
79 default=3.0,
80 doc="k-sigma threshold for masking pixels.",
81 )
82 maskList = pexConfig.ListField(
83 dtype=str,
84 default=["DETECTED", "BAD", "NO_DATA", "SAT"],
85 doc="Mask planes to reject.",
86 )
88 largeScaleBackground = pexConfig.ConfigField(
89 dtype=FocalPlaneBackgroundConfig,
90 doc="Large-scale background configuration.",
91 )
93 def setDefaults(self):
94 # These values correspond to the HSC recommendation. As noted
95 # below, the sizes are in millimeters, and correspond to an
96 # background image 8192x8192 pixels (8192*0.015=122.88).
97 self.largeScaleBackground.xSize = 122.88 # in mm
98 self.largeScaleBackground.ySize = 122.88 # in mm
99 self.largeScaleBackground.pixelSize = 0.015 # in mm per pixel
100 self.largeScaleBackground.minFrac = 0.1
101 self.largeScaleBackground.mask = ['BAD', 'SAT', 'INTRP', 'DETECTED', 'DETECTED_NEGATIVE',
102 'EDGE', 'NO_DATA']
105class CpSkyImageTask(pipeBase.PipelineTask):
106 """Mask the detections on the postISRCCD.
108 This task maps the MaskObjectsTask across all of the initial ISR
109 processed cpSkyIsr images to create cpSkyMaskedIsr products for
110 all (exposure, detector) values.
111 """
113 ConfigClass = CpSkyImageConfig
114 _DefaultName = "CpSkyImage"
116 def __init__(self, **kwargs):
117 super().__init__(**kwargs)
118 self.makeSubtask("maskTask")
120 def run(self, inputExp, camera):
121 """Mask the detections on the postISRCCD.
123 Parameters
124 ----------
125 inputExp : `lsst.afw.image.Exposure`
126 An ISR processed exposure that will have detections
127 masked.
128 camera : `lsst.afw.cameraGeom.Camera`
129 The camera geometry for this exposure. This is needed to
130 create the background model.
132 Returns
133 -------
134 results : `lsst.pipe.base.Struct`
135 The results struct containing:
137 ``maskedExp`` : `lsst.afw.image.Exposure`
138 The detection-masked version of the ``inputExp``.
139 ``maskedBkg`` : `lsst.pipe.tasks.background.FocalPlaneBackground`
140 The partial focal plane background containing only
141 this exposure/detector's worth of data.
142 """
143 # As constructCalibs.py SkyTask.processSingleBackground()
144 # Except: check if a detector is fully masked to avoid
145 # self.maskTask raising.
146 currentMask = inputExp.getMask()
147 badMask = currentMask.getPlaneBitMask(self.config.maskList)
148 if (currentMask.getArray() & badMask).all():
149 self.log.warning("All pixels are masked!")
150 else:
151 self.maskTask.run(inputExp, self.config.maskList)
153 # As constructCalibs.py SkyTask.measureBackground()
154 bgModel = FocalPlaneBackground.fromCamera(self.config.largeScaleBackground, camera)
155 bgModel.addCcd(inputExp)
157 return pipeBase.Struct(
158 maskedExp=inputExp,
159 maskedBkg=bgModel,
160 )
163class CpSkyScaleMeasureConnections(pipeBase.PipelineTaskConnections,
164 dimensions=("instrument", "physical_filter", "exposure")):
165 inputBkgs = cT.Input(
166 name="cpSkyDetectorBackground",
167 doc="Initial background model from one exposure/detector",
168 storageClass="FocalPlaneBackground",
169 dimensions=("instrument", "exposure", "detector"),
170 multiple=True
171 )
173 outputBkg = cT.Output(
174 name="cpSkyExpBackground",
175 doc="Background model for a full exposure.",
176 storageClass="FocalPlaneBackground",
177 dimensions=("instrument", "exposure"),
178 )
179 outputScale = cT.Output(
180 name="cpSkyExpScale",
181 doc="Scale for the full exposure.",
182 storageClass="PropertyList",
183 dimensions=("instrument", "exposure"),
184 )
187class CpSkyScaleMeasureConfig(pipeBase.PipelineTaskConfig,
188 pipelineConnections=CpSkyScaleMeasureConnections):
189 # There are no configurable parameters here.
190 pass
193class CpSkyScaleMeasureTask(pipeBase.PipelineTask):
194 """Measure per-exposure scale factors and merge focal plane backgrounds.
196 Merge all the per-detector partial backgrounds to a full focal
197 plane background for each exposure, and measure the scale factor
198 from that full background.
199 """
201 ConfigClass = CpSkyScaleMeasureConfig
202 _DefaultName = "cpSkyScaleMeasure"
204 def run(self, inputBkgs):
205 """Merge focal plane backgrounds and measure the scale factor.
207 Parameters
208 ----------
209 inputBkgs : `list` [`lsst.pipe.tasks.background.FocalPlaneBackground`]
210 A list of all of the partial focal plane backgrounds, one
211 from each detector in this exposure.
213 Returns
214 -------
215 results : `lsst.pipe.base.Struct`
216 The results struct containing:
218 ``outputBkg`` : `lsst.pipe.tasks.background.FocalPlaneBackground`
219 The full merged background for the entire focal plane.
220 ``outputScale`` : `lsst.daf.base.PropertyList`
221 A metadata containing the median level of the
222 background, stored in the key 'scale'.
223 """
224 # As constructCalibs.py SkyTask.scatterProcess()
225 # Merge into the full focal plane.
226 background = inputBkgs[0]
227 for bg in inputBkgs[1:]:
228 background.merge(bg)
230 backgroundPixels = background.getStatsImage().getArray()
231 self.log.info("Background model min/max: %f %f. Scale %f",
232 np.min(backgroundPixels), np.max(backgroundPixels),
233 np.median(backgroundPixels))
235 # A property list is overkill, but FocalPlaneBackground
236 # doesn't have a metadata object that this can be stored in.
237 scale = np.median(background.getStatsImage().getArray())
238 scaleMD = PropertyList()
239 scaleMD.set("scale", float(scale))
241 return pipeBase.Struct(
242 outputBkg=background,
243 outputScale=scaleMD,
244 )
247class CpSkySubtractBackgroundConnections(pipeBase.PipelineTaskConnections,
248 dimensions=("instrument", "physical_filter",
249 "exposure", "detector")):
250 inputExp = cT.Input(
251 name="cpSkyMaskedIsr",
252 doc="Masked post-ISR image.",
253 storageClass="Exposure",
254 dimensions=("instrument", "exposure", "detector"),
255 )
256 inputBkg = cT.Input(
257 name="cpSkyExpBackground",
258 doc="Background model for the full exposure.",
259 storageClass="FocalPlaneBackground",
260 dimensions=("instrument", "exposure"),
261 )
262 inputScale = cT.Input(
263 name="cpSkyExpScale",
264 doc="Scale for the full exposure.",
265 storageClass="PropertyList",
266 dimensions=("instrument", "exposure"),
267 )
269 outputBkg = cT.Output(
270 name="cpExpBackground",
271 doc="Normalized, static background.",
272 storageClass="Background",
273 dimensions=("instrument", "exposure", "detector"),
274 )
277class CpSkySubtractBackgroundConfig(pipeBase.PipelineTaskConfig,
278 pipelineConnections=CpSkySubtractBackgroundConnections):
279 sky = pexConfig.ConfigurableField(
280 target=SkyMeasurementTask,
281 doc="Sky measurement",
282 )
285class CpSkySubtractBackgroundTask(pipeBase.PipelineTask):
286 """Subtract per-exposure background from individual detector masked images.
288 The cpSkyMaskedIsr images constructed by CpSkyImageTask have the
289 scaled background constructed by CpSkyScaleMeasureTask subtracted,
290 and new background models are constructed for the remaining
291 signal.
293 The output was called `icExpBackground` in gen2, but the product
294 created here has definition clashes that prevent that from being
295 reused.
296 """
298 ConfigClass = CpSkySubtractBackgroundConfig
299 _DefaultName = "cpSkySubtractBkg"
301 def __init__(self, **kwargs):
302 super().__init__(**kwargs)
303 self.makeSubtask("sky")
305 def run(self, inputExp, inputBkg, inputScale):
306 """Subtract per-exposure background from individual detector masked
307 images.
309 Parameters
310 ----------
311 inputExp : `lsst.afw.image.Exposure`
312 The ISR processed, detection masked image.
313 inputBkg : `lsst.pipe.tasks.background.FocalPlaneBackground.
314 Full focal plane background for this exposure.
315 inputScale : `lsst.daf.base.PropertyList`
316 Metadata containing the scale factor.
318 Returns
319 -------
320 results : `lsst.pipe.base.Struct`
321 The results struct containing:
323 ``outputBkg``
324 Remnant sky background with the full-exposure
325 component removed. (`lsst.afw.math.BackgroundList`)
326 """
327 # As constructCalibs.py SkyTask.processSingle()
328 image = inputExp.getMaskedImage()
329 detector = inputExp.getDetector()
330 bbox = image.getBBox()
332 scale = inputScale.get('scale')
333 background = inputBkg.toCcdBackground(detector, bbox)
334 image -= background.getImage()
335 image /= scale
337 newBackground = self.sky.measureBackground(image)
338 return pipeBase.Struct(
339 outputBkg=newBackground
340 )
343class CpSkyCombineConnections(pipeBase.PipelineTaskConnections,
344 dimensions=("instrument", "physical_filter", "detector")):
345 inputBkgs = cT.Input(
346 name="cpExpBackground",
347 doc="Normalized, static background.",
348 storageClass="Background",
349 dimensions=("instrument", "exposure", "detector"),
350 multiple=True,
351 )
352 inputExpHandles = cT.Input(
353 name="cpSkyMaskedIsr",
354 doc="Masked post-ISR image.",
355 storageClass="Exposure",
356 dimensions=("instrument", "exposure", "detector"),
357 multiple=True,
358 deferLoad=True,
359 )
361 outputCalib = cT.Output(
362 name="sky",
363 doc="Averaged static background.",
364 storageClass="ExposureF",
365 dimensions=("instrument", "detector", "physical_filter"),
366 isCalibration=True,
367 )
370class CpSkyCombineConfig(pipeBase.PipelineTaskConfig,
371 pipelineConnections=CpSkyCombineConnections):
372 sky = pexConfig.ConfigurableField(
373 target=SkyMeasurementTask,
374 doc="Sky measurement",
375 )
378class CpSkyCombineTask(pipeBase.PipelineTask):
379 """Merge per-exposure measurements into a detector level calibration.
381 Each of the per-detector results from all input exposures are
382 averaged to produce the final SKY calibration.
384 As before, this is written to a skyCalib instead of a SKY to avoid
385 definition classes in gen3.
386 """
388 ConfigClass = CpSkyCombineConfig
389 _DefaultName = "cpSkyCombine"
391 def __init__(self, **kwargs):
392 super().__init__(**kwargs)
393 self.makeSubtask("sky")
395 def run(self, inputBkgs, inputExpHandles):
396 """Merge per-exposure measurements into a detector level calibration.
398 Parameters
399 ----------
400 inputBkgs : `list` [`lsst.afw.math.BackgroundList`]
401 Remnant backgrounds from each exposure.
402 inputHandles : `list` [`lsst.daf.butler.DeferredDatasetHandles`]
403 The Butler handles to the ISR processed, detection masked images.
405 Returns
406 -------
407 results : `lsst.pipe.base.Struct`
408 The results struct containing:
410 `outputCalib` : `lsst.afw.image.Exposure`
411 The final sky calibration product.
412 """
413 skyCalib = self.sky.averageBackgrounds(inputBkgs)
414 skyCalib.setDetector(inputExpHandles[0].get(component='detector'))
415 skyCalib.setFilter(inputExpHandles[0].get(component='filter'))
417 CalibCombineTask().combineHeaders(inputExpHandles, skyCalib, calibType='SKY')
419 return pipeBase.Struct(
420 outputCalib=skyCalib,
421 )