Coverage for python/lsst/drp/tasks/forcedPhotCoadd.py: 32%

99 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-27 12:14 +0000

1# This file is part of drp_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 

22import lsst.pex.config 

23import lsst.afw.table 

24 

25import lsst.pipe.base as pipeBase 

26 

27from lsst.meas.base._id_generator import SkyMapIdGeneratorConfig 

28from lsst.meas.base.forcedMeasurement import ForcedMeasurementTask 

29from lsst.meas.base.applyApCorr import ApplyApCorrTask 

30from lsst.meas.base.catalogCalculation import CatalogCalculationTask 

31from lsst.meas.extensions.scarlet.io import updateCatalogFootprints 

32 

33__all__ = ("ForcedPhotCoaddConfig", "ForcedPhotCoaddTask") 

34 

35 

36class ForcedPhotCoaddConnections(pipeBase.PipelineTaskConnections, 

37 dimensions=("band", "skymap", "tract", "patch"), 

38 defaultTemplates={"inputCoaddName": "deep", 

39 "outputCoaddName": "deep"}): 

40 inputSchema = pipeBase.connectionTypes.InitInput( 

41 doc="Schema for the input measurement catalogs.", 

42 name="{inputCoaddName}Coadd_ref_schema", 

43 storageClass="SourceCatalog", 

44 ) 

45 outputSchema = pipeBase.connectionTypes.InitOutput( 

46 doc="Schema for the output forced measurement catalogs.", 

47 name="{outputCoaddName}Coadd_forced_src_schema", 

48 storageClass="SourceCatalog", 

49 ) 

50 exposure = pipeBase.connectionTypes.Input( 

51 doc="Input exposure to perform photometry on.", 

52 name="{inputCoaddName}Coadd_calexp", 

53 storageClass="ExposureF", 

54 dimensions=["band", "skymap", "tract", "patch"], 

55 ) 

56 refCat = pipeBase.connectionTypes.Input( 

57 doc="Catalog of shapes and positions at which to force photometry.", 

58 name="{inputCoaddName}Coadd_ref", 

59 storageClass="SourceCatalog", 

60 dimensions=["skymap", "tract", "patch"], 

61 ) 

62 refCatInBand = pipeBase.connectionTypes.Input( 

63 doc="Catalog of shapes and positions in the band having forced photometry done", 

64 name="{inputCoaddName}Coadd_meas", 

65 storageClass="SourceCatalog", 

66 dimensions=("band", "skymap", "tract", "patch") 

67 ) 

68 footprintCatInBand = pipeBase.connectionTypes.Input( 

69 doc="Catalog of footprints to attach to sources", 

70 name="{inputCoaddName}Coadd_deblendedFlux", 

71 storageClass="SourceCatalog", 

72 dimensions=("band", "skymap", "tract", "patch") 

73 ) 

74 scarletModels = pipeBase.connectionTypes.Input( 

75 doc="Multiband scarlet models produced by the deblender", 

76 name="{inputCoaddName}Coadd_scarletModelData", 

77 storageClass="ScarletModelData", 

78 dimensions=("tract", "patch", "skymap"), 

79 ) 

80 refWcs = pipeBase.connectionTypes.Input( 

81 doc="Reference world coordinate system.", 

82 name="{inputCoaddName}Coadd.wcs", 

83 storageClass="Wcs", 

84 dimensions=["band", "skymap", "tract", "patch"], 

85 ) # used in place of a skymap wcs because of DM-28880 

86 measCat = pipeBase.connectionTypes.Output( 

87 doc="Output forced photometry catalog.", 

88 name="{outputCoaddName}Coadd_forced_src", 

89 storageClass="SourceCatalog", 

90 dimensions=["band", "skymap", "tract", "patch"], 

91 ) 

92 

93 def __init__(self, *, config=None): 

94 super().__init__(config=config) 

95 if config.footprintDatasetName != "ScarletModelData": 

96 self.inputs.remove("scarletModels") 

97 if config.footprintDatasetName != "DeblendedFlux": 

98 self.inputs.remove("footprintCatInBand") 

99 

100 

101class ForcedPhotCoaddConfig(pipeBase.PipelineTaskConfig, 

102 pipelineConnections=ForcedPhotCoaddConnections): 

103 measurement = lsst.pex.config.ConfigurableField( 

104 target=ForcedMeasurementTask, 

105 doc="subtask to do forced measurement" 

106 ) 

107 coaddName = lsst.pex.config.Field( 

108 doc="coadd name: typically one of deep or goodSeeing", 

109 dtype=str, 

110 default="deep", 

111 ) 

112 doApCorr = lsst.pex.config.Field( 

113 dtype=bool, 

114 default=True, 

115 doc="Run subtask to apply aperture corrections" 

116 ) 

117 applyApCorr = lsst.pex.config.ConfigurableField( 

118 target=ApplyApCorrTask, 

119 doc="Subtask to apply aperture corrections" 

120 ) 

121 catalogCalculation = lsst.pex.config.ConfigurableField( 

122 target=CatalogCalculationTask, 

123 doc="Subtask to run catalogCalculation plugins on catalog" 

124 ) 

125 footprintDatasetName = lsst.pex.config.Field( 

126 doc="Dataset (without coadd prefix) that should be used to obtain (Heavy)Footprints for sources. " 

127 "Must have IDs that match those of the reference catalog." 

128 "If None, Footprints will be generated by transforming the reference Footprints.", 

129 dtype=str, 

130 default="ScarletModelData", 

131 optional=True 

132 ) 

133 doConserveFlux = lsst.pex.config.Field( 

134 dtype=bool, 

135 default=True, 

136 doc="Whether to use the deblender models as templates to re-distribute the flux " 

137 "from the 'exposure' (True), or to perform measurements on the deblender model footprints. " 

138 "If footprintDatasetName != 'ScarletModelData' then this field is ignored.") 

139 doStripFootprints = lsst.pex.config.Field( 

140 dtype=bool, 

141 default=True, 

142 doc="Whether to strip footprints from the output catalog before " 

143 "saving to disk. " 

144 "This is usually done when using scarlet models to save disk space.") 

145 hasFakes = lsst.pex.config.Field( 

146 dtype=bool, 

147 default=False, 

148 doc="Should be set to True if fake sources have been inserted into the input data." 

149 ) 

150 idGenerator = SkyMapIdGeneratorConfig.make_field() 

151 

152 def setDefaults(self): 

153 # Docstring inherited. 

154 # Make catalogCalculation a no-op by default as no modelFlux is setup 

155 # by default in ForcedMeasurementTask 

156 super().setDefaults() 

157 

158 self.catalogCalculation.plugins.names = [] 

159 self.measurement.copyColumns["id"] = "id" 

160 self.measurement.copyColumns["parent"] = "parent" 

161 self.measurement.plugins.names |= ['base_InputCount', 'base_Variance'] 

162 self.measurement.plugins['base_PixelFlags'].masksFpAnywhere = ['CLIPPED', 'SENSOR_EDGE', 

163 'REJECTED', 'INEXACT_PSF', 

164 'STREAK'] 

165 self.measurement.plugins['base_PixelFlags'].masksFpCenter = ['CLIPPED', 'SENSOR_EDGE', 

166 'REJECTED', 'INEXACT_PSF', 

167 'STREAK'] 

168 

169 

170class ForcedPhotCoaddTask(pipeBase.PipelineTask): 

171 """A pipeline task for performing forced measurement on coadd images. 

172 

173 Parameters 

174 ---------- 

175 refSchema : `lsst.afw.table.Schema`, optional 

176 The schema of the reference catalog, passed to the constructor of the 

177 references subtask. Optional, but must be specified if ``initInputs`` 

178 is not; if both are specified, ``initInputs`` takes precedence. 

179 initInputs : `dict` 

180 Dictionary that can contain a key ``inputSchema`` containing the 

181 schema. If present will override the value of ``refSchema``. 

182 **kwds 

183 Keyword arguments are passed to the supertask constructor. 

184 """ 

185 

186 ConfigClass = ForcedPhotCoaddConfig 

187 _DefaultName = "forcedPhotCoadd" 

188 dataPrefix = "deepCoadd_" 

189 

190 def __init__(self, refSchema=None, initInputs=None, **kwds): 

191 super().__init__(**kwds) 

192 

193 if initInputs is not None: 

194 refSchema = initInputs['inputSchema'].schema 

195 

196 if refSchema is None: 

197 raise ValueError("No reference schema provided.") 

198 self.makeSubtask("measurement", refSchema=refSchema) 

199 # It is necessary to get the schema internal to the forced measurement 

200 # task until such a time that the schema is not owned by the 

201 # measurement task, but is passed in by an external caller. 

202 if self.config.doApCorr: 

203 self.makeSubtask("applyApCorr", schema=self.measurement.schema) 

204 self.makeSubtask('catalogCalculation', schema=self.measurement.schema) 

205 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema) 

206 

207 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

208 inputs = butlerQC.get(inputRefs) 

209 

210 refCatInBand = inputs.pop('refCatInBand') 

211 if self.config.footprintDatasetName == "ScarletModelData": 

212 footprintData = inputs.pop("scarletModels") 

213 elif self.config.footprintDatasetName == "DeblendedFlux": 

214 footprintData = inputs.pop("footprintCatIndBand") 

215 else: 

216 footprintData = None 

217 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(inputRefs.exposure.dataId, 

218 inputs['exposure'], 

219 inputs['refCat'], 

220 refCatInBand, 

221 inputs['refWcs'], 

222 footprintData) 

223 outputs = self.run(**inputs) 

224 # Strip HeavyFootprints to save space on disk 

225 if self.config.footprintDatasetName == "ScarletModelData" and self.config.doStripFootprints: 

226 sources = outputs.measCat 

227 for source in sources[sources["parent"] != 0]: 

228 source.setFootprint(None) 

229 butlerQC.put(outputs, outputRefs) 

230 

231 def generateMeasCat(self, dataId, exposure, refCat, refCatInBand, refWcs, footprintData): 

232 """Generate a measurement catalog. 

233 

234 Parameters 

235 ---------- 

236 dataId : `lsst.daf.butler.DataCoordinate` 

237 Butler data ID for this image, with ``{tract, patch, band}`` keys. 

238 exposure : `lsst.afw.image.exposure.Exposure` 

239 Exposure to generate the catalog for. 

240 refCat : `lsst.afw.table.SourceCatalog` 

241 Catalog of shapes and positions at which to force photometry. 

242 refCatInBand : `lsst.afw.table.SourceCatalog` 

243 Catalog of shapes and position in the band forced photometry is 

244 currently being performed 

245 refWcs : `lsst.afw.image.SkyWcs` 

246 Reference world coordinate system. 

247 footprintData : `ScarletDataModel` or `lsst.afw.table.SourceCatalog` 

248 Either the scarlet data models or the deblended catalog containing 

249 footprints. If `footprintData` is `None` then the footprints 

250 contained in `refCatInBand` are used. 

251 

252 Returns 

253 ------- 

254 measCat : `lsst.afw.table.SourceCatalog` 

255 Catalog of forced sources to measure. 

256 expId : `int` 

257 Unique binary id associated with the input exposure 

258 

259 Raises 

260 ------ 

261 LookupError 

262 Raised if a footprint with a given source id was in the reference 

263 catalog but not in the reference catalog in band (meaning there was 

264 some sort of mismatch in the two input catalogs) 

265 """ 

266 id_generator = self.config.idGenerator.apply(dataId) 

267 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs, 

268 idFactory=id_generator.make_table_id_factory()) 

269 # attach footprints here as this can naturally live inside this method 

270 if self.config.footprintDatasetName == "ScarletModelData": 

271 # Load the scarlet models 

272 self._attachScarletFootprints( 

273 catalog=measCat, 

274 modelData=footprintData, 

275 exposure=exposure, 

276 band=dataId["band"] 

277 ) 

278 else: 

279 if self.config.footprintDatasetName is None: 

280 footprintCat = refCatInBand 

281 else: 

282 footprintCat = footprintData 

283 for srcRecord in measCat: 

284 fpRecord = footprintCat.find(srcRecord.getId()) 

285 if fpRecord is None: 

286 raise LookupError("Cannot find Footprint for source {}; please check that {} " 

287 "IDs are compatible with reference source IDs" 

288 .format(srcRecord.getId(), footprintCat)) 

289 srcRecord.setFootprint(fpRecord.getFootprint()) 

290 return measCat, id_generator.catalog_id 

291 

292 def run(self, measCat, exposure, refCat, refWcs, exposureId=None): 

293 """Perform forced measurement on a single exposure. 

294 

295 Parameters 

296 ---------- 

297 measCat : `lsst.afw.table.SourceCatalog` 

298 The measurement catalog, based on the sources listed in the 

299 reference catalog. 

300 exposure : `lsst.afw.image.Exposure` 

301 The measurement image upon which to perform forced detection. 

302 refCat : `lsst.afw.table.SourceCatalog` 

303 The reference catalog of sources to measure. 

304 refWcs : `lsst.afw.image.SkyWcs` 

305 The WCS for the references. 

306 exposureId : `int` 

307 Optional unique exposureId used for random seed in measurement 

308 task. 

309 

310 Returns 

311 ------- 

312 result : ~`lsst.pipe.base.Struct` 

313 Structure with fields: 

314 

315 ``measCat`` 

316 Catalog of forced measurement results 

317 (`lsst.afw.table.SourceCatalog`). 

318 """ 

319 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId) 

320 if self.config.doApCorr: 

321 self.applyApCorr.run( 

322 catalog=measCat, 

323 apCorrMap=exposure.getInfo().getApCorrMap() 

324 ) 

325 self.catalogCalculation.run(measCat) 

326 

327 return pipeBase.Struct(measCat=measCat) 

328 

329 def _attachScarletFootprints(self, catalog, modelData, exposure, band): 

330 """Attach scarlet models as HeavyFootprints 

331 """ 

332 if self.config.doConserveFlux: 

333 redistributeImage = exposure 

334 else: 

335 redistributeImage = None 

336 # Attach the footprints 

337 updateCatalogFootprints( 

338 modelData=modelData, 

339 catalog=catalog, 

340 band=band, 

341 imageForRedistribution=redistributeImage, 

342 removeScarletData=True, 

343 updateFluxColumns=False, 

344 )