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

103 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-15 00:33 +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 os 

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 

38from lsst.utils import getPackageDir 

39 

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

41 

42 

43class QuickLookIsrTaskConnections(IsrTaskConnections): 

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

45 

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

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

48 minimum values to zero. 

49 """ 

50 

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

52 # programatically clone all of the connections from isrTask 

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

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

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

56 if hasattr(connection, "minimum"): 

57 setattr( 

58 self, 

59 name, 

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

61 ) 

62 

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

64 name="quickLookExp", 

65 doc="The quickLook output exposure.", 

66 storageClass="ExposureF", 

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

68 ) 

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

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

71 # API compatibility. 

72 self.outputExposure = exposure 

73 

74 

75class QuickLookIsrTaskConfig( 

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

77): 

78 """Configuration parameters for QuickLookIsrTask.""" 

79 

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

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

82 ) 

83 

84 

85class QuickLookIsrTask(pipeBase.PipelineTask): 

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

87 

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

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

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

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

92 interpolated over. 

93 """ 

94 

95 ConfigClass = QuickLookIsrTaskConfig 

96 config: QuickLookIsrTaskConfig 

97 _DefaultName = "quickLook" 

98 

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

100 super().__init__(**kwargs) 

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

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

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

104 # with the dynamically generated config. 

105 self.isrTask = IsrTask 

106 

107 def run( 

108 self, 

109 ccdExposure: afwImage.Exposure, 

110 *, 

111 camera: camGeom.Camera | None = None, 

112 bias: afwImage.Exposure | None = None, 

113 dark: afwImage.Exposure | None = None, 

114 flat: afwImage.Exposure | None = None, 

115 fringes: afwImage.Exposure | None = None, 

116 defects: ipIsr.Defects | None = None, 

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

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

119 bfKernel: np.ndarray | None = None, 

120 newBFKernel: ipIsr.BrighterFatterKernel | None = None, 

121 ptc: ipIsr.PhotonTransferCurveDataset | None = None, 

122 crosstalkSources: list | None = None, 

123 isrBaseConfig: ipIsr.IsrTaskConfig | None = None, 

124 filterTransmission: afwImage.TransmissionCurve | None = None, 

125 opticsTransmission: afwImage.TransmissionCurve | None = None, 

126 strayLightData: Any | None = None, 

127 sensorTransmission: afwImage.TransmissionCurve | None = None, 

128 atmosphereTransmission: afwImage.TransmissionCurve | None = None, 

129 deferredChargeCalib: Any | None = None, 

130 illumMaskedImage: afwImage.MaskedImage | None = None, 

131 ) -> pipeBase.Struct: 

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

133 

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

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

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

137 

138 Parameters 

139 ---------- 

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

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

142 exposure is modified by this method. 

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

144 The camera geometry for this exposure. Required if 

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

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

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

148 Bias calibration frame. 

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

150 Dark calibration frame. 

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

152 Flat calibration frame. 

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

154 The fringe correction data. 

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

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

157 `runQuantum` method is instead done here. 

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

159 List of defects. 

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

161 Functor for linearization. 

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

163 Calibration for crosstalk. 

164 bfKernel : `numpy.ndarray`, optional 

165 Brighter-fatter kernel. 

166 newBFKernel : `ipIsr.BrighterFatterKernel`, optional 

167 New Brighter-fatter kernel. 

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

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

170 and read noise. 

171 crosstalkSources : `list`, optional 

172 List of possible crosstalk sources. 

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

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

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

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

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

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

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

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

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

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

183 strayLightData : `object`, optional 

184 Opaque object containing calibration information for stray-light 

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

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

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

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

189 coordinates. 

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

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

192 atmosphere, assumed to be spatially constant. 

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

194 Illumination correction image. 

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

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

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

198 the detector in question. 

199 

200 Returns 

201 ------- 

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

203 Result struct with component: 

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

205 The ISRed and cosmic-ray-repaired exposure. 

206 """ 

207 if not isrBaseConfig: 

208 isrConfig = IsrTask.ConfigClass() 

209 packageDir = getPackageDir("summit_utils") 

210 isrConfig.load(os.path.join(packageDir, "config", "quickLookIsr.py")) 

211 else: 

212 isrConfig = isrBaseConfig 

213 

214 isrConfig.doBias = False 

215 isrConfig.doDark = False 

216 isrConfig.doFlat = False 

217 isrConfig.doFringe = False 

218 isrConfig.doDefect = False 

219 isrConfig.doLinearize = False 

220 isrConfig.doCrosstalk = False 

221 isrConfig.doBrighterFatter = False 

222 isrConfig.usePtcGains = False 

223 

224 if bias: 

225 isrConfig.doBias = True 

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

227 

228 if dark: 

229 isrConfig.doDark = True 

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

231 

232 if flat: 

233 isrConfig.doFlat = True 

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

235 

236 if fringes: 

237 isrConfig.doFringe = True 

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

239 

240 if defects: 

241 isrConfig.doDefect = True 

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

243 

244 if linearizer: 

245 isrConfig.doLinearize = True 

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

247 

248 if crosstalk: 

249 isrConfig.doCrosstalk = True 

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

251 

252 if newBFKernel is not None: 

253 bfGains = newBFKernel.gain 

254 isrConfig.doBrighterFatter = True 

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

256 else: 

257 bfGains = None 

258 

259 if bfKernel is not None and bfGains is None: 

260 isrConfig.doBrighterFatter = True 

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

262 

263 if ptc: 

264 isrConfig.usePtcGains = True 

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

266 

267 isrConfig.doWrite = False 

268 isrTask = self.isrTask(config=isrConfig) 

269 

270 if fringes: 

271 # Must be run after isrTask is instantiated. 

272 isrTask.fringe.loadFringes( 

273 fringes, 

274 expId=ccdExposure.info.id, 

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

276 ) 

277 

278 result = isrTask.run( 

279 ccdExposure, 

280 camera=camera, 

281 bias=bias, 

282 dark=dark, 

283 flat=flat, 

284 fringes=fringes, 

285 defects=defects, 

286 linearizer=linearizer, 

287 crosstalk=crosstalk, 

288 bfKernel=bfKernel, 

289 bfGains=bfGains, 

290 ptc=ptc, 

291 crosstalkSources=crosstalkSources, 

292 filterTransmission=filterTransmission, 

293 opticsTransmission=opticsTransmission, 

294 sensorTransmission=sensorTransmission, 

295 atmosphereTransmission=atmosphereTransmission, 

296 strayLightData=strayLightData, 

297 deferredChargeCalib=deferredChargeCalib, 

298 illumMaskedImage=illumMaskedImage, 

299 ) 

300 

301 postIsr = result.exposure 

302 

303 if self.config.doRepairCosmics: 

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

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

306 if postIsr.getPsf() is None: 

307 installPsfTask = InstallGaussianPsfTask() 

308 installPsfTask.run(postIsr) 

309 

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

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

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

313 repairConfig = CharacterizeImageTask.ConfigClass() 

314 repairConfig.doMeasurePsf = False 

315 repairConfig.doApCorr = False 

316 repairConfig.doDeblend = False 

317 repairConfig.doWrite = False 

318 repairConfig.repair.cosmicray.nCrPixelMax = 200000 

319 repairTask = CharacterizeImageTask(config=repairConfig) 

320 

321 repairTask.repair.run(postIsr) 

322 except Exception as e: 

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

324 

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

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