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

98 statements  

« prev     ^ index     » next       coverage.py v7.3.0, created at 2023-08-31 11:18 +0000

1# This file is part of meas_base. 

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 

31 

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

33 

34 

35class ForcedPhotCoaddConnections(pipeBase.PipelineTaskConnections, 

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

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

38 "outputCoaddName": "deep"}): 

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, 

101 pipelineConnections=ForcedPhotCoaddConnections): 

102 measurement = lsst.pex.config.ConfigurableField( 

103 target=ForcedMeasurementTask, 

104 doc="subtask to do forced measurement" 

105 ) 

106 coaddName = lsst.pex.config.Field( 

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

108 dtype=str, 

109 default="deep", 

110 ) 

111 doApCorr = lsst.pex.config.Field( 

112 dtype=bool, 

113 default=True, 

114 doc="Run subtask to apply aperture corrections" 

115 ) 

116 applyApCorr = lsst.pex.config.ConfigurableField( 

117 target=ApplyApCorrTask, 

118 doc="Subtask to apply aperture corrections" 

119 ) 

120 catalogCalculation = lsst.pex.config.ConfigurableField( 

121 target=CatalogCalculationTask, 

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

123 ) 

124 footprintDatasetName = lsst.pex.config.Field( 

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

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

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

128 dtype=str, 

129 default="ScarletModelData", 

130 optional=True 

131 ) 

132 doConserveFlux = lsst.pex.config.Field( 

133 dtype=bool, 

134 default=True, 

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

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

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

138 doStripFootprints = lsst.pex.config.Field( 

139 dtype=bool, 

140 default=True, 

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

142 "saving to disk. " 

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

144 hasFakes = lsst.pex.config.Field( 

145 dtype=bool, 

146 default=False, 

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

148 ) 

149 idGenerator = SkyMapIdGeneratorConfig.make_field() 

150 

151 def setDefaults(self): 

152 # Docstring inherited. 

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

154 # by default in ForcedMeasurementTask 

155 super().setDefaults() 

156 

157 self.catalogCalculation.plugins.names = [] 

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

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

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

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

162 'REJECTED', 'INEXACT_PSF'] 

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

164 'REJECTED', 'INEXACT_PSF'] 

165 

166 

167class ForcedPhotCoaddTask(pipeBase.PipelineTask): 

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

169 

170 Parameters 

171 ---------- 

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

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

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

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

176 initInputs : `dict` 

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

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

179 **kwds 

180 Keyword arguments are passed to the supertask constructor. 

181 """ 

182 

183 ConfigClass = ForcedPhotCoaddConfig 

184 _DefaultName = "forcedPhotCoadd" 

185 dataPrefix = "deepCoadd_" 

186 

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

188 super().__init__(**kwds) 

189 

190 if initInputs is not None: 

191 refSchema = initInputs['inputSchema'].schema 

192 

193 if refSchema is None: 

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

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

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

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

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

199 if self.config.doApCorr: 

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

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

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

203 

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

205 inputs = butlerQC.get(inputRefs) 

206 

207 refCatInBand = inputs.pop('refCatInBand') 

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

209 footprintData = inputs.pop("scarletModels") 

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

211 footprintData = inputs.pop("footprintCatIndBand") 

212 else: 

213 footprintData = None 

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

215 inputs['exposure'], 

216 inputs['refCat'], 

217 refCatInBand, 

218 inputs['refWcs'], 

219 footprintData) 

220 outputs = self.run(**inputs) 

221 # Strip HeavyFootprints to save space on disk 

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

223 sources = outputs.measCat 

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

225 source.setFootprint(None) 

226 butlerQC.put(outputs, outputRefs) 

227 

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

229 """Generate a measurement catalog. 

230 

231 Parameters 

232 ---------- 

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

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

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

236 Exposure to generate the catalog for. 

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

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

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

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

241 currently being performed 

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

243 Reference world coordinate system. 

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

245 Either the scarlet data models or the deblended catalog containing 

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

247 contained in `refCatInBand` are used. 

248 

249 Returns 

250 ------- 

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

252 Catalog of forced sources to measure. 

253 expId : `int` 

254 Unique binary id associated with the input exposure 

255 

256 Raises 

257 ------ 

258 LookupError 

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

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

261 some sort of mismatch in the two input catalogs) 

262 """ 

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

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

265 idFactory=id_generator.make_table_id_factory()) 

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

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

268 # Load the scarlet models 

269 self._attachScarletFootprints( 

270 catalog=measCat, 

271 modelData=footprintData, 

272 exposure=exposure, 

273 band=dataId["band"] 

274 ) 

275 else: 

276 if self.config.footprintDatasetName is None: 

277 footprintCat = refCatInBand 

278 else: 

279 footprintCat = footprintData 

280 for srcRecord in measCat: 

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

282 if fpRecord is None: 

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

284 "IDs are compatible with reference source IDs" 

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

286 srcRecord.setFootprint(fpRecord.getFootprint()) 

287 return measCat, id_generator.catalog_id 

288 

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

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

291 

292 Parameters 

293 ---------- 

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

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

296 reference catalog. 

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

298 The measurement image upon which to perform forced detection. 

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

300 The reference catalog of sources to measure. 

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

302 The WCS for the references. 

303 exposureId : `int` 

304 Optional unique exposureId used for random seed in measurement 

305 task. 

306 

307 Returns 

308 ------- 

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

310 Structure with fields: 

311 

312 ``measCat`` 

313 Catalog of forced measurement results 

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

315 """ 

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

317 if self.config.doApCorr: 

318 self.applyApCorr.run( 

319 catalog=measCat, 

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

321 ) 

322 self.catalogCalculation.run(measCat) 

323 

324 return pipeBase.Struct(measCat=measCat) 

325 

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

327 """Attach scarlet models as HeavyFootprints 

328 """ 

329 if self.config.doConserveFlux: 

330 redistributeImage = exposure.image 

331 else: 

332 redistributeImage = None 

333 # Attach the footprints 

334 modelData.updateCatalogFootprints( 

335 catalog=catalog, 

336 band=band, 

337 psfModel=exposure.getPsf(), 

338 redistributeImage=redistributeImage, 

339 removeScarletData=True, 

340 updateFluxColumns=False, 

341 )