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

100 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-04-27 04:31 -0700

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.afw.table 

23import lsst.pex.config 

24import lsst.pipe.base as pipeBase 

25from lsst.meas.base._id_generator import SkyMapIdGeneratorConfig 

26from lsst.meas.base.applyApCorr import ApplyApCorrTask 

27from lsst.meas.base.catalogCalculation import CatalogCalculationTask 

28from lsst.meas.base.forcedMeasurement import ForcedMeasurementTask 

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

30 

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

32 

33 

34class ForcedPhotCoaddConnections( 

35 pipeBase.PipelineTaskConnections, 

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

37 defaultTemplates={"inputCoaddName": "deep", "outputCoaddName": "deep"}, 

38): 

39 inputSchema = pipeBase.connectionTypes.InitInput( 

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

41 name="{inputCoaddName}Coadd_ref_schema", 

42 storageClass="SourceCatalog", 

43 ) 

44 outputSchema = pipeBase.connectionTypes.InitOutput( 

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

46 name="{outputCoaddName}Coadd_forced_src_schema", 

47 storageClass="SourceCatalog", 

48 ) 

49 exposure = pipeBase.connectionTypes.Input( 

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

51 name="{inputCoaddName}Coadd_calexp", 

52 storageClass="ExposureF", 

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

54 ) 

55 refCat = pipeBase.connectionTypes.Input( 

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

57 name="{inputCoaddName}Coadd_ref", 

58 storageClass="SourceCatalog", 

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

60 ) 

61 refCatInBand = pipeBase.connectionTypes.Input( 

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

63 name="{inputCoaddName}Coadd_meas", 

64 storageClass="SourceCatalog", 

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

66 ) 

67 footprintCatInBand = pipeBase.connectionTypes.Input( 

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

69 name="{inputCoaddName}Coadd_deblendedFlux", 

70 storageClass="SourceCatalog", 

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

72 ) 

73 scarletModels = pipeBase.connectionTypes.Input( 

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

75 name="{inputCoaddName}Coadd_scarletModelData", 

76 storageClass="ScarletModelData", 

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

78 ) 

79 refWcs = pipeBase.connectionTypes.Input( 

80 doc="Reference world coordinate system.", 

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

82 storageClass="Wcs", 

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

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

85 measCat = pipeBase.connectionTypes.Output( 

86 doc="Output forced photometry catalog.", 

87 name="{outputCoaddName}Coadd_forced_src", 

88 storageClass="SourceCatalog", 

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

90 ) 

91 

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

93 super().__init__(config=config) 

94 if config.footprintDatasetName != "ScarletModelData": 

95 self.inputs.remove("scarletModels") 

96 if config.footprintDatasetName != "DeblendedFlux": 

97 self.inputs.remove("footprintCatInBand") 

98 

99 

100class ForcedPhotCoaddConfig(pipeBase.PipelineTaskConfig, pipelineConnections=ForcedPhotCoaddConnections): 

101 measurement = lsst.pex.config.ConfigurableField( 

102 target=ForcedMeasurementTask, doc="subtask to do forced measurement" 

103 ) 

104 coaddName = lsst.pex.config.Field( 

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

106 dtype=str, 

107 default="deep", 

108 ) 

109 doApCorr = lsst.pex.config.Field( 

110 dtype=bool, default=True, doc="Run subtask to apply aperture corrections" 

111 ) 

112 applyApCorr = lsst.pex.config.ConfigurableField( 

113 target=ApplyApCorrTask, doc="Subtask to apply aperture corrections" 

114 ) 

115 catalogCalculation = lsst.pex.config.ConfigurableField( 

116 target=CatalogCalculationTask, doc="Subtask to run catalogCalculation plugins on catalog" 

117 ) 

118 footprintDatasetName = lsst.pex.config.Field( 

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

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

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

122 dtype=str, 

123 default="ScarletModelData", 

124 optional=True, 

125 ) 

126 doConserveFlux = lsst.pex.config.Field( 

127 dtype=bool, 

128 default=True, 

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

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

131 "If footprintDatasetName != 'ScarletModelData' then this field is ignored.", 

132 ) 

133 doStripFootprints = lsst.pex.config.Field( 

134 dtype=bool, 

135 default=True, 

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

137 "saving to disk. " 

138 "This is usually done when using scarlet models to save disk space.", 

139 ) 

140 hasFakes = lsst.pex.config.Field( 

141 dtype=bool, 

142 default=False, 

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

144 ) 

145 idGenerator = SkyMapIdGeneratorConfig.make_field() 

146 

147 def setDefaults(self): 

148 # Docstring inherited. 

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

150 # by default in ForcedMeasurementTask 

151 super().setDefaults() 

152 

153 self.catalogCalculation.plugins.names = [] 

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

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

156 self.measurement.plugins.names |= ["base_InputCount", "base_Variance"] 

157 self.measurement.plugins["base_PixelFlags"].masksFpAnywhere = [ 

158 "CLIPPED", 

159 "SENSOR_EDGE", 

160 "REJECTED", 

161 "INEXACT_PSF", 

162 "STREAK", 

163 ] 

164 self.measurement.plugins["base_PixelFlags"].masksFpCenter = [ 

165 "CLIPPED", 

166 "SENSOR_EDGE", 

167 "REJECTED", 

168 "INEXACT_PSF", 

169 "STREAK", 

170 ] 

171 

172 

173class ForcedPhotCoaddTask(pipeBase.PipelineTask): 

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

175 

176 Parameters 

177 ---------- 

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

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

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

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

182 initInputs : `dict` 

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

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

185 **kwds 

186 Keyword arguments are passed to the supertask constructor. 

187 """ 

188 

189 ConfigClass = ForcedPhotCoaddConfig 

190 _DefaultName = "forcedPhotCoadd" 

191 dataPrefix = "deepCoadd_" 

192 

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

194 super().__init__(**kwds) 

195 

196 if initInputs is not None: 

197 refSchema = initInputs["inputSchema"].schema 

198 

199 if refSchema is None: 

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

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

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

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

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

205 if self.config.doApCorr: 

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

207 self.makeSubtask("catalogCalculation", schema=self.measurement.schema) 

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

209 

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

211 inputs = butlerQC.get(inputRefs) 

212 

213 refCatInBand = inputs.pop("refCatInBand") 

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

215 footprintData = inputs.pop("scarletModels") 

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

217 footprintData = inputs.pop("footprintCatIndBand") 

218 else: 

219 footprintData = None 

220 inputs["measCat"], inputs["exposureId"] = self.generateMeasCat( 

221 inputRefs.exposure.dataId, 

222 inputs["exposure"], 

223 inputs["refCat"], 

224 refCatInBand, 

225 inputs["refWcs"], 

226 footprintData, 

227 ) 

228 outputs = self.run(**inputs) 

229 # Strip HeavyFootprints to save space on disk 

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

231 sources = outputs.measCat 

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

233 source.setFootprint(None) 

234 butlerQC.put(outputs, outputRefs) 

235 

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

237 """Generate a measurement catalog. 

238 

239 Parameters 

240 ---------- 

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

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

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

244 Exposure to generate the catalog for. 

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

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

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

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

249 currently being performed 

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

251 Reference world coordinate system. 

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

253 Either the scarlet data models or the deblended catalog containing 

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

255 contained in `refCatInBand` are used. 

256 

257 Returns 

258 ------- 

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

260 Catalog of forced sources to measure. 

261 expId : `int` 

262 Unique binary id associated with the input exposure 

263 

264 Raises 

265 ------ 

266 LookupError 

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

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

269 some sort of mismatch in the two input catalogs) 

270 """ 

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

272 measCat = self.measurement.generateMeasCat( 

273 exposure, refCat, refWcs, idFactory=id_generator.make_table_id_factory() 

274 ) 

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

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

277 # Load the scarlet models 

278 self._attachScarletFootprints( 

279 catalog=measCat, modelData=footprintData, exposure=exposure, band=dataId["band"] 

280 ) 

281 else: 

282 if self.config.footprintDatasetName is None: 

283 footprintCat = refCatInBand 

284 else: 

285 footprintCat = footprintData 

286 for srcRecord in measCat: 

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

288 if fpRecord is None: 

289 raise LookupError( 

290 "Cannot find Footprint for source {}; please check that {} " 

291 "IDs are compatible with reference source IDs".format(srcRecord.getId(), footprintCat) 

292 ) 

293 srcRecord.setFootprint(fpRecord.getFootprint()) 

294 return measCat, id_generator.catalog_id 

295 

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

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

298 

299 Parameters 

300 ---------- 

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

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

303 reference catalog. 

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

305 The measurement image upon which to perform forced detection. 

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

307 The reference catalog of sources to measure. 

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

309 The WCS for the references. 

310 exposureId : `int` 

311 Optional unique exposureId used for random seed in measurement 

312 task. 

313 

314 Returns 

315 ------- 

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

317 Structure with fields: 

318 

319 ``measCat`` 

320 Catalog of forced measurement results 

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

322 """ 

323 # We want to cache repeated PSF evaluations at the same point coming 

324 # from different measurement plugins. We assume each algorithm tries 

325 # to evaluate the PSF twice, which is more than enough since many don't 

326 # evaluate it at all, and there's no *good* reason for any algorithm to 

327 # evaluate it more than once. 

328 exposure.psf.setCacheCapacity(2 * len(self.config.measurement.plugins.names)) 

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

330 if self.config.doApCorr: 

331 self.applyApCorr.run(catalog=measCat, apCorrMap=exposure.getInfo().getApCorrMap()) 

332 self.catalogCalculation.run(measCat) 

333 

334 return pipeBase.Struct(measCat=measCat) 

335 

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

337 """Attach scarlet models as HeavyFootprints""" 

338 if self.config.doConserveFlux: 

339 redistributeImage = exposure 

340 else: 

341 redistributeImage = None 

342 # Attach the footprints 

343 updateCatalogFootprints( 

344 modelData=modelData, 

345 catalog=catalog, 

346 band=band, 

347 imageForRedistribution=redistributeImage, 

348 removeScarletData=True, 

349 updateFluxColumns=False, 

350 )