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 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.pluginRegistry import register 

34from lsst.meas.base import ( 

35 ForcedMeasurementTask, 

36 ForcedTransformedCentroidConfig, 

37 ForcedTransformedCentroidPlugin) 

38import lsst.pex.config as pexConfig 

39import lsst.pipe.base as pipeBase 

40 

41 

42class ForcedTransformedCentroidFromCoordConfig(ForcedTransformedCentroidConfig): 

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

44 """ 

45 pass 

46 

47 

48@register("ap_assoc_TransformedCentroid") 

49class ForcedTransformedCentroidFromCoordPlugin(ForcedTransformedCentroidPlugin): 

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

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

52 measurement coordinate system and stored. 

53 

54 Parameters 

55 ---------- 

56 config : `ForcedTransformedCentroidFromCoordConfig` 

57 Plugin configuration 

58 name : `str` 

59 Plugin name 

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

61 A mapping from reference catalog fields to output 

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

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

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

65 

66 Notes 

67 ----- 

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

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

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

71 """ 

72 

73 ConfigClass = ForcedTransformedCentroidFromCoordConfig 

74 

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

76 targetWcs = exposure.getWcs() 

77 

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

79 measRecord.set(self.centroidKey, targetPos) 

80 

81 if self.flagKey is not None: 

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

83 

84 

85class DiaForcedSourcedConfig(pexConfig.Config): 

86 """Configuration for the generic DiaForcedSourcedTask class. 

87 """ 

88 forcedMeasurement = pexConfig.ConfigurableField( 

89 target=ForcedMeasurementTask, 

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

91 "difference images.", 

92 ) 

93 dropColumns = pexConfig.ListField( 

94 dtype=str, 

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

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

97 ) 

98 

99 def setDefaults(self): 

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

101 "base_PsfFlux"] 

102 self.forcedMeasurement.doReplaceWithNoise = False 

103 self.forcedMeasurement.copyColumns = { 

104 "id": "diaObjectId", 

105 "coord_ra": "coord_ra", 

106 "coord_dec": "coord_dec"} 

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

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

109 self.forcedMeasurement.slots.shape = None 

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

111 'ap_assoc_TransformedCentroid_x', 

112 'ap_assoc_TransformedCentroid_y', 

113 'base_PsfFlux_instFlux', 

114 'base_PsfFlux_instFluxErr', 'base_PsfFlux_area', 

115 'slot_PsfFlux_area', 'base_PsfFlux_flag', 

116 'slot_PsfFlux_flag', 

117 'base_PsfFlux_flag_noGoodPixels', 

118 'slot_PsfFlux_flag_noGoodPixels', 

119 'base_PsfFlux_flag_edge', 'slot_PsfFlux_flag_edge', 

120 'base_PsfFlux_chi2', 'slot_PsfFlux_chi2', 

121 'base_PsfFlux_npixels', 'slot_PsfFlux_npixels', 

122 ] 

123 

124 

125class DiaForcedSourceTask(pipeBase.Task): 

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

127 in both difference and direct images. 

128 """ 

129 ConfigClass = DiaForcedSourcedConfig 

130 _DefaultName = "diaForcedSource" 

131 

132 def __init__(self, **kwargs): 

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

134 self.makeSubtask("forcedMeasurement", 

135 refSchema=afwTable.SourceTable.makeMinimalSchema()) 

136 

137 @pipeBase.timeMethod 

138 def run(self, 

139 dia_objects, 

140 updatedDiaObjectIds, 

141 expIdBits, 

142 exposure, 

143 diffim): 

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

145 

146 Parameters 

147 ---------- 

148 dia_objects : `pandas.DataFrame` 

149 Catalog of previously observed and newly created DiaObjects 

150 contained within the difference and direct images. DiaObjects 

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

152 updatedDiaObjectIds : `numpy.ndarray` 

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

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

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

156 expIdBits : `int` 

157 Bit length of the exposure id. 

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

159 Direct image exposure. 

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

161 Difference image. 

162 

163 Returns 

164 ------- 

165 output_forced_sources : `pandas.DataFrame` 

166 Catalog of calibrated forced photometered fluxes on both the 

167 difference and direct images at DiaObject locations. 

168 """ 

169 

170 afw_dia_objects = self._convert_from_pandas(dia_objects) 

171 

172 idFactoryDiff = afwTable.IdFactory.makeSource( 

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

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

175 

176 diffForcedSources = self.forcedMeasurement.generateMeasCat( 

177 diffim, 

178 afw_dia_objects, 

179 diffim.getWcs(), 

180 idFactory=idFactoryDiff) 

181 self.forcedMeasurement.run( 

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

183 

184 directForcedSources = self.forcedMeasurement.generateMeasCat( 

185 exposure, 

186 afw_dia_objects, 

187 exposure.getWcs()) 

188 self.forcedMeasurement.run( 

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

190 

191 output_forced_sources = self._calibrate_and_merge(diffForcedSources, 

192 directForcedSources, 

193 diffim, 

194 exposure) 

195 

196 output_forced_sources = self._trim_to_exposure(output_forced_sources, 

197 updatedDiaObjectIds, 

198 exposure) 

199 return output_forced_sources.set_index( 

200 ["diaObjectId", "diaForcedSourceId"], 

201 drop=False) 

202 

203 def _convert_from_pandas(self, input_objects): 

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

205 

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

207 subtask. 

208 

209 Parameters 

210 ---------- 

211 input_objects : `pandas.DataFrame` 

212 DiaObjects with locations and ids. `` 

213 

214 Returns 

215 ------- 

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

217 Output catalog with minimal schema. 

218 """ 

219 schema = afwTable.SourceTable.makeMinimalSchema() 

220 

221 outputCatalog = afwTable.SourceCatalog(schema) 

222 outputCatalog.reserve(len(input_objects)) 

223 

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

225 outputRecord = outputCatalog.addNew() 

226 outputRecord.setId(obj_id) 

227 outputRecord.setCoord( 

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

229 df_row["decl"], 

230 geom.degrees)) 

231 return outputCatalog 

232 

233 def _calibrate_and_merge(self, 

234 diff_sources, 

235 direct_sources, 

236 diff_exp, 

237 direct_exp): 

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

239 calibrate, combine, and convert them to Pandas. 

240 

241 Parameters 

242 ---------- 

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

244 Catalog with PsFluxes measured on the difference image. 

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

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

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

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

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

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

251 

252 Returns 

253 ------- 

254 output_catalog : `pandas.DataFrame` 

255 Catalog calibrated diaForcedSources. 

256 """ 

257 diff_calib = diff_exp.getPhotoCalib() 

258 direct_calib = direct_exp.getPhotoCalib() 

259 

260 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources, 

261 "slot_PsfFlux") 

262 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources, 

263 "slot_PsfFlux") 

264 

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

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

267 "slot_PsfFlux_instFlux": "psFlux", 

268 "slot_PsfFlux_instFluxErr": "psFluxErr", 

269 "slot_Centroid_x": "x", 

270 "slot_Centroid_y": "y"}, 

271 inplace=True) 

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

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

274 

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

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

277 

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

279 ccdVisitId = visit_info.getExposureId() 

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

281 output_catalog["ccdVisitId"] = ccdVisitId 

282 output_catalog["midPointTai"] = midPointTaiMJD 

283 output_catalog["filterName"] = diff_exp.getFilterLabel().bandLabel 

284 

285 # Drop superfluous columns from output DataFrame. 

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

287 

288 return output_catalog 

289 

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

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

292 

293 Paramters 

294 --------- 

295 catalog : `pandas.DataFrame` 

296 DiaForcedSources to check against the exposure bounding box. 

297 updatedDiaObjectIds : `numpy.ndarray` 

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

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

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

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

302 Exposure to check against. 

303 

304 Returns 

305 ------- 

306 output : `pandas.DataFrame` 

307 DataFrame trimmed to only the objects within the exposure bounding 

308 box. 

309 """ 

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

311 

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

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

314 

315 return catalog[ 

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

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

318 updatedDiaObjectIds))]