Coverage for python/lsst/ap/association/diaForcedSource.py: 31%

77 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-19 11:23 +0000

1# This file is part of ap_association. 

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 

22"""Methods for force photometering direct and difference images at DiaObject 

23locations. 

24""" 

25 

26__all__ = ["DiaForcedSourceTask", "DiaForcedSourcedConfig"] 

27 

28import warnings 

29import numpy as np 

30 

31import lsst.afw.table as afwTable 

32from lsst.daf.base import DateTime 

33import lsst.geom as geom 

34from lsst.meas.base import ForcedMeasurementTask 

35import lsst.pex.config as pexConfig 

36import lsst.pipe.base as pipeBase 

37from lsst.utils.timer import timeMethod 

38 

39 

40class DiaForcedSourcedConfig(pexConfig.Config): 

41 """Configuration for the generic DiaForcedSourcedTask class. 

42 """ 

43 forcedMeasurement = pexConfig.ConfigurableField( 

44 target=ForcedMeasurementTask, 

45 doc="Subtask to force photometer DiaObjects in the direct and " 

46 "difference images.", 

47 ) 

48 dropColumns = pexConfig.ListField( 

49 dtype=str, 

50 doc="Columns produced in forced measurement that can be dropped upon " 

51 "creation and storage of the final pandas data.", 

52 ) 

53 

54 def setDefaults(self): 

55 self.forcedMeasurement.plugins = ["base_TransformedCentroidFromCoord", 

56 "base_PsfFlux"] 

57 self.forcedMeasurement.doReplaceWithNoise = False 

58 self.forcedMeasurement.copyColumns = { 

59 "id": "diaObjectId", 

60 "coord_ra": "coord_ra", 

61 "coord_dec": "coord_dec"} 

62 self.forcedMeasurement.slots.centroid = "base_TransformedCentroidFromCoord" 

63 self.forcedMeasurement.slots.psfFlux = "base_PsfFlux" 

64 self.forcedMeasurement.slots.shape = None 

65 self.dropColumns = ['coord_ra', 'coord_dec', 'parent', 

66 'base_TransformedCentroidFromCoord_x', 

67 'base_TransformedCentroidFromCoord_y', 

68 'base_PsfFlux_instFlux', 

69 'base_PsfFlux_instFluxErr', 'base_PsfFlux_area', 

70 'slot_PsfFlux_area', 'base_PsfFlux_flag', 

71 'slot_PsfFlux_flag', 

72 'base_PsfFlux_flag_noGoodPixels', 

73 'slot_PsfFlux_flag_noGoodPixels', 

74 'base_PsfFlux_flag_edge', 'slot_PsfFlux_flag_edge', 

75 'base_PsfFlux_chi2', 'slot_PsfFlux_chi2', 

76 'base_PsfFlux_npixels', 'slot_PsfFlux_npixels', 

77 ] 

78 

79 

80class DiaForcedSourceTask(pipeBase.Task): 

81 """Task for measuring and storing forced sources at DiaObject locations 

82 in both difference and direct images. 

83 """ 

84 ConfigClass = DiaForcedSourcedConfig 

85 _DefaultName = "diaForcedSource" 

86 

87 def __init__(self, **kwargs): 

88 pipeBase.Task.__init__(self, **kwargs) 

89 self.makeSubtask("forcedMeasurement", 

90 refSchema=afwTable.SourceTable.makeMinimalSchema()) 

91 

92 @timeMethod 

93 def run(self, 

94 dia_objects, 

95 updatedDiaObjectIds, 

96 expIdBits, # TODO: remove on DM-38687. 

97 exposure, 

98 diffim, 

99 idGenerator=None): 

100 """Measure forced sources on the direct and difference images. 

101 

102 Parameters 

103 ---------- 

104 dia_objects : `pandas.DataFrame` 

105 Catalog of previously observed and newly created DiaObjects 

106 contained within the difference and direct images. DiaObjects 

107 must be indexed on the ``diaObjectId`` column. 

108 updatedDiaObjectIds : `numpy.ndarray` 

109 Array of diaObjectIds that were updated during this dia processing. 

110 Used to assure that the pipeline includes all diaObjects that were 

111 updated in case one falls on the edge of the CCD. 

112 expIdBits : `int` 

113 Bit length of the exposure id. Deprecated in favor of 

114 ``idGenerator``, and ignored if that is present. Pass `None` 

115 explicitly to avoid a deprecation warning (a default is impossible 

116 given that later positional arguments are not defaulted). 

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

118 Direct image exposure. 

119 diffim : `lsst.afw.image.Exposure` 

120 Difference image. 

121 idGenerator : `lsst.meas.base.IdGenerator`, optional 

122 Object that generates source IDs and random number generator seeds. 

123 Will be required after ``expIdBits`` is removed. 

124 

125 Returns 

126 ------- 

127 output_forced_sources : `pandas.DataFrame` 

128 Catalog of calibrated forced photometered fluxes on both the 

129 difference and direct images at DiaObject locations. 

130 """ 

131 

132 afw_dia_objects = self._convert_from_pandas(dia_objects) 

133 

134 if expIdBits is not None: 

135 warnings.warn( 

136 "'expIdBits' argument is deprecated in favor of 'idGenerator'; will be removed after v26.", 

137 category=FutureWarning, 

138 stacklevel=3, # Caller + timeMethod 

139 ) 

140 

141 if idGenerator is None: 

142 idFactoryDiff = afwTable.IdFactory.makeSource( 

143 diffim.info.id, 

144 afwTable.IdFactory.computeReservedFromMaxBits(int(expIdBits))) 

145 else: 

146 idFactoryDiff = idGenerator.make_table_id_factory() 

147 

148 diffForcedSources = self.forcedMeasurement.generateMeasCat( 

149 diffim, 

150 afw_dia_objects, 

151 diffim.getWcs(), 

152 idFactory=idFactoryDiff) 

153 self.forcedMeasurement.run( 

154 diffForcedSources, diffim, afw_dia_objects, diffim.getWcs()) 

155 

156 directForcedSources = self.forcedMeasurement.generateMeasCat( 

157 exposure, 

158 afw_dia_objects, 

159 exposure.getWcs(), 

160 idFactory=idFactoryDiff) 

161 self.forcedMeasurement.run( 

162 directForcedSources, exposure, afw_dia_objects, exposure.getWcs()) 

163 

164 output_forced_sources = self._calibrate_and_merge(diffForcedSources, 

165 directForcedSources, 

166 diffim, 

167 exposure) 

168 

169 output_forced_sources = self._trim_to_exposure(output_forced_sources, 

170 updatedDiaObjectIds, 

171 exposure) 

172 return output_forced_sources.set_index( 

173 ["diaObjectId", "diaForcedSourceId"], 

174 drop=False) 

175 

176 def _convert_from_pandas(self, input_objects): 

177 """Create minimal schema SourceCatalog from a pandas DataFrame. 

178 

179 We need a catalog of this type to run within the forced measurement 

180 subtask. 

181 

182 Parameters 

183 ---------- 

184 input_objects : `pandas.DataFrame` 

185 DiaObjects with locations and ids. `` 

186 

187 Returns 

188 ------- 

189 outputCatalog : `lsst.afw.table.SourceTable` 

190 Output catalog with minimal schema. 

191 """ 

192 schema = afwTable.SourceTable.makeMinimalSchema() 

193 

194 outputCatalog = afwTable.SourceCatalog(schema) 

195 outputCatalog.reserve(len(input_objects)) 

196 

197 for obj_id, df_row in input_objects.iterrows(): 

198 outputRecord = outputCatalog.addNew() 

199 outputRecord.setId(obj_id) 

200 outputRecord.setCoord( 

201 geom.SpherePoint(df_row["ra"], 

202 df_row["dec"], 

203 geom.degrees)) 

204 return outputCatalog 

205 

206 def _calibrate_and_merge(self, 

207 diff_sources, 

208 direct_sources, 

209 diff_exp, 

210 direct_exp): 

211 """Take the two output catalogs from the ForcedMeasurementTasks and 

212 calibrate, combine, and convert them to Pandas. 

213 

214 Parameters 

215 ---------- 

216 diff_sources : `lsst.afw.table.SourceTable` 

217 Catalog with PsFluxes measured on the difference image. 

218 direct_sources : `lsst.afw.table.SourceTable` 

219 Catalog with PsfFluxes measured on the direct (calexp) image. 

220 diff_exp : `lsst.afw.image.Exposure` 

221 Difference exposure ``diff_sources`` were measured on. 

222 direct_exp : `lsst.afw.image.Exposure` 

223 Direct (calexp) exposure ``direct_sources`` were measured on. 

224 

225 Returns 

226 ------- 

227 output_catalog : `pandas.DataFrame` 

228 Catalog calibrated diaForcedSources. 

229 """ 

230 diff_calib = diff_exp.getPhotoCalib() 

231 direct_calib = direct_exp.getPhotoCalib() 

232 

233 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources, 

234 "slot_PsfFlux") 

235 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources, 

236 "slot_PsfFlux") 

237 

238 output_catalog = diff_sources.asAstropy().to_pandas() 

239 output_catalog.rename(columns={"id": "diaForcedSourceId", 

240 "slot_PsfFlux_instFlux": "psfFlux", 

241 "slot_PsfFlux_instFluxErr": "psfFluxErr", 

242 "slot_Centroid_x": "x", 

243 "slot_Centroid_y": "y"}, 

244 inplace=True) 

245 output_catalog.loc[:, "psfFlux"] = diff_fluxes[:, 0] 

246 output_catalog.loc[:, "psfFluxErr"] = diff_fluxes[:, 1] 

247 

248 output_catalog["scienceFlux"] = direct_fluxes[:, 0] 

249 output_catalog["scienceFluxErr"] = direct_fluxes[:, 1] 

250 

251 visit_info = direct_exp.visitInfo 

252 ccdVisitId = direct_exp.info.id 

253 midpointMjdTai = visit_info.date.get(system=DateTime.MJD) 

254 output_catalog["ccdVisitId"] = ccdVisitId 

255 output_catalog["midpointMjdTai"] = midpointMjdTai 

256 output_catalog["band"] = diff_exp.getFilter().bandLabel 

257 output_catalog["time_processed"] = DateTime.now().toPython() 

258 

259 # Drop superfluous columns from output DataFrame. 

260 output_catalog.drop(columns=self.config.dropColumns, inplace=True) 

261 

262 return output_catalog 

263 

264 def _trim_to_exposure(self, catalog, updatedDiaObjectIds, exposure): 

265 """Remove DiaForcedSources that are outside of the bounding box region. 

266 

267 Paramters 

268 --------- 

269 catalog : `pandas.DataFrame` 

270 DiaForcedSources to check against the exposure bounding box. 

271 updatedDiaObjectIds : `numpy.ndarray` 

272 Array of diaObjectIds that were updated during this dia processing. 

273 Used to assure that the pipeline includes all diaObjects that were 

274 updated in case one falls on the edge of the CCD. 

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

276 Exposure to check against. 

277 

278 Returns 

279 ------- 

280 output : `pandas.DataFrame` 

281 DataFrame trimmed to only the objects within the exposure bounding 

282 box. 

283 """ 

284 bbox = geom.Box2D(exposure.getBBox()) 

285 

286 xS = catalog.loc[:, "x"] 

287 yS = catalog.loc[:, "y"] 

288 

289 return catalog[ 

290 np.logical_or(bbox.contains(xS, yS), 

291 np.isin(catalog.loc[:, "diaObjectId"], 

292 updatedDiaObjectIds))]