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

73 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-03-12 11:45 +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 numpy as np 

29 

30import lsst.afw.table as afwTable 

31from lsst.daf.base import DateTime 

32import lsst.geom as geom 

33from lsst.meas.base import ForcedMeasurementTask 

34import lsst.pex.config as pexConfig 

35import lsst.pipe.base as pipeBase 

36from lsst.utils.timer import timeMethod 

37 

38 

39class DiaForcedSourcedConfig(pexConfig.Config): 

40 """Configuration for the generic DiaForcedSourcedTask class. 

41 """ 

42 forcedMeasurement = pexConfig.ConfigurableField( 

43 target=ForcedMeasurementTask, 

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

45 "difference images.", 

46 ) 

47 dropColumns = pexConfig.ListField( 

48 dtype=str, 

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

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

51 ) 

52 

53 def setDefaults(self): 

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

55 "base_PsfFlux"] 

56 self.forcedMeasurement.doReplaceWithNoise = False 

57 self.forcedMeasurement.copyColumns = { 

58 "id": "diaObjectId", 

59 "coord_ra": "coord_ra", 

60 "coord_dec": "coord_dec"} 

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

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

63 self.forcedMeasurement.slots.shape = None 

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

65 'base_TransformedCentroidFromCoord_x', 

66 'base_TransformedCentroidFromCoord_y', 

67 'base_PsfFlux_instFlux', 

68 'base_PsfFlux_instFluxErr', 'base_PsfFlux_area', 

69 'slot_PsfFlux_area', 'base_PsfFlux_flag', 

70 'slot_PsfFlux_flag', 

71 'base_PsfFlux_flag_noGoodPixels', 

72 'slot_PsfFlux_flag_noGoodPixels', 

73 'base_PsfFlux_flag_edge', 'slot_PsfFlux_flag_edge', 

74 'base_PsfFlux_chi2', 'slot_PsfFlux_chi2', 

75 'base_PsfFlux_npixels', 'slot_PsfFlux_npixels', 

76 ] 

77 

78 

79class DiaForcedSourceTask(pipeBase.Task): 

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

81 in both difference and direct images. 

82 """ 

83 ConfigClass = DiaForcedSourcedConfig 

84 _DefaultName = "diaForcedSource" 

85 

86 def __init__(self, **kwargs): 

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

88 self.makeSubtask("forcedMeasurement", 

89 refSchema=afwTable.SourceTable.makeMinimalSchema()) 

90 

91 @timeMethod 

92 def run(self, 

93 dia_objects, 

94 updatedDiaObjectIds, 

95 exposure, 

96 diffim, 

97 idGenerator): 

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

99 

100 Parameters 

101 ---------- 

102 dia_objects : `pandas.DataFrame` 

103 Catalog of previously observed and newly created DiaObjects 

104 contained within the difference and direct images. DiaObjects 

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

106 updatedDiaObjectIds : `numpy.ndarray` 

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

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

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

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

111 Direct image exposure. 

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

113 Difference image. 

114 idGenerator : `lsst.meas.base.IdGenerator` 

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

116 

117 Returns 

118 ------- 

119 output_forced_sources : `pandas.DataFrame` 

120 Catalog of calibrated forced photometered fluxes on both the 

121 difference and direct images at DiaObject locations. 

122 """ 

123 

124 afw_dia_objects = self._convert_from_pandas(dia_objects) 

125 

126 idFactoryDiff = idGenerator.make_table_id_factory() 

127 

128 diffForcedSources = self.forcedMeasurement.generateMeasCat( 

129 diffim, 

130 afw_dia_objects, 

131 diffim.getWcs(), 

132 idFactory=idFactoryDiff) 

133 self.forcedMeasurement.run( 

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

135 

136 directForcedSources = self.forcedMeasurement.generateMeasCat( 

137 exposure, 

138 afw_dia_objects, 

139 exposure.getWcs(), 

140 idFactory=idFactoryDiff) 

141 self.forcedMeasurement.run( 

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

143 

144 output_forced_sources = self._calibrate_and_merge(diffForcedSources, 

145 directForcedSources, 

146 diffim, 

147 exposure) 

148 

149 output_forced_sources = self._trim_to_exposure(output_forced_sources, 

150 updatedDiaObjectIds, 

151 exposure) 

152 return output_forced_sources.set_index( 

153 ["diaObjectId", "diaForcedSourceId"], 

154 drop=False) 

155 

156 def _convert_from_pandas(self, input_objects): 

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

158 

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

160 subtask. 

161 

162 Parameters 

163 ---------- 

164 input_objects : `pandas.DataFrame` 

165 DiaObjects with locations and ids. `` 

166 

167 Returns 

168 ------- 

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

170 Output catalog with minimal schema. 

171 """ 

172 schema = afwTable.SourceTable.makeMinimalSchema() 

173 

174 outputCatalog = afwTable.SourceCatalog(schema) 

175 outputCatalog.reserve(len(input_objects)) 

176 

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

178 outputRecord = outputCatalog.addNew() 

179 outputRecord.setId(obj_id) 

180 outputRecord.setCoord( 

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

182 df_row["dec"], 

183 geom.degrees)) 

184 return outputCatalog 

185 

186 def _calibrate_and_merge(self, 

187 diff_sources, 

188 direct_sources, 

189 diff_exp, 

190 direct_exp): 

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

192 calibrate, combine, and convert them to Pandas. 

193 

194 Parameters 

195 ---------- 

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

197 Catalog with PsFluxes measured on the difference image. 

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

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

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

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

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

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

204 

205 Returns 

206 ------- 

207 output_catalog : `pandas.DataFrame` 

208 Catalog calibrated diaForcedSources. 

209 """ 

210 diff_calib = diff_exp.getPhotoCalib() 

211 direct_calib = direct_exp.getPhotoCalib() 

212 

213 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources, 

214 "slot_PsfFlux") 

215 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources, 

216 "slot_PsfFlux") 

217 

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

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

220 "slot_PsfFlux_instFlux": "psfFlux", 

221 "slot_PsfFlux_instFluxErr": "psfFluxErr", 

222 "slot_Centroid_x": "x", 

223 "slot_Centroid_y": "y"}, 

224 inplace=True) 

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

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

227 

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

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

230 

231 visit_info = direct_exp.visitInfo 

232 ccdVisitId = direct_exp.info.id 

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

234 output_catalog["ccdVisitId"] = ccdVisitId 

235 output_catalog["midpointMjdTai"] = midpointMjdTai 

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

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

238 # TODO: propagate actual flags (DM-42355) 

239 output_catalog["flags"] = 0 

240 

241 # Drop superfluous columns from output DataFrame. 

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

243 

244 return output_catalog 

245 

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

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

248 

249 Paramters 

250 --------- 

251 catalog : `pandas.DataFrame` 

252 DiaForcedSources to check against the exposure bounding box. 

253 updatedDiaObjectIds : `numpy.ndarray` 

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

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

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

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

258 Exposure to check against. 

259 

260 Returns 

261 ------- 

262 output : `pandas.DataFrame` 

263 DataFrame trimmed to only the objects within the exposure bounding 

264 box. 

265 """ 

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

267 

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

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

270 

271 return catalog[ 

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

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

274 updatedDiaObjectIds))]