Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 lsst.afw.table as afwTable 

29from lsst.daf.base import DateTime 

30import lsst.geom as geom 

31from lsst.meas.base.pluginRegistry import register 

32from lsst.meas.base import ( 

33 ForcedMeasurementTask, 

34 ForcedTransformedCentroidConfig, 

35 ForcedTransformedCentroidPlugin) 

36import lsst.pex.config as pexConfig 

37import lsst.pipe.base as pipeBase 

38 

39 

40class ForcedTransformedCentroidFromCoordConfig(ForcedTransformedCentroidConfig): 

41 """Configuration for the forced transformed coord algorithm. 

42 """ 

43 pass 

44 

45 

46@register("ap_assoc_TransformedCentroid") 

47class ForcedTransformedCentroidFromCoordPlugin(ForcedTransformedCentroidPlugin): 

48 """Record the transformation of the reference catalog coord. 

49 The coord recorded in the reference catalog is tranformed to the 

50 measurement coordinate system and stored. 

51 

52 Parameters 

53 ---------- 

54 config : `ForcedTransformedCentroidFromCoordConfig` 

55 Plugin configuration 

56 name : `str` 

57 Plugin name 

58 schemaMapper : `lsst.afw.table.SchemaMapper` 

59 A mapping from reference catalog fields to output 

60 catalog fields. Output fields are added to the output schema. 

61 metadata : `lsst.daf.base.PropertySet` 

62 Plugin metadata that will be attached to the output catalog. 

63 

64 Notes 

65 ----- 

66 This can be used as the slot centroid in forced measurement when only a 

67 reference coord exits, allowing subsequent measurements to simply refer to 

68 the slot value just as they would in single-frame measurement. 

69 """ 

70 

71 ConfigClass = ForcedTransformedCentroidFromCoordConfig 

72 

73 def measure(self, measRecord, exposure, refRecord, refWcs): 

74 targetWcs = exposure.getWcs() 

75 

76 targetPos = targetWcs.skyToPixel(refRecord.getCoord()) 

77 measRecord.set(self.centroidKey, targetPos) 

78 

79 if self.flagKey is not None: 

80 measRecord.set(self.flagKey, refRecord.getCentroidFlag()) 

81 

82 

83class DiaForcedSourcedConfig(pexConfig.Config): 

84 """Configuration for the generic DiaForcedSourcedTask class. 

85 """ 

86 forcedMeasurement = pexConfig.ConfigurableField( 

87 target=ForcedMeasurementTask, 

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

89 "difference images.", 

90 ) 

91 dropColumns = pexConfig.ListField( 

92 dtype=str, 

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

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

95 ) 

96 

97 def setDefaults(self): 

98 self.forcedMeasurement.plugins = ["ap_assoc_TransformedCentroid", 

99 "base_PsfFlux"] 

100 self.forcedMeasurement.doReplaceWithNoise = False 

101 self.forcedMeasurement.copyColumns = { 

102 "id": "diaObjectId", 

103 "coord_ra": "coord_ra", 

104 "coord_dec": "coord_dec"} 

105 self.forcedMeasurement.slots.centroid = "ap_assoc_TransformedCentroid" 

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

107 self.forcedMeasurement.slots.shape = None 

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

109 'ap_assoc_TransformedCentroid_x', 

110 'ap_assoc_TransformedCentroid_y', 

111 'base_PsfFlux_instFlux', 

112 'base_PsfFlux_instFluxErr', 'base_PsfFlux_area', 

113 'slot_PsfFlux_area', 'base_PsfFlux_flag', 

114 'slot_PsfFlux_flag', 

115 'base_PsfFlux_flag_noGoodPixels', 

116 'slot_PsfFlux_flag_noGoodPixels', 

117 'base_PsfFlux_flag_edge', 'slot_PsfFlux_flag_edge'] 

118 

119 

120class DiaForcedSourceTask(pipeBase.Task): 

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

122 in both difference and direct images. 

123 """ 

124 ConfigClass = DiaForcedSourcedConfig 

125 _DefaultName = "diaForcedSource" 

126 

127 def __init__(self, **kwargs): 

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

129 self.makeSubtask("forcedMeasurement", 

130 refSchema=afwTable.SourceTable.makeMinimalSchema()) 

131 

132 @pipeBase.timeMethod 

133 def run(self, dia_objects, expIdBits, exposure, diffim): 

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

135 

136 Parameters 

137 ---------- 

138 dia_objects : `pandas.DataFrame` 

139 Catalog of previously observed and newly created DiaObjects 

140 contained within the difference and direct images. 

141 expIdBits : `int` 

142 Bit length of the exposure id. 

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

144 Direct image exposure. 

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

146 Difference image. 

147 

148 Returns 

149 ------- 

150 output_forced_sources : `pandas.DataFrame` 

151 Catalog of calibrated forced photometered fluxes on both the 

152 difference and direct images at DiaObject locations. 

153 """ 

154 

155 afw_dia_objects = self._convert_from_pandas(dia_objects) 

156 

157 idFactoryDiff = afwTable.IdFactory.makeSource( 

158 diffim.getInfo().getVisitInfo().getExposureId(), 

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

160 

161 diffForcedSources = self.forcedMeasurement.generateMeasCat( 

162 diffim, 

163 afw_dia_objects, 

164 diffim.getWcs(), 

165 idFactory=idFactoryDiff) 

166 self.forcedMeasurement.run( 

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

168 

169 directForcedSources = self.forcedMeasurement.generateMeasCat( 

170 exposure, 

171 afw_dia_objects, 

172 exposure.getWcs()) 

173 self.forcedMeasurement.run( 

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

175 

176 output_forced_sources = self._calibrate_and_merge(diffForcedSources, 

177 directForcedSources, 

178 diffim, 

179 exposure) 

180 

181 output_forced_sources = self._trim_to_exposure(output_forced_sources, 

182 exposure) 

183 return output_forced_sources.set_index( 

184 ["diaObjectId", "diaForcedSourceId"], 

185 drop=False) 

186 

187 def _convert_from_pandas(self, input_objects): 

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

189 

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

191 subtask. 

192 

193 Parameters 

194 ---------- 

195 input_objects : `pandas.DataFrame` 

196 DiaObjects with locations and ids. `` 

197 

198 Returns 

199 ------- 

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

201 Output catalog with minimal schema. 

202 """ 

203 schema = afwTable.SourceTable.makeMinimalSchema() 

204 

205 outputCatalog = afwTable.SourceCatalog(schema) 

206 outputCatalog.reserve(len(input_objects)) 

207 

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

209 outputRecord = outputCatalog.addNew() 

210 outputRecord.setId(obj_id) 

211 outputRecord.setCoord( 

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

213 df_row["decl"], 

214 geom.degrees)) 

215 return outputCatalog 

216 

217 def _calibrate_and_merge(self, 

218 diff_sources, 

219 direct_sources, 

220 diff_exp, 

221 direct_exp): 

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

223 calibrate, combine, and convert them to Pandas. 

224 

225 Parameters 

226 ---------- 

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

228 Catalog with PsFluxes measured on the difference image. 

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

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

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

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

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

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

235 

236 Returns 

237 ------- 

238 output_catalog : `pandas.DataFrame` 

239 Catalog calibrated diaForcedSources. 

240 """ 

241 diff_calib = diff_exp.getPhotoCalib() 

242 direct_calib = direct_exp.getPhotoCalib() 

243 

244 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources, 

245 "slot_PsfFlux") 

246 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources, 

247 "slot_PsfFlux") 

248 

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

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

251 "slot_PsfFlux_instFlux": "psFlux", 

252 "slot_PsfFlux_instFluxErr": "psFluxErr", 

253 "slot_Centroid_x": "x", 

254 "slot_Centroid_y": "y"}, 

255 inplace=True) 

256 output_catalog.loc[:, "psFlux"] = diff_fluxes[:, 0] 

257 output_catalog.loc[:, "psFluxErr"] = diff_fluxes[:, 1] 

258 

259 output_catalog["totFlux"] = direct_fluxes[:, 0] 

260 output_catalog["totFluxErr"] = direct_fluxes[:, 1] 

261 

262 visit_info = direct_exp.getInfo().getVisitInfo() 

263 ccdVisitId = visit_info.getExposureId() 

264 midPointTaiMJD = visit_info.getDate().get(system=DateTime.MJD) 

265 output_catalog["ccdVisitId"] = ccdVisitId 

266 output_catalog["midPointTai"] = midPointTaiMJD 

267 output_catalog["filterName"] = diff_exp.getFilter().getCanonicalName() 

268 

269 # Drop superfluous columns from output DataFrame. 

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

271 

272 return output_catalog 

273 

274 def _trim_to_exposure(self, catalog, exposure): 

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

276 

277 Paramters 

278 --------- 

279 catalog : `pandas.DataFrame` 

280 DiaForcedSources to check against the exposure bounding box. 

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

282 Exposure to check against. 

283 

284 Returns 

285 ------- 

286 output : `pandas.DataFrame` 

287 DataFrame trimmed to only the objects within the exposure bounding 

288 box. 

289 """ 

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

291 

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

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

294 

295 return catalog[bbox.contains(xS, yS)]