Coverage for python/lsst/meas/base/forcedPhotCoadd.py: 31%

102 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-28 09:04 +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 warnings 

23 

24import lsst.pex.config 

25import lsst.afw.table 

26 

27import lsst.pipe.base as pipeBase 

28 

29from ._id_generator import SkyMapIdGeneratorConfig 

30from .forcedMeasurement import ForcedMeasurementTask 

31from .applyApCorr import ApplyApCorrTask 

32from .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 by default in 

156 # 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 task until such a time 

206 # that the schema is not owned by the measurement task, but is passed in by an external caller 

207 if self.config.doApCorr: 

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

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

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

211 

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

213 inputs = butlerQC.get(inputRefs) 

214 

215 refCatInBand = inputs.pop('refCatInBand') 

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

217 footprintData = inputs.pop("scarletModels") 

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

219 footprintData = inputs.pop("footprintCatIndBand") 

220 else: 

221 footprintData = None 

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

223 inputs['exposure'], 

224 inputs['refCat'], 

225 refCatInBand, 

226 inputs['refWcs'], 

227 footprintData) 

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(exposure, refCat, refWcs, 

273 idFactory=id_generator.make_table_id_factory()) 

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

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

276 # Load the scarlet models 

277 self._attachScarletFootprints( 

278 catalog=measCat, 

279 modelData=footprintData, 

280 exposure=exposure, 

281 band=dataId["band"] 

282 ) 

283 else: 

284 if self.config.footprintDatasetName is None: 

285 footprintCat = refCatInBand 

286 else: 

287 footprintCat = footprintData 

288 for srcRecord in measCat: 

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

290 if fpRecord is None: 

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

292 "IDs are compatible with reference source IDs" 

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

294 srcRecord.setFootprint(fpRecord.getFootprint()) 

295 return measCat, id_generator.catalog_id 

296 

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

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

299 

300 Parameters 

301 ---------- 

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

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

304 reference catalog. 

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

306 The measurement image upon which to perform forced detection. 

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

308 The reference catalog of sources to measure. 

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

310 The WCS for the references. 

311 exposureId : `int` 

312 Optional unique exposureId used for random seed in measurement 

313 task. 

314 

315 Returns 

316 ------- 

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

318 Structure with fields: 

319 

320 ``measCat`` 

321 Catalog of forced measurement results 

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

323 """ 

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

325 if self.config.doApCorr: 

326 self.applyApCorr.run( 

327 catalog=measCat, 

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

329 ) 

330 self.catalogCalculation.run(measCat) 

331 

332 return pipeBase.Struct(measCat=measCat) 

333 

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

335 """Attach scarlet models as HeavyFootprints 

336 """ 

337 if self.config.doConserveFlux: 

338 redistributeImage = exposure.image 

339 else: 

340 redistributeImage = None 

341 # Attach the footprints 

342 modelData.updateCatalogFootprints( 

343 catalog=catalog, 

344 band=band, 

345 psfModel=exposure.getPsf(), 

346 redistributeImage=redistributeImage, 

347 removeScarletData=True, 

348 updateFluxColumns=False, 

349 )