Coverage for python / lsst / summit / utils / quickLook.py: 19%

102 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-05 08:54 +0000

1# This file is part of summit_utils. 

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 

22import dataclasses 

23import importlib.resources 

24from typing import Any 

25 

26import numpy as np 

27 

28import lsst.afw.cameraGeom as camGeom 

29import lsst.afw.image as afwImage 

30import lsst.ip.isr as ipIsr 

31import lsst.pex.config as pexConfig 

32import lsst.pipe.base as pipeBase 

33import lsst.pipe.base.connectionTypes as cT 

34from lsst.ip.isr import IsrTask 

35from lsst.ip.isr.isrTask import IsrTaskConnections 

36from lsst.meas.algorithms.installGaussianPsf import InstallGaussianPsfTask 

37from lsst.pipe.tasks.characterizeImage import CharacterizeImageTask 

38 

39__all__ = ["QuickLookIsrTask", "QuickLookIsrTaskConfig"] 

40 

41 

42class QuickLookIsrTaskConnections(IsrTaskConnections): 

43 """Copy isrTask's connections, changing prereq min values to zero. 

44 

45 Copy all the connections directly for IsrTask, keeping ccdExposure as 

46 required as non-zero, but changing all the other PrerequisiteInputs' 

47 minimum values to zero. 

48 """ 

49 

50 def __init__(self, *, config: Any = None): 

51 # programatically clone all of the connections from isrTask 

52 # setting minimum values to zero for everything except the ccdExposure 

53 super().__init__(config=IsrTask.ConfigClass()) # need a dummy config, isn't used other than for ctor 

54 for name, connection in self.allConnections.items(): 

55 if hasattr(connection, "minimum"): 

56 setattr( 

57 self, 

58 name, 

59 dataclasses.replace(connection, minimum=(0 if name != "ccdExposure" else 1)), 

60 ) 

61 

62 exposure = cT.Output( # called just "exposure" to mimic isrTask's return struct 

63 name="quickLookExp", 

64 doc="The quickLook output exposure.", 

65 storageClass="ExposureF", 

66 dimensions=("instrument", "exposure", "detector"), 

67 ) 

68 # set like this to make it explicit that the outputExposure 

69 # and the exposure are identical. The only reason there are two is for 

70 # API compatibility. 

71 self.outputExposure = exposure 

72 

73 

74class QuickLookIsrTaskConfig( 

75 pipeBase.PipelineTaskConfig, pipelineConnections=QuickLookIsrTaskConnections # type: ignore 

76): 

77 """Configuration parameters for QuickLookIsrTask.""" 

78 

79 doRepairCosmics: pexConfig.Field[bool] = pexConfig.Field( 

80 dtype=bool, doc="Interpolate over cosmic rays?", default=True 

81 ) 

82 

83 

84class QuickLookIsrTask(pipeBase.PipelineTask): 

85 """Task automatically performing as much isr as possible. Should never fail 

86 

87 Automatically performs as much isr as is possible, depending on the 

88 calibration products available. All calibration products that can be found 

89 are applied, and if none are found, the image is assembled, the overscan is 

90 subtracted and the assembled image is returned. Optionally, cosmic rays are 

91 interpolated over. 

92 """ 

93 

94 ConfigClass = QuickLookIsrTaskConfig 

95 config: QuickLookIsrTaskConfig 

96 _DefaultName = "quickLook" 

97 

98 def __init__(self, isrTask: IsrTask = IsrTask, **kwargs: Any): 

99 super().__init__(**kwargs) 

100 # Pass in IsrTask so that we can modify it slightly for unit tests. 

101 # Note that this is not an instance of the IsrTask class, but the class 

102 # itself, which is then instantiated later on, in the run() method, 

103 # with the dynamically generated config. 

104 self.isrTask = IsrTask 

105 

106 def run( 

107 self, 

108 ccdExposure: afwImage.Exposure, 

109 *, 

110 camera: camGeom.Camera | None = None, 

111 bias: afwImage.Exposure | None = None, 

112 dark: afwImage.Exposure | None = None, 

113 flat: afwImage.Exposure | None = None, 

114 fringes: afwImage.Exposure | None = None, 

115 defects: ipIsr.Defects | None = None, 

116 linearizer: ipIsr.linearize.LinearizeBase | None = None, 

117 crosstalk: ipIsr.crosstalk.CrosstalkCalib | None = None, 

118 bfKernel: np.ndarray | None = None, 

119 newBFKernel: ipIsr.BrighterFatterKernel | None = None, 

120 ptc: ipIsr.PhotonTransferCurveDataset | None = None, 

121 crosstalkSources: list | None = None, 

122 isrBaseConfig: ipIsr.IsrTaskConfig | None = None, 

123 filterTransmission: afwImage.TransmissionCurve | None = None, 

124 opticsTransmission: afwImage.TransmissionCurve | None = None, 

125 strayLightData: Any | None = None, 

126 sensorTransmission: afwImage.TransmissionCurve | None = None, 

127 atmosphereTransmission: afwImage.TransmissionCurve | None = None, 

128 deferredChargeCalib: Any | None = None, 

129 illumMaskedImage: afwImage.MaskedImage | None = None, 

130 ) -> pipeBase.Struct: 

131 """Run isr and cosmic ray repair using, doing as much isr as possible. 

132 

133 Retrieves as many calibration products as are available, and runs isr 

134 with those settings enabled, but always returns an assembled image at 

135 a minimum. Then performs cosmic ray repair if configured to. 

136 

137 Parameters 

138 ---------- 

139 ccdExposure : `lsst.afw.image.Exposure` 

140 The raw exposure that is to be run through ISR. The 

141 exposure is modified by this method. 

142 camera : `lsst.afw.cameraGeom.Camera`, optional 

143 The camera geometry for this exposure. Required if 

144 one or more of ``ccdExposure``, ``bias``, ``dark``, or 

145 ``flat`` does not have an associated detector. 

146 bias : `lsst.afw.image.Exposure`, optional 

147 Bias calibration frame. 

148 dark : `lsst.afw.image.Exposure`, optional 

149 Dark calibration frame. 

150 flat : `lsst.afw.image.Exposure`, optional 

151 Flat calibration frame. 

152 fringes : `lsst.afw.image.Exposure`, optional 

153 The fringe correction data. 

154 This input is slightly different than the `fringes` keyword to 

155 `lsst.ip.isr.IsrTask`, since the processing done in that task's 

156 `runQuantum` method is instead done here. 

157 defects : `lsst.ip.isr.Defects`, optional 

158 List of defects. 

159 linearizer : `lsst.ip.isr.linearize.LinearizeBase`, optional 

160 Functor for linearization. 

161 crosstalk : `lsst.ip.isr.crosstalk.CrosstalkCalib`, optional 

162 Calibration for crosstalk. 

163 bfKernel : `numpy.ndarray`, optional 

164 Brighter-fatter kernel. 

165 newBFKernel : `ipIsr.BrighterFatterKernel`, optional 

166 New Brighter-fatter kernel. 

167 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional 

168 Photon transfer curve dataset, with, e.g., gains 

169 and read noise. 

170 crosstalkSources : `list`, optional 

171 List of possible crosstalk sources. 

172 isrBaseConfig : `lsst.ip.isr.IsrTaskConfig`, optional 

173 An isrTask config to act as the base configuration. Options which 

174 involve applying a calibration product are ignored, but this allows 

175 for the configuration of e.g. the number of overscan columns. 

176 filterTransmission : `lsst.afw.image.TransmissionCurve` 

177 A ``TransmissionCurve`` that represents the throughput of the 

178 filter itself, to be evaluated in focal-plane coordinates. 

179 opticsTransmission: `lsst.afw.image.TransmissionCurve`, optional 

180 A ``TransmissionCurve`` that represents the throughput of the, 

181 optics, to be evaluated in focal-plane coordinates. 

182 strayLightData : `object`, optional 

183 Opaque object containing calibration information for stray-light 

184 correction. If `None`, no correction will be performed. 

185 sensorTransmission : `lsst.afw.image.TransmissionCurve` 

186 A ``TransmissionCurve`` that represents the throughput of the 

187 sensor itself, to be evaluated in post-assembly trimmed detector 

188 coordinates. 

189 atmosphereTransmission : `lsst.afw.image.TransmissionCurve` 

190 A ``TransmissionCurve`` that represents the throughput of the 

191 atmosphere, assumed to be spatially constant. 

192 illumMaskedImage : `lsst.afw.image.MaskedImage`, optional 

193 Illumination correction image. 

194 bfGains : `dict` of `float`, optional 

195 Gains used to override the detector's nominal gains for the 

196 brighter-fatter correction. A dict keyed by amplifier name for 

197 the detector in question. 

198 

199 Returns 

200 ------- 

201 result : `lsst.pipe.base.Struct` 

202 Result struct with component: 

203 - ``exposure`` : `afw.image.Exposure` 

204 The ISRed and cosmic-ray-repaired exposure. 

205 """ 

206 if not isrBaseConfig: 

207 isrConfig = IsrTask.ConfigClass() 

208 with importlib.resources.path("lsst.summit.utils", "resources/config/quickLookIsr.py") as cfgPath: 

209 isrConfig.load(cfgPath) 

210 else: 

211 isrConfig = isrBaseConfig 

212 

213 isrConfig.doBias = False 

214 isrConfig.doDark = False 

215 isrConfig.doFlat = False 

216 isrConfig.doFringe = False 

217 isrConfig.doDefect = False 

218 isrConfig.doLinearize = False 

219 isrConfig.doCrosstalk = False 

220 isrConfig.doBrighterFatter = False 

221 isrConfig.usePtcGains = False 

222 

223 if bias: 

224 isrConfig.doBias = True 

225 self.log.info("Running with bias correction") 

226 

227 if dark: 

228 isrConfig.doDark = True 

229 self.log.info("Running with dark correction") 

230 

231 if flat: 

232 isrConfig.doFlat = True 

233 self.log.info("Running with flat correction") 

234 

235 if fringes: 

236 isrConfig.doFringe = True 

237 self.log.info("Running with fringe correction") 

238 

239 if defects: 

240 isrConfig.doDefect = True 

241 self.log.info("Running with defect correction") 

242 

243 if linearizer: 

244 isrConfig.doLinearize = True 

245 self.log.info("Running with linearity correction") 

246 

247 if crosstalk: 

248 isrConfig.doCrosstalk = True 

249 self.log.info("Running with crosstalk correction") 

250 

251 if newBFKernel is not None: 

252 bfGains = newBFKernel.gain 

253 isrConfig.doBrighterFatter = True 

254 self.log.info("Running with new brighter-fatter correction") 

255 else: 

256 bfGains = None 

257 

258 if bfKernel is not None and bfGains is None: 

259 isrConfig.doBrighterFatter = True 

260 self.log.info("Running with brighter-fatter correction") 

261 

262 if ptc: 

263 isrConfig.usePtcGains = True 

264 self.log.info("Running with ptc correction") 

265 

266 isrConfig.doWrite = False 

267 isrTask = self.isrTask(config=isrConfig) 

268 

269 if fringes: 

270 # Must be run after isrTask is instantiated. 

271 isrTask.fringe.loadFringes( 

272 fringes, 

273 expId=ccdExposure.info.id, 

274 assembler=isrTask.assembleCcd if isrConfig.doAssembleIsrExposures else None, 

275 ) 

276 

277 result = isrTask.run( 

278 ccdExposure, 

279 camera=camera, 

280 bias=bias, 

281 dark=dark, 

282 flat=flat, 

283 fringes=fringes, 

284 defects=defects, 

285 linearizer=linearizer, 

286 crosstalk=crosstalk, 

287 bfKernel=bfKernel, 

288 bfGains=bfGains, 

289 ptc=ptc, 

290 crosstalkSources=crosstalkSources, 

291 filterTransmission=filterTransmission, 

292 opticsTransmission=opticsTransmission, 

293 sensorTransmission=sensorTransmission, 

294 atmosphereTransmission=atmosphereTransmission, 

295 strayLightData=strayLightData, 

296 deferredChargeCalib=deferredChargeCalib, 

297 illumMaskedImage=illumMaskedImage, 

298 ) 

299 

300 postIsr = result.exposure 

301 

302 if self.config.doRepairCosmics: 

303 try: # can fail due to too many CRs detected, and we always want an exposure back 

304 self.log.info("Repairing cosmics...") 

305 if postIsr.getPsf() is None: 

306 installPsfTask = InstallGaussianPsfTask() 

307 installPsfTask.run(postIsr) 

308 

309 # TODO: try adding a reasonably wide Gaussian as a temp PSF 

310 # and then just running repairTask on its own without any 

311 # imChar. It should work, and be faster. 

312 repairConfig = CharacterizeImageTask.ConfigClass() 

313 repairConfig.doMeasurePsf = False 

314 repairConfig.doApCorr = False 

315 repairConfig.doDeblend = False 

316 repairConfig.doWrite = False 

317 repairConfig.repair.cosmicray.nCrPixelMax = 200000 

318 repairTask = CharacterizeImageTask(config=repairConfig) 

319 

320 repairTask.repair.run(postIsr) 

321 except Exception as e: 

322 self.log.warning(f"During CR repair caught: {e}") 

323 

324 # exposure is returned for convenience to mimic isrTask's API. 

325 return pipeBase.Struct(exposure=postIsr, outputExposure=postIsr)