Coverage for python/lsst/cp/pipe/cpSkyTask.py: 55%
99 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-05 13:48 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-05 13:48 +0000
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 .cpCombine import CalibCombineTask
32__all__ = ['CpSkyImageTask', 'CpSkyImageConfig',
33 'CpSkyScaleMeasureTask', 'CpSkyScaleMeasureConfig',
34 'CpSkySubtractBackgroundTask', 'CpSkySubtractBackgroundConfig',
35 'CpSkyCombineTask', 'CpSkyCombineConfig']
38class CpSkyImageConnections(pipeBase.PipelineTaskConnections,
39 dimensions=("instrument", "physical_filter", "exposure", "detector")):
40 inputExp = cT.Input(
41 name="postISRCCD",
42 doc="Input pre-processed exposures to combine.",
43 storageClass="Exposure",
44 dimensions=("instrument", "exposure", "detector"),
45 )
46 camera = cT.PrerequisiteInput(
47 name="camera",
48 doc="Input camera to use for geometry.",
49 storageClass="Camera",
50 dimensions=("instrument",),
51 isCalibration=True,
52 )
54 maskedExp = cT.Output(
55 name="cpSkyMaskedIsr",
56 doc="Output masked post-ISR exposure.",
57 storageClass="Exposure",
58 dimensions=("instrument", "exposure", "detector"),
59 )
60 maskedBkg = cT.Output(
61 name="cpSkyDetectorBackground",
62 doc="Initial background model from one image.",
63 storageClass="FocalPlaneBackground",
64 dimensions=("instrument", "exposure", "detector"),
65 )
68class CpSkyImageConfig(pipeBase.PipelineTaskConfig,
69 pipelineConnections=CpSkyImageConnections):
70 maskTask = pexConfig.ConfigurableField(
71 target=MaskObjectsTask,
72 doc="Object masker to use.",
73 )
75 maskThresh = pexConfig.Field(
76 dtype=float,
77 default=3.0,
78 doc="k-sigma threshold for masking pixels.",
79 )
80 maskList = pexConfig.ListField(
81 dtype=str,
82 default=["DETECTED", "BAD", "NO_DATA", "SAT"],
83 doc="Mask planes to reject.",
84 )
86 largeScaleBackground = pexConfig.ConfigField(
87 dtype=FocalPlaneBackgroundConfig,
88 doc="Large-scale background configuration.",
89 )
91 def setDefaults(self):
92 # These values correspond to the HSC recommendation. As noted
93 # below, the sizes are in millimeters, and correspond to an
94 # background image 8192x8192 pixels (8192*0.015=122.88).
95 self.largeScaleBackground.xSize = 122.88 # in mm
96 self.largeScaleBackground.ySize = 122.88 # in mm
97 self.largeScaleBackground.pixelSize = 0.015 # in mm per pixel
98 self.largeScaleBackground.minFrac = 0.1
99 self.largeScaleBackground.mask = ['BAD', 'SAT', 'INTRP', 'DETECTED', 'DETECTED_NEGATIVE',
100 'EDGE', 'NO_DATA']
103class CpSkyImageTask(pipeBase.PipelineTask):
104 """Mask the detections on the postISRCCD.
106 This task maps the MaskObjectsTask across all of the initial ISR
107 processed cpSkyIsr images to create cpSkyMaskedIsr products for
108 all (exposure, detector) values.
109 """
111 ConfigClass = CpSkyImageConfig
112 _DefaultName = "CpSkyImage"
114 def __init__(self, **kwargs):
115 super().__init__(**kwargs)
116 self.makeSubtask("maskTask")
118 def run(self, inputExp, camera):
119 """Mask the detections on the postISRCCD.
121 Parameters
122 ----------
123 inputExp : `lsst.afw.image.Exposure`
124 An ISR processed exposure that will have detections
125 masked.
126 camera : `lsst.afw.cameraGeom.Camera`
127 The camera geometry for this exposure. This is needed to
128 create the background model.
130 Returns
131 -------
132 results : `lsst.pipe.base.Struct`
133 The results struct containing:
135 ``maskedExp`` : `lsst.afw.image.Exposure`
136 The detection-masked version of the ``inputExp``.
137 ``maskedBkg`` : `lsst.pipe.tasks.background.FocalPlaneBackground`
138 The partial focal plane background containing only
139 this exposure/detector's worth of data.
140 """
141 # As constructCalibs.py SkyTask.processSingleBackground()
142 # Except: check if a detector is fully masked to avoid
143 # self.maskTask raising.
144 currentMask = inputExp.getMask()
145 badMask = currentMask.getPlaneBitMask(self.config.maskList)
146 if (currentMask.getArray() & badMask).all():
147 self.log.warning("All pixels are masked!")
148 else:
149 self.maskTask.run(inputExp, self.config.maskList)
151 # As constructCalibs.py SkyTask.measureBackground()
152 bgModel = FocalPlaneBackground.fromCamera(self.config.largeScaleBackground, camera)
153 bgModel.addCcd(inputExp)
155 return pipeBase.Struct(
156 maskedExp=inputExp,
157 maskedBkg=bgModel,
158 )
161class CpSkyScaleMeasureConnections(pipeBase.PipelineTaskConnections,
162 dimensions=("instrument", "physical_filter", "exposure")):
163 inputBkgs = cT.Input(
164 name="cpSkyDetectorBackground",
165 doc="Initial background model from one exposure/detector",
166 storageClass="FocalPlaneBackground",
167 dimensions=("instrument", "exposure", "detector"),
168 multiple=True
169 )
171 outputBkg = cT.Output(
172 name="cpSkyExpBackground",
173 doc="Background model for a full exposure.",
174 storageClass="FocalPlaneBackground",
175 dimensions=("instrument", "exposure"),
176 )
177 outputScale = cT.Output(
178 name="cpSkyExpScale",
179 doc="Scale for the full exposure.",
180 storageClass="PropertyList",
181 dimensions=("instrument", "exposure"),
182 )
185class CpSkyScaleMeasureConfig(pipeBase.PipelineTaskConfig,
186 pipelineConnections=CpSkyScaleMeasureConnections):
187 # There are no configurable parameters here.
188 pass
191class CpSkyScaleMeasureTask(pipeBase.PipelineTask):
192 """Measure per-exposure scale factors and merge focal plane backgrounds.
194 Merge all the per-detector partial backgrounds to a full focal
195 plane background for each exposure, and measure the scale factor
196 from that full background.
197 """
199 ConfigClass = CpSkyScaleMeasureConfig
200 _DefaultName = "cpSkyScaleMeasure"
202 def run(self, inputBkgs):
203 """Merge focal plane backgrounds and measure the scale factor.
205 Parameters
206 ----------
207 inputBkgs : `list` [`lsst.pipe.tasks.background.FocalPlaneBackground`]
208 A list of all of the partial focal plane backgrounds, one
209 from each detector in this exposure.
211 Returns
212 -------
213 results : `lsst.pipe.base.Struct`
214 The results struct containing:
216 ``outputBkg`` : `lsst.pipe.tasks.background.FocalPlaneBackground`
217 The full merged background for the entire focal plane.
218 ``outputScale`` : `lsst.daf.base.PropertyList`
219 A metadata containing the median level of the
220 background, stored in the key 'scale'.
221 """
222 # As constructCalibs.py SkyTask.scatterProcess()
223 # Merge into the full focal plane.
224 background = inputBkgs[0]
225 for bg in inputBkgs[1:]:
226 background.merge(bg)
228 backgroundPixels = background.getStatsImage().getArray()
229 self.log.info("Background model min/max: %f %f. Scale %f",
230 np.min(backgroundPixels), np.max(backgroundPixels),
231 np.median(backgroundPixels))
233 # A property list is overkill, but FocalPlaneBackground
234 # doesn't have a metadata object that this can be stored in.
235 scale = np.median(background.getStatsImage().getArray())
236 scaleMD = PropertyList()
237 scaleMD.set("scale", float(scale))
239 return pipeBase.Struct(
240 outputBkg=background,
241 outputScale=scaleMD,
242 )
245class CpSkySubtractBackgroundConnections(pipeBase.PipelineTaskConnections,
246 dimensions=("instrument", "physical_filter",
247 "exposure", "detector")):
248 inputExp = cT.Input(
249 name="cpSkyMaskedIsr",
250 doc="Masked post-ISR image.",
251 storageClass="Exposure",
252 dimensions=("instrument", "exposure", "detector"),
253 )
254 inputBkg = cT.Input(
255 name="cpSkyExpBackground",
256 doc="Background model for the full exposure.",
257 storageClass="FocalPlaneBackground",
258 dimensions=("instrument", "exposure"),
259 )
260 inputScale = cT.Input(
261 name="cpSkyExpScale",
262 doc="Scale for the full exposure.",
263 storageClass="PropertyList",
264 dimensions=("instrument", "exposure"),
265 )
267 outputBkg = cT.Output(
268 name="cpExpBackground",
269 doc="Normalized, static background.",
270 storageClass="Background",
271 dimensions=("instrument", "exposure", "detector"),
272 )
275class CpSkySubtractBackgroundConfig(pipeBase.PipelineTaskConfig,
276 pipelineConnections=CpSkySubtractBackgroundConnections):
277 sky = pexConfig.ConfigurableField(
278 target=SkyMeasurementTask,
279 doc="Sky measurement",
280 )
283class CpSkySubtractBackgroundTask(pipeBase.PipelineTask):
284 """Subtract per-exposure background from individual detector masked images.
286 The cpSkyMaskedIsr images constructed by CpSkyImageTask have the
287 scaled background constructed by CpSkyScaleMeasureTask subtracted,
288 and new background models are constructed for the remaining
289 signal.
291 The output was called `icExpBackground` in gen2, but the product
292 created here has definition clashes that prevent that from being
293 reused.
294 """
296 ConfigClass = CpSkySubtractBackgroundConfig
297 _DefaultName = "cpSkySubtractBkg"
299 def __init__(self, **kwargs):
300 super().__init__(**kwargs)
301 self.makeSubtask("sky")
303 def run(self, inputExp, inputBkg, inputScale):
304 """Subtract per-exposure background from individual detector masked
305 images.
307 Parameters
308 ----------
309 inputExp : `lsst.afw.image.Exposure`
310 The ISR processed, detection masked image.
311 inputBkg : `lsst.pipe.tasks.background.FocalPlaneBackground.
312 Full focal plane background for this exposure.
313 inputScale : `lsst.daf.base.PropertyList`
314 Metadata containing the scale factor.
316 Returns
317 -------
318 results : `lsst.pipe.base.Struct`
319 The results struct containing:
321 ``outputBkg``
322 Remnant sky background with the full-exposure
323 component removed. (`lsst.afw.math.BackgroundList`)
324 """
325 # As constructCalibs.py SkyTask.processSingle()
326 image = inputExp.getMaskedImage()
327 detector = inputExp.getDetector()
328 bbox = image.getBBox()
330 scale = inputScale.get('scale')
331 background = inputBkg.toCcdBackground(detector, bbox)
332 image -= background.getImage()
333 image /= scale
335 newBackground = self.sky.measureBackground(image)
336 return pipeBase.Struct(
337 outputBkg=newBackground
338 )
341class CpSkyCombineConnections(pipeBase.PipelineTaskConnections,
342 dimensions=("instrument", "physical_filter", "detector")):
343 inputBkgs = cT.Input(
344 name="cpExpBackground",
345 doc="Normalized, static background.",
346 storageClass="Background",
347 dimensions=("instrument", "exposure", "detector"),
348 multiple=True,
349 )
350 inputExpHandles = cT.Input(
351 name="cpSkyMaskedIsr",
352 doc="Masked post-ISR image.",
353 storageClass="Exposure",
354 dimensions=("instrument", "exposure", "detector"),
355 multiple=True,
356 deferLoad=True,
357 )
359 outputCalib = cT.Output(
360 name="sky",
361 doc="Averaged static background.",
362 storageClass="ExposureF",
363 dimensions=("instrument", "detector", "physical_filter"),
364 isCalibration=True,
365 )
368class CpSkyCombineConfig(pipeBase.PipelineTaskConfig,
369 pipelineConnections=CpSkyCombineConnections):
370 sky = pexConfig.ConfigurableField(
371 target=SkyMeasurementTask,
372 doc="Sky measurement",
373 )
376class CpSkyCombineTask(pipeBase.PipelineTask):
377 """Merge per-exposure measurements into a detector level calibration.
379 Each of the per-detector results from all input exposures are
380 averaged to produce the final SKY calibration.
382 As before, this is written to a skyCalib instead of a SKY to avoid
383 definition classes in gen3.
384 """
386 ConfigClass = CpSkyCombineConfig
387 _DefaultName = "cpSkyCombine"
389 def __init__(self, **kwargs):
390 super().__init__(**kwargs)
391 self.makeSubtask("sky")
393 def run(self, inputBkgs, inputExpHandles):
394 """Merge per-exposure measurements into a detector level calibration.
396 Parameters
397 ----------
398 inputBkgs : `list` [`lsst.afw.math.BackgroundList`]
399 Remnant backgrounds from each exposure.
400 inputHandles : `list` [`lsst.daf.butler.DeferredDatasetHandles`]
401 The Butler handles to the ISR processed, detection masked images.
403 Returns
404 -------
405 results : `lsst.pipe.base.Struct`
406 The results struct containing:
408 `outputCalib` : `lsst.afw.image.Exposure`
409 The final sky calibration product.
410 """
411 skyCalib = self.sky.averageBackgrounds(inputBkgs)
412 skyCalib.setDetector(inputExpHandles[0].get(component='detector'))
413 skyCalib.setFilter(inputExpHandles[0].get(component='filter'))
415 CalibCombineTask().combineHeaders(inputExpHandles, skyCalib, calibType='SKY')
417 return pipeBase.Struct(
418 outputCalib=skyCalib,
419 )