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

71 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-10 10:38 +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 'base_InvalidPsf_flag', 

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 exposure, 

97 diffim, 

98 idGenerator): 

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

100 

101 Parameters 

102 ---------- 

103 dia_objects : `pandas.DataFrame` 

104 Catalog of previously observed and newly created DiaObjects 

105 contained within the difference and direct images. DiaObjects 

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

107 updatedDiaObjectIds : `numpy.ndarray` 

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

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

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

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

112 Direct image exposure. 

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

114 Difference image. 

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

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

117 

118 Returns 

119 ------- 

120 output_forced_sources : `pandas.DataFrame` 

121 Catalog of calibrated forced photometered fluxes on both the 

122 difference and direct images at DiaObject locations. 

123 """ 

124 

125 afw_dia_objects = self._convert_from_pandas(dia_objects) 

126 

127 idFactoryDiff = idGenerator.make_table_id_factory() 

128 

129 diffForcedSources = self.forcedMeasurement.generateMeasCat( 

130 diffim, 

131 afw_dia_objects, 

132 diffim.getWcs(), 

133 idFactory=idFactoryDiff) 

134 self.forcedMeasurement.run( 

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

136 

137 directForcedSources = self.forcedMeasurement.generateMeasCat( 

138 exposure, 

139 afw_dia_objects, 

140 exposure.getWcs(), 

141 idFactory=idFactoryDiff) 

142 self.forcedMeasurement.run( 

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

144 

145 output_forced_sources = self._calibrate_and_merge(diffForcedSources, 

146 directForcedSources, 

147 diffim, 

148 exposure) 

149 

150 output_forced_sources = self._trim_to_exposure(output_forced_sources, 

151 updatedDiaObjectIds, 

152 exposure) 

153 return output_forced_sources.set_index( 

154 ["diaObjectId", "diaForcedSourceId"], 

155 drop=False) 

156 

157 def _convert_from_pandas(self, input_objects): 

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

159 

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

161 subtask. 

162 

163 Parameters 

164 ---------- 

165 input_objects : `pandas.DataFrame` 

166 DiaObjects with locations and ids. `` 

167 

168 Returns 

169 ------- 

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

171 Output catalog with minimal schema. 

172 """ 

173 schema = afwTable.SourceTable.makeMinimalSchema() 

174 

175 outputCatalog = afwTable.SourceCatalog(schema) 

176 outputCatalog.reserve(len(input_objects)) 

177 

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

179 outputRecord = outputCatalog.addNew() 

180 outputRecord.setId(obj_id) 

181 outputRecord.setCoord( 

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

183 df_row["dec"], 

184 geom.degrees)) 

185 return outputCatalog 

186 

187 def _calibrate_and_merge(self, 

188 diff_sources, 

189 direct_sources, 

190 diff_exp, 

191 direct_exp): 

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

193 calibrate, combine, and convert them to Pandas. 

194 

195 Parameters 

196 ---------- 

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

198 Catalog with PsFluxes measured on the difference image. 

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

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

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

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

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

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

205 

206 Returns 

207 ------- 

208 output_catalog : `pandas.DataFrame` 

209 Catalog calibrated diaForcedSources. 

210 """ 

211 diff_calib = diff_exp.getPhotoCalib() 

212 direct_calib = direct_exp.getPhotoCalib() 

213 

214 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources, 

215 "slot_PsfFlux") 

216 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources, 

217 "slot_PsfFlux") 

218 

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

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

221 "slot_PsfFlux_instFlux": "psfFlux", 

222 "slot_PsfFlux_instFluxErr": "psfFluxErr", 

223 "slot_Centroid_x": "x", 

224 "slot_Centroid_y": "y"}, 

225 inplace=True) 

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

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

228 

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

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

231 

232 midpointMjdTai = direct_exp.visitInfo.date.get(system=DateTime.MJD) 

233 output_catalog["visit"] = direct_exp.visitInfo.id 

234 output_catalog["detector"] = direct_exp.detector.getId() 

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 

240 # Drop superfluous columns from output DataFrame. 

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

242 

243 return output_catalog 

244 

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

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

247 

248 Paramters 

249 --------- 

250 catalog : `pandas.DataFrame` 

251 DiaForcedSources to check against the exposure bounding box. 

252 updatedDiaObjectIds : `numpy.ndarray` 

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

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

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

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

257 Exposure to check against. 

258 

259 Returns 

260 ------- 

261 output : `pandas.DataFrame` 

262 DataFrame trimmed to only the objects within the exposure bounding 

263 box. 

264 """ 

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

266 

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

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

269 

270 return catalog[ 

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

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

273 updatedDiaObjectIds))]