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 

121 

122class DiaForcedSourceTask(pipeBase.Task): 

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

124 in both difference and direct images. 

125 """ 

126 ConfigClass = DiaForcedSourcedConfig 

127 _DefaultName = "diaForcedSource" 

128 

129 def __init__(self, **kwargs): 

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

131 self.makeSubtask("forcedMeasurement", 

132 refSchema=afwTable.SourceTable.makeMinimalSchema()) 

133 

134 @pipeBase.timeMethod 

135 def run(self, 

136 dia_objects, 

137 updatedDiaObjectIds, 

138 expIdBits, 

139 exposure, 

140 diffim): 

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

142 

143 Parameters 

144 ---------- 

145 dia_objects : `pandas.DataFrame` 

146 Catalog of previously observed and newly created DiaObjects 

147 contained within the difference and direct images. DiaObjects 

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

149 updatedDiaObjectIds : `numpy.ndarray` 

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

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

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

153 expIdBits : `int` 

154 Bit length of the exposure id. 

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

156 Direct image exposure. 

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

158 Difference image. 

159 

160 Returns 

161 ------- 

162 output_forced_sources : `pandas.DataFrame` 

163 Catalog of calibrated forced photometered fluxes on both the 

164 difference and direct images at DiaObject locations. 

165 """ 

166 

167 afw_dia_objects = self._convert_from_pandas(dia_objects) 

168 

169 idFactoryDiff = afwTable.IdFactory.makeSource( 

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

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

172 

173 diffForcedSources = self.forcedMeasurement.generateMeasCat( 

174 diffim, 

175 afw_dia_objects, 

176 diffim.getWcs(), 

177 idFactory=idFactoryDiff) 

178 self.forcedMeasurement.run( 

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

180 

181 directForcedSources = self.forcedMeasurement.generateMeasCat( 

182 exposure, 

183 afw_dia_objects, 

184 exposure.getWcs()) 

185 self.forcedMeasurement.run( 

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

187 

188 output_forced_sources = self._calibrate_and_merge(diffForcedSources, 

189 directForcedSources, 

190 diffim, 

191 exposure) 

192 

193 output_forced_sources = self._trim_to_exposure(output_forced_sources, 

194 updatedDiaObjectIds, 

195 exposure) 

196 return output_forced_sources.set_index( 

197 ["diaObjectId", "diaForcedSourceId"], 

198 drop=False) 

199 

200 def _convert_from_pandas(self, input_objects): 

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

202 

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

204 subtask. 

205 

206 Parameters 

207 ---------- 

208 input_objects : `pandas.DataFrame` 

209 DiaObjects with locations and ids. `` 

210 

211 Returns 

212 ------- 

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

214 Output catalog with minimal schema. 

215 """ 

216 schema = afwTable.SourceTable.makeMinimalSchema() 

217 

218 outputCatalog = afwTable.SourceCatalog(schema) 

219 outputCatalog.reserve(len(input_objects)) 

220 

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

222 outputRecord = outputCatalog.addNew() 

223 outputRecord.setId(obj_id) 

224 outputRecord.setCoord( 

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

226 df_row["decl"], 

227 geom.degrees)) 

228 return outputCatalog 

229 

230 def _calibrate_and_merge(self, 

231 diff_sources, 

232 direct_sources, 

233 diff_exp, 

234 direct_exp): 

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

236 calibrate, combine, and convert them to Pandas. 

237 

238 Parameters 

239 ---------- 

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

241 Catalog with PsFluxes measured on the difference image. 

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

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

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

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

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

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

248 

249 Returns 

250 ------- 

251 output_catalog : `pandas.DataFrame` 

252 Catalog calibrated diaForcedSources. 

253 """ 

254 diff_calib = diff_exp.getPhotoCalib() 

255 direct_calib = direct_exp.getPhotoCalib() 

256 

257 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources, 

258 "slot_PsfFlux") 

259 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources, 

260 "slot_PsfFlux") 

261 

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

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

264 "slot_PsfFlux_instFlux": "psFlux", 

265 "slot_PsfFlux_instFluxErr": "psFluxErr", 

266 "slot_Centroid_x": "x", 

267 "slot_Centroid_y": "y"}, 

268 inplace=True) 

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

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

271 

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

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

274 

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

276 ccdVisitId = visit_info.getExposureId() 

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

278 output_catalog["ccdVisitId"] = ccdVisitId 

279 output_catalog["midPointTai"] = midPointTaiMJD 

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

281 

282 # Drop superfluous columns from output DataFrame. 

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

284 

285 return output_catalog 

286 

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

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

289 

290 Paramters 

291 --------- 

292 catalog : `pandas.DataFrame` 

293 DiaForcedSources to check against the exposure bounding box. 

294 updatedDiaObjectIds : `numpy.ndarray` 

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

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

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

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

299 Exposure to check against. 

300 

301 Returns 

302 ------- 

303 output : `pandas.DataFrame` 

304 DataFrame trimmed to only the objects within the exposure bounding 

305 box. 

306 """ 

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

308 

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

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

311 

312 return catalog[ 

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

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

315 updatedDiaObjectIds))]