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 def run(self, dia_objects, expIdBits, exposure, diffim): 

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

134 

135 Parameters 

136 ---------- 

137 dia_objects : `pandas.DataFrame` 

138 Catalog of previously observed and newly created DiaObjects 

139 contained within the difference and direct images. 

140 expIdBits : `int` 

141 Bit length of the exposure id. 

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

143 Direct image exposure. 

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

145 Difference image. 

146 

147 Returns 

148 ------- 

149 output_forced_sources : `pandas.DataFrame` 

150 Catalog of calibrated forced photometered fluxes on both the 

151 difference and direct images at DiaObject locations. 

152 """ 

153 

154 afw_dia_objects = self._convert_from_pandas(dia_objects) 

155 

156 idFactoryDiff = afwTable.IdFactory.makeSource( 

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

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

159 

160 diffForcedSources = self.forcedMeasurement.generateMeasCat( 

161 diffim, 

162 afw_dia_objects, 

163 diffim.getWcs(), 

164 idFactory=idFactoryDiff) 

165 self.forcedMeasurement.run( 

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

167 

168 directForcedSources = self.forcedMeasurement.generateMeasCat( 

169 exposure, 

170 afw_dia_objects, 

171 exposure.getWcs()) 

172 self.forcedMeasurement.run( 

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

174 

175 output_forced_sources = self._calibrate_and_merge(diffForcedSources, 

176 directForcedSources, 

177 diffim, 

178 exposure) 

179 

180 return self._trim_to_exposure(output_forced_sources, exposure) 

181 

182 def _convert_from_pandas(self, input_objects): 

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

184 

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

186 subtask. 

187 

188 Parameters 

189 ---------- 

190 input_objects : `pandas.DataFrame` 

191 DiaObjects with locations and ids. `` 

192 

193 Returns 

194 ------- 

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

196 Output catalog with minimal schema. 

197 """ 

198 schema = afwTable.SourceTable.makeMinimalSchema() 

199 

200 outputCatalog = afwTable.SourceCatalog(schema) 

201 outputCatalog.reserve(len(input_objects)) 

202 

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

204 outputRecord = outputCatalog.addNew() 

205 outputRecord.setId(obj_id) 

206 outputRecord.setCoord( 

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

208 df_row["decl"], 

209 geom.degrees)) 

210 return outputCatalog 

211 

212 def _calibrate_and_merge(self, 

213 diff_sources, 

214 direct_sources, 

215 diff_exp, 

216 direct_exp): 

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

218 calibrate, combine, and convert them to Pandas. 

219 

220 Parameters 

221 ---------- 

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

223 Catalog with PsFluxes measured on the difference image. 

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

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

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

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

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

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

230 

231 Returns 

232 ------- 

233 output_catalog : `pandas.DataFrame` 

234 Catalog calibrated diaForcedSources. 

235 """ 

236 diff_calib = diff_exp.getPhotoCalib() 

237 direct_calib = direct_exp.getPhotoCalib() 

238 

239 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources, 

240 "slot_PsfFlux") 

241 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources, 

242 "slot_PsfFlux") 

243 

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

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

246 "slot_PsfFlux_instFlux": "psFlux", 

247 "slot_PsfFlux_instFluxErr": "psFluxErr", 

248 "slot_Centroid_x": "x", 

249 "slot_Centroid_y": "y"}, 

250 inplace=True) 

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

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

253 

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

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

256 

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

258 ccdVisitId = visit_info.getExposureId() 

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

260 output_catalog["ccdVisitId"] = ccdVisitId 

261 output_catalog["midPointTai"] = midPointTaiMJD 

262 

263 # Drop superfluous columns from output DataFrame. 

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

265 

266 return output_catalog 

267 

268 def _trim_to_exposure(self, catalog, exposure): 

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

270 

271 Paramters 

272 --------- 

273 catalog : `pandas.DataFrame` 

274 DiaForcedSources to check against the exposure bounding box. 

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[bbox.contains(xS, yS)]