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

99 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-15 11:41 +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 self.measurement.plugins['base_PixelFlags'].masksFpCenter = ['CLIPPED', 'SENSOR_EDGE', 

165 'REJECTED', 'INEXACT_PSF'] 

166 

167 

168class ForcedPhotCoaddTask(pipeBase.PipelineTask): 

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

170 

171 Parameters 

172 ---------- 

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

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

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

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

177 initInputs : `dict` 

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

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

180 **kwds 

181 Keyword arguments are passed to the supertask constructor. 

182 """ 

183 

184 ConfigClass = ForcedPhotCoaddConfig 

185 _DefaultName = "forcedPhotCoadd" 

186 dataPrefix = "deepCoadd_" 

187 

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

189 super().__init__(**kwds) 

190 

191 if initInputs is not None: 

192 refSchema = initInputs['inputSchema'].schema 

193 

194 if refSchema is None: 

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

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

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

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

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

200 if self.config.doApCorr: 

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

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

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

204 

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

206 inputs = butlerQC.get(inputRefs) 

207 

208 refCatInBand = inputs.pop('refCatInBand') 

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

210 footprintData = inputs.pop("scarletModels") 

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

212 footprintData = inputs.pop("footprintCatIndBand") 

213 else: 

214 footprintData = None 

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

216 inputs['exposure'], 

217 inputs['refCat'], 

218 refCatInBand, 

219 inputs['refWcs'], 

220 footprintData) 

221 outputs = self.run(**inputs) 

222 # Strip HeavyFootprints to save space on disk 

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

224 sources = outputs.measCat 

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

226 source.setFootprint(None) 

227 butlerQC.put(outputs, outputRefs) 

228 

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

230 """Generate a measurement catalog. 

231 

232 Parameters 

233 ---------- 

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

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

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

237 Exposure to generate the catalog for. 

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

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

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

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

242 currently being performed 

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

244 Reference world coordinate system. 

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

246 Either the scarlet data models or the deblended catalog containing 

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

248 contained in `refCatInBand` are used. 

249 

250 Returns 

251 ------- 

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

253 Catalog of forced sources to measure. 

254 expId : `int` 

255 Unique binary id associated with the input exposure 

256 

257 Raises 

258 ------ 

259 LookupError 

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

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

262 some sort of mismatch in the two input catalogs) 

263 """ 

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

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

266 idFactory=id_generator.make_table_id_factory()) 

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

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

269 # Load the scarlet models 

270 self._attachScarletFootprints( 

271 catalog=measCat, 

272 modelData=footprintData, 

273 exposure=exposure, 

274 band=dataId["band"] 

275 ) 

276 else: 

277 if self.config.footprintDatasetName is None: 

278 footprintCat = refCatInBand 

279 else: 

280 footprintCat = footprintData 

281 for srcRecord in measCat: 

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

283 if fpRecord is None: 

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

285 "IDs are compatible with reference source IDs" 

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

287 srcRecord.setFootprint(fpRecord.getFootprint()) 

288 return measCat, id_generator.catalog_id 

289 

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

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

292 

293 Parameters 

294 ---------- 

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

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

297 reference catalog. 

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

299 The measurement image upon which to perform forced detection. 

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

301 The reference catalog of sources to measure. 

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

303 The WCS for the references. 

304 exposureId : `int` 

305 Optional unique exposureId used for random seed in measurement 

306 task. 

307 

308 Returns 

309 ------- 

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

311 Structure with fields: 

312 

313 ``measCat`` 

314 Catalog of forced measurement results 

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

316 """ 

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

318 if self.config.doApCorr: 

319 self.applyApCorr.run( 

320 catalog=measCat, 

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

322 ) 

323 self.catalogCalculation.run(measCat) 

324 

325 return pipeBase.Struct(measCat=measCat) 

326 

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

328 """Attach scarlet models as HeavyFootprints 

329 """ 

330 if self.config.doConserveFlux: 

331 redistributeImage = exposure 

332 else: 

333 redistributeImage = None 

334 # Attach the footprints 

335 updateCatalogFootprints( 

336 modelData=modelData, 

337 catalog=catalog, 

338 band=band, 

339 imageForRedistribution=redistributeImage, 

340 removeScarletData=True, 

341 updateFluxColumns=False, 

342 )