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

1from lsst.afw.table import ( 

2 SchemaMapper, 

3 Field, 

4 MultiMatch, 

5 SimpleRecord, 

6 SourceCatalog, 

7 updateSourceCoords, 

8) 

9 

10import numpy as np 

11from astropy.table import join, Table 

12 

13__all__ = ( 

14 "matchCatalogs", 

15 "ellipticityFromCat", 

16 "ellipticity", 

17 "makeMatchedPhotom", 

18 "mergeCatalogs", 

19) 

20 

21 

22def matchCatalogs(inputs, photoCalibs, astromCalibs, dataIds, matchRadius, logger=None): 

23 schema = inputs[0].schema 

24 mapper = SchemaMapper(schema) 

25 mapper.addMinimalSchema(schema) 

26 mapper.addOutputField(Field[float]("base_PsfFlux_snr", "PSF flux SNR")) 

27 mapper.addOutputField(Field[float]("base_PsfFlux_mag", "PSF magnitude")) 

28 mapper.addOutputField( 

29 Field[float]("base_PsfFlux_magErr", "PSF magnitude uncertainty") 

30 ) 

31 # Needed because addOutputField(... 'slot_ModelFlux_mag') will add a field with that literal name 

32 aliasMap = schema.getAliasMap() 

33 # Possibly not needed since base_GaussianFlux is the default, but this ought to be safe 

34 modelName = ( 

35 aliasMap["slot_ModelFlux"] 

36 if "slot_ModelFlux" in aliasMap.keys() 

37 else "base_GaussianFlux" 

38 ) 

39 mapper.addOutputField(Field[float](f"{modelName}_mag", "Model magnitude")) 

40 mapper.addOutputField( 

41 Field[float](f"{modelName}_magErr", "Model magnitude uncertainty") 

42 ) 

43 mapper.addOutputField(Field[float](f"{modelName}_snr", "Model flux snr")) 

44 mapper.addOutputField(Field[float]("e1", "Source Ellipticity 1")) 

45 mapper.addOutputField(Field[float]("e2", "Source Ellipticity 1")) 

46 mapper.addOutputField(Field[float]("psf_e1", "PSF Ellipticity 1")) 

47 mapper.addOutputField(Field[float]("psf_e2", "PSF Ellipticity 1")) 

48 mapper.addOutputField(Field[np.int32]("filt", "filter code")) 

49 newSchema = mapper.getOutputSchema() 

50 newSchema.setAliasMap(schema.getAliasMap()) 

51 

52 # Create an object that matches multiple catalogs with same schema 

53 mmatch = MultiMatch( 

54 newSchema, 

55 dataIdFormat={"visit": np.int32, "detector": np.int32}, 

56 radius=matchRadius, 

57 RecordClass=SimpleRecord, 

58 ) 

59 

60 # create the new extended source catalog 

61 srcVis = SourceCatalog(newSchema) 

62 

63 filter_dict = { 

64 "u": 1, 

65 "g": 2, 

66 "r": 3, 

67 "i": 4, 

68 "z": 5, 

69 "y": 6, 

70 "HSC-U": 1, 

71 "HSC-G": 2, 

72 "HSC-R": 3, 

73 "HSC-I": 4, 

74 "HSC-Z": 5, 

75 "HSC-Y": 6, 

76 } 

77 

78 # Sort by visit, detector, then filter 

79 vislist = [v["visit"] for v in dataIds] 

80 ccdlist = [v["detector"] for v in dataIds] 

81 filtlist = [v["band"] for v in dataIds] 

82 tab_vids = Table([vislist, ccdlist, filtlist], names=["vis", "ccd", "filt"]) 

83 sortinds = np.argsort(tab_vids, order=("vis", "ccd", "filt")) 

84 

85 for ind in sortinds: 

86 oldSrc = inputs[ind] 

87 photoCalib = photoCalibs[ind] 

88 wcs = astromCalibs[ind] 

89 dataId = dataIds[ind] 

90 

91 if logger: 

92 logger.debug( 

93 "%d sources in ccd %s visit %s", 

94 len(oldSrc), 

95 dataId["detector"], 

96 dataId["visit"], 

97 ) 

98 

99 # create temporary catalog 

100 tmpCat = SourceCatalog(SourceCatalog(newSchema).table) 

101 tmpCat.extend(oldSrc, mapper=mapper) 

102 

103 filtnum = filter_dict[dataId["band"]] 

104 tmpCat["filt"] = np.repeat(filtnum, len(oldSrc)) 

105 

106 tmpCat["base_PsfFlux_snr"][:] = ( 

107 tmpCat["base_PsfFlux_instFlux"] / tmpCat["base_PsfFlux_instFluxErr"] 

108 ) 

109 

110 updateSourceCoords(wcs, tmpCat) 

111 

112 photoCalib.instFluxToMagnitude(tmpCat, "base_PsfFlux", "base_PsfFlux") 

113 tmpCat["slot_ModelFlux_snr"][:] = ( 

114 tmpCat["slot_ModelFlux_instFlux"] / tmpCat["slot_ModelFlux_instFluxErr"] 

115 ) 

116 photoCalib.instFluxToMagnitude(tmpCat, "slot_ModelFlux", "slot_ModelFlux") 

117 

118 _, psf_e1, psf_e2 = ellipticityFromCat(oldSrc, slot_shape="slot_PsfShape") 

119 _, star_e1, star_e2 = ellipticityFromCat(oldSrc, slot_shape="slot_Shape") 

120 tmpCat["e1"][:] = star_e1 

121 tmpCat["e2"][:] = star_e2 

122 tmpCat["psf_e1"][:] = psf_e1 

123 tmpCat["psf_e2"][:] = psf_e2 

124 

125 srcVis.extend(tmpCat, False) 

126 mmatch.add(catalog=tmpCat, dataId=dataId) 

127 

128 # Complete the match, returning a catalog that includes 

129 # all matched sources with object IDs that can be used to group them. 

130 matchCat = mmatch.finish() 

131 

132 # Create a mapping object that allows the matches to be manipulated 

133 # as a mapping of object ID to catalog of sources. 

134 

135 # I don't think I can persist a group view, so this may need to be called in a subsequent task 

136 # allMatches = GroupView.build(matchCat) 

137 

138 return srcVis, matchCat 

139 

140 

141def ellipticityFromCat(cat, slot_shape="slot_Shape"): 

142 """Calculate the ellipticity of the Shapes in a catalog from the 2nd moments. 

143 Parameters 

144 ---------- 

145 cat : `lsst.afw.table.BaseCatalog` 

146 A catalog with 'slot_Shape' defined and '_xx', '_xy', '_yy' 

147 entries for the target of 'slot_Shape'. 

148 E.g., 'slot_shape' defined as 'base_SdssShape' 

149 And 'base_SdssShape_xx', 'base_SdssShape_xy', 'base_SdssShape_yy' defined. 

150 slot_shape : str, optional 

151 Specify what slot shape requested. Intended use is to get the PSF shape 

152 estimates by specifying 'slot_shape=slot_PsfShape' 

153 instead of the default 'slot_shape=slot_Shape'. 

154 Returns 

155 ------- 

156 e, e1, e2 : complex, float, float 

157 Complex ellipticity, real part, imaginary part 

158 """ 

159 i_xx, i_xy, i_yy = ( 

160 cat.get(slot_shape + "_xx"), 

161 cat.get(slot_shape + "_xy"), 

162 cat.get(slot_shape + "_yy"), 

163 ) 

164 return ellipticity(i_xx, i_xy, i_yy) 

165 

166 

167def ellipticity(i_xx, i_xy, i_yy): 

168 """Calculate ellipticity from second moments. 

169 Parameters 

170 ---------- 

171 i_xx : float or `numpy.array` 

172 i_xy : float or `numpy.array` 

173 i_yy : float or `numpy.array` 

174 Returns 

175 ------- 

176 e, e1, e2 : (float, float, float) or (numpy.array, numpy.array, numpy.array) 

177 Complex ellipticity, real component, imaginary component 

178 """ 

179 e = (i_xx - i_yy + 2j * i_xy) / (i_xx + i_yy) 

180 e1 = np.real(e) 

181 e2 = np.imag(e) 

182 return e, e1, e2 

183 

184 

185def makeMatchedPhotom(dataIds, catalogs, photoCalibs): 

186 # inputs: dataIds, catalogs, photoCalibs 

187 

188 # Match all input bands: 

189 bands = list(set([f["band"] for f in dataIds])) 

190 

191 # Should probably add an "assert" that requires bands>1... 

192 

193 empty_cat = catalogs[0].copy() 

194 empty_cat.clear() 

195 

196 cat_dict = {} 

197 mags_dict = {} 

198 magerrs_dict = {} 

199 for band in bands: 

200 cat_dict[band] = empty_cat.copy() 

201 mags_dict[band] = [] 

202 magerrs_dict[band] = [] 

203 

204 for i in range(len(catalogs)): 

205 for band in bands: 

206 if dataIds[i]["band"] in band: 

207 cat_dict[band].extend(catalogs[i].copy(deep=True)) 

208 mags = photoCalibs[i].instFluxToMagnitude(catalogs[i], "base_PsfFlux") 

209 mags_dict[band] = np.append(mags_dict[band], mags[:, 0]) 

210 magerrs_dict[band] = np.append(magerrs_dict[band], mags[:, 1]) 

211 

212 for band in bands: 

213 cat_tmp = cat_dict[band] 

214 if cat_tmp: 

215 if not cat_tmp.isContiguous(): 

216 cat_tmp = cat_tmp.copy(deep=True) 

217 cat_tmp_final = cat_tmp.asAstropy() 

218 cat_tmp_final["base_PsfFlux_mag"] = mags_dict[band] 

219 cat_tmp_final["base_PsfFlux_magErr"] = magerrs_dict[band] 

220 # Put the bandpass name in the column names: 

221 for c in cat_tmp_final.colnames: 

222 if c not in "id": 

223 cat_tmp_final[c].name = c + "_" + str(band) 

224 # Write the new catalog to the dict of catalogs: 

225 cat_dict[band] = cat_tmp_final 

226 

227 cat_combined = join(cat_dict[bands[1]], cat_dict[bands[0]], keys="id") 

228 if len(bands) > 2: 

229 for i in range(2, len(bands)): 

230 cat_combined = join(cat_combined, cat_dict[bands[i]], keys="id") 

231 

232 qual_cuts = ( 

233 (cat_combined["base_ClassificationExtendedness_value_g"] < 0.5) 

234 & (cat_combined["base_PixelFlags_flag_saturated_g"] is False) 

235 & (cat_combined["base_PixelFlags_flag_cr_g"] is False) 

236 & (cat_combined["base_PixelFlags_flag_bad_g"] is False) 

237 & (cat_combined["base_PixelFlags_flag_edge_g"] is False) 

238 & (cat_combined["base_ClassificationExtendedness_value_r"] < 0.5) 

239 & (cat_combined["base_PixelFlags_flag_saturated_r"] is False) 

240 & (cat_combined["base_PixelFlags_flag_cr_r"] is False) 

241 & (cat_combined["base_PixelFlags_flag_bad_r"] is False) 

242 & (cat_combined["base_PixelFlags_flag_edge_r"] is False) 

243 & (cat_combined["base_ClassificationExtendedness_value_i"] < 0.5) 

244 & (cat_combined["base_PixelFlags_flag_saturated_i"] is False) 

245 & (cat_combined["base_PixelFlags_flag_cr_i"] is False) 

246 & (cat_combined["base_PixelFlags_flag_bad_i"] is False) 

247 & (cat_combined["base_PixelFlags_flag_edge_i"] is False) 

248 ) # noqa: E712 

249 

250 # Return the astropy table of matched catalogs: 

251 return cat_combined[qual_cuts] 

252 

253 

254def mergeCatalogs( 

255 catalogs, 

256 photoCalibs=None, 

257 astromCalibs=None, 

258 models=["slot_PsfFlux"], 

259 applyExternalWcs=False, 

260): 

261 """Merge catalogs and optionally apply photometric and astrometric calibrations. 

262 """ 

263 

264 schema = catalogs[0].schema 

265 mapper = SchemaMapper(schema) 

266 mapper.addMinimalSchema(schema) 

267 aliasMap = schema.getAliasMap() 

268 for model in models: 

269 modelName = aliasMap[model] if model in aliasMap.keys() else model 

270 mapper.addOutputField( 

271 Field[float](f"{modelName}_mag", f"{modelName} magnitude") 

272 ) 

273 mapper.addOutputField( 

274 Field[float](f"{modelName}_magErr", f"{modelName} magnitude uncertainty") 

275 ) 

276 newSchema = mapper.getOutputSchema() 

277 newSchema.setAliasMap(schema.getAliasMap()) 

278 

279 size = sum([len(cat) for cat in catalogs]) 

280 catalog = SourceCatalog(newSchema) 

281 catalog.reserve(size) 

282 

283 for ii in range(0, len(catalogs)): 

284 cat = catalogs[ii] 

285 

286 # Create temporary catalog. Is this step needed? 

287 tempCat = SourceCatalog(SourceCatalog(newSchema).table) 

288 tempCat.extend(cat, mapper=mapper) 

289 

290 if applyExternalWcs and astromCalibs is not None: 

291 wcs = astromCalibs[ii] 

292 updateSourceCoords(wcs, tempCat) 

293 

294 if photoCalibs is not None: 

295 photoCalib = photoCalibs[ii] 

296 for model in models: 

297 modelName = aliasMap[model] if model in aliasMap.keys() else model 

298 photoCalib.instFluxToMagnitude(tempCat, modelName, modelName) 

299 

300 catalog.extend(tempCat) 

301 

302 return catalog