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

102 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-14 03:03 -0700

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 warnings 

23 

24import lsst.pex.config 

25import lsst.afw.table 

26 

27import lsst.pipe.base as pipeBase 

28 

29from lsst.meas.base._id_generator import SkyMapIdGeneratorConfig 

30from lsst.meas.base.forcedMeasurement import ForcedMeasurementTask 

31from lsst.meas.base.applyApCorr import ApplyApCorrTask 

32from lsst.meas.base.catalogCalculation import CatalogCalculationTask 

33 

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

35 

36 

37class ForcedPhotCoaddConnections(pipeBase.PipelineTaskConnections, 

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

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

40 "outputCoaddName": "deep"}): 

41 inputSchema = pipeBase.connectionTypes.InitInput( 

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

43 name="{inputCoaddName}Coadd_ref_schema", 

44 storageClass="SourceCatalog", 

45 ) 

46 outputSchema = pipeBase.connectionTypes.InitOutput( 

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

48 name="{outputCoaddName}Coadd_forced_src_schema", 

49 storageClass="SourceCatalog", 

50 ) 

51 exposure = pipeBase.connectionTypes.Input( 

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

53 name="{inputCoaddName}Coadd_calexp", 

54 storageClass="ExposureF", 

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

56 ) 

57 refCat = pipeBase.connectionTypes.Input( 

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

59 name="{inputCoaddName}Coadd_ref", 

60 storageClass="SourceCatalog", 

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

62 ) 

63 refCatInBand = pipeBase.connectionTypes.Input( 

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

65 name="{inputCoaddName}Coadd_meas", 

66 storageClass="SourceCatalog", 

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

68 ) 

69 footprintCatInBand = pipeBase.connectionTypes.Input( 

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

71 name="{inputCoaddName}Coadd_deblendedFlux", 

72 storageClass="SourceCatalog", 

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

74 ) 

75 scarletModels = pipeBase.connectionTypes.Input( 

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

77 name="{inputCoaddName}Coadd_scarletModelData", 

78 storageClass="ScarletModelData", 

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

80 ) 

81 refWcs = pipeBase.connectionTypes.Input( 

82 doc="Reference world coordinate system.", 

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

84 storageClass="Wcs", 

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

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

87 measCat = pipeBase.connectionTypes.Output( 

88 doc="Output forced photometry catalog.", 

89 name="{outputCoaddName}Coadd_forced_src", 

90 storageClass="SourceCatalog", 

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

92 ) 

93 

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

95 super().__init__(config=config) 

96 if config.footprintDatasetName != "ScarletModelData": 

97 self.inputs.remove("scarletModels") 

98 if config.footprintDatasetName != "DeblendedFlux": 

99 self.inputs.remove("footprintCatInBand") 

100 

101 

102class ForcedPhotCoaddConfig(pipeBase.PipelineTaskConfig, 

103 pipelineConnections=ForcedPhotCoaddConnections): 

104 measurement = lsst.pex.config.ConfigurableField( 

105 target=ForcedMeasurementTask, 

106 doc="subtask to do forced measurement" 

107 ) 

108 coaddName = lsst.pex.config.Field( 

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

110 dtype=str, 

111 default="deep", 

112 ) 

113 doApCorr = lsst.pex.config.Field( 

114 dtype=bool, 

115 default=True, 

116 doc="Run subtask to apply aperture corrections" 

117 ) 

118 applyApCorr = lsst.pex.config.ConfigurableField( 

119 target=ApplyApCorrTask, 

120 doc="Subtask to apply aperture corrections" 

121 ) 

122 catalogCalculation = lsst.pex.config.ConfigurableField( 

123 target=CatalogCalculationTask, 

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

125 ) 

126 footprintDatasetName = lsst.pex.config.Field( 

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

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

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

130 dtype=str, 

131 default="ScarletModelData", 

132 optional=True 

133 ) 

134 doConserveFlux = lsst.pex.config.Field( 

135 dtype=bool, 

136 default=True, 

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

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

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

140 doStripFootprints = lsst.pex.config.Field( 

141 dtype=bool, 

142 default=True, 

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

144 "saving to disk. " 

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

146 hasFakes = lsst.pex.config.Field( 

147 dtype=bool, 

148 default=False, 

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

150 ) 

151 idGenerator = SkyMapIdGeneratorConfig.make_field() 

152 

153 def setDefaults(self): 

154 # Docstring inherited. 

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

156 # by default in ForcedMeasurementTask 

157 super().setDefaults() 

158 

159 self.catalogCalculation.plugins.names = [] 

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

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

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

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

164 'REJECTED', 'INEXACT_PSF'] 

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

166 'REJECTED', 'INEXACT_PSF'] 

167 

168 

169class ForcedPhotCoaddTask(pipeBase.PipelineTask): 

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

171 

172 Parameters 

173 ---------- 

174 butler : `None` 

175 Deprecated and unused. Should always be `None`. 

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

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

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

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

180 initInputs : `dict` 

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

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

183 **kwds 

184 Keyword arguments are passed to the supertask constructor. 

185 """ 

186 

187 ConfigClass = ForcedPhotCoaddConfig 

188 _DefaultName = "forcedPhotCoadd" 

189 dataPrefix = "deepCoadd_" 

190 

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

192 super().__init__(**kwds) 

193 

194 if butler is not None: 

195 warnings.warn("The 'butler' parameter is no longer used and can be safely removed.", 

196 category=FutureWarning, stacklevel=2) 

197 butler = None 

198 

199 if initInputs is not None: 

200 refSchema = initInputs['inputSchema'].schema 

201 

202 if refSchema is None: 

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

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

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

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

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

208 if self.config.doApCorr: 

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

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

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

212 

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

214 inputs = butlerQC.get(inputRefs) 

215 

216 refCatInBand = inputs.pop('refCatInBand') 

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

218 footprintData = inputs.pop("scarletModels") 

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

220 footprintData = inputs.pop("footprintCatIndBand") 

221 else: 

222 footprintData = None 

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

224 inputs['exposure'], 

225 inputs['refCat'], 

226 refCatInBand, 

227 inputs['refWcs'], 

228 footprintData) 

229 outputs = self.run(**inputs) 

230 # Strip HeavyFootprints to save space on disk 

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

232 sources = outputs.measCat 

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

234 source.setFootprint(None) 

235 butlerQC.put(outputs, outputRefs) 

236 

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

238 """Generate a measurement catalog. 

239 

240 Parameters 

241 ---------- 

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

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

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

245 Exposure to generate the catalog for. 

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

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

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

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

250 currently being performed 

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

252 Reference world coordinate system. 

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

254 Either the scarlet data models or the deblended catalog containing 

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

256 contained in `refCatInBand` are used. 

257 

258 Returns 

259 ------- 

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

261 Catalog of forced sources to measure. 

262 expId : `int` 

263 Unique binary id associated with the input exposure 

264 

265 Raises 

266 ------ 

267 LookupError 

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

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

270 some sort of mismatch in the two input catalogs) 

271 """ 

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

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

274 idFactory=id_generator.make_table_id_factory()) 

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, 

280 modelData=footprintData, 

281 exposure=exposure, 

282 band=dataId["band"] 

283 ) 

284 else: 

285 if self.config.footprintDatasetName is None: 

286 footprintCat = refCatInBand 

287 else: 

288 footprintCat = footprintData 

289 for srcRecord in measCat: 

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

291 if fpRecord is None: 

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

293 "IDs are compatible with reference source IDs" 

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

295 srcRecord.setFootprint(fpRecord.getFootprint()) 

296 return measCat, id_generator.catalog_id 

297 

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

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

300 

301 Parameters 

302 ---------- 

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

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

305 reference catalog. 

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

307 The measurement image upon which to perform forced detection. 

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

309 The reference catalog of sources to measure. 

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

311 The WCS for the references. 

312 exposureId : `int` 

313 Optional unique exposureId used for random seed in measurement 

314 task. 

315 

316 Returns 

317 ------- 

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

319 Structure with fields: 

320 

321 ``measCat`` 

322 Catalog of forced measurement results 

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

324 """ 

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

326 if self.config.doApCorr: 

327 self.applyApCorr.run( 

328 catalog=measCat, 

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

330 ) 

331 self.catalogCalculation.run(measCat) 

332 

333 return pipeBase.Struct(measCat=measCat) 

334 

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

336 """Attach scarlet models as HeavyFootprints 

337 """ 

338 if self.config.doConserveFlux: 

339 redistributeImage = exposure.image 

340 else: 

341 redistributeImage = None 

342 # Attach the footprints 

343 modelData.updateCatalogFootprints( 

344 catalog=catalog, 

345 band=band, 

346 psfModel=exposure.getPsf(), 

347 redistributeImage=redistributeImage, 

348 removeScarletData=True, 

349 updateFluxColumns=False, 

350 )