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 (SchemaMapper, Field, 

2 MultiMatch, SimpleRecord, 

3 SourceCatalog, updateSourceCoords) 

4 

5import numpy as np 

6from astropy.table import join, Table 

7 

8__all__ = ("match_catalogs", "ellipticity_from_cat", "ellipticity", "make_matched_photom", 

9 "mergeCatalogs") 

10 

11 

12def match_catalogs(inputs, photoCalibs, astromCalibs, vIds, matchRadius, 

13 apply_external_wcs=False, logger=None): 

14 schema = inputs[0].schema 

15 mapper = SchemaMapper(schema) 

16 mapper.addMinimalSchema(schema) 

17 mapper.addOutputField(Field[float]('base_PsfFlux_snr', 

18 'PSF flux SNR')) 

19 mapper.addOutputField(Field[float]('base_PsfFlux_mag', 

20 'PSF magnitude')) 

21 mapper.addOutputField(Field[float]('base_PsfFlux_magErr', 

22 'PSF magnitude uncertainty')) 

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

24 aliasMap = schema.getAliasMap() 

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

26 modelName = aliasMap['slot_ModelFlux'] if 'slot_ModelFlux' in aliasMap.keys() else 'base_GaussianFlux' 

27 mapper.addOutputField(Field[float](f'{modelName}_mag', 

28 'Model magnitude')) 

29 mapper.addOutputField(Field[float](f'{modelName}_magErr', 

30 'Model magnitude uncertainty')) 

31 mapper.addOutputField(Field[float](f'{modelName}_snr', 

32 'Model flux snr')) 

33 mapper.addOutputField(Field[float]('e1', 

34 'Source Ellipticity 1')) 

35 mapper.addOutputField(Field[float]('e2', 

36 'Source Ellipticity 1')) 

37 mapper.addOutputField(Field[float]('psf_e1', 

38 'PSF Ellipticity 1')) 

39 mapper.addOutputField(Field[float]('psf_e2', 

40 'PSF Ellipticity 1')) 

41 mapper.addOutputField(Field[np.int32]('filt', 

42 'filter code')) 

43 newSchema = mapper.getOutputSchema() 

44 newSchema.setAliasMap(schema.getAliasMap()) 

45 

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

47 mmatch = MultiMatch(newSchema, 

48 dataIdFormat={'visit': np.int32, 'detector': np.int32}, 

49 radius=matchRadius, 

50 RecordClass=SimpleRecord) 

51 

52 # create the new extended source catalog 

53 srcVis = SourceCatalog(newSchema) 

54 

55 filter_dict = {'u': 1, 'g': 2, 'r': 3, 'i': 4, 'z': 5, 'y': 6, 

56 'HSC-U': 1, 'HSC-G': 2, 'HSC-R': 3, 'HSC-I': 4, 'HSC-Z': 5, 'HSC-Y': 6} 

57 

58 # Sort by visit, detector, then filter 

59 vislist = [v['visit'] for v in vIds] 

60 ccdlist = [v['detector'] for v in vIds] 

61 filtlist = [v['band'] for v in vIds] 

62 tab_vids = Table([vislist, ccdlist, filtlist], names=['vis', 'ccd', 'filt']) 

63 sortinds = np.argsort(tab_vids, order=('vis', 'ccd', 'filt')) 

64 

65 for ind in sortinds: 

66 oldSrc = inputs[ind] 

67 photoCalib = photoCalibs[ind] 

68 wcs = astromCalibs[ind] 

69 vId = vIds[ind] 

70 

71 if logger: 

72 logger.debug(f"{len(oldSrc)} sources in ccd {vId['detector']} visit {vId['visit']}") 

73 

74 # create temporary catalog 

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

76 tmpCat.extend(oldSrc, mapper=mapper) 

77 

78 filtnum = filter_dict[vId['band']] 

79 tmpCat['filt'] = np.repeat(filtnum, len(oldSrc)) 

80 

81 tmpCat['base_PsfFlux_snr'][:] = tmpCat['base_PsfFlux_instFlux'] \ 

82 / tmpCat['base_PsfFlux_instFluxErr'] 

83 

84 if apply_external_wcs and wcs is not None: 

85 updateSourceCoords(wcs, tmpCat) 

86 

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

88 tmpCat['slot_ModelFlux_snr'][:] = (tmpCat['slot_ModelFlux_instFlux'] 

89 / tmpCat['slot_ModelFlux_instFluxErr']) 

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

91 

92 _, psf_e1, psf_e2 = ellipticity_from_cat(oldSrc, slot_shape='slot_PsfShape') 

93 _, star_e1, star_e2 = ellipticity_from_cat(oldSrc, slot_shape='slot_Shape') 

94 tmpCat['e1'][:] = star_e1 

95 tmpCat['e2'][:] = star_e2 

96 tmpCat['psf_e1'][:] = psf_e1 

97 tmpCat['psf_e2'][:] = psf_e2 

98 

99 srcVis.extend(tmpCat, False) 

100 mmatch.add(catalog=tmpCat, dataId=vId) 

101 

102 # Complete the match, returning a catalog that includes 

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

104 matchCat = mmatch.finish() 

105 

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

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

108 

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

110 # allMatches = GroupView.build(matchCat) 

111 

112 return srcVis, matchCat 

113 

114 

115def ellipticity_from_cat(cat, slot_shape='slot_Shape'): 

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

117 Parameters 

118 ---------- 

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

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

121 entries for the target of 'slot_Shape'. 

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

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

124 slot_shape : str, optional 

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

126 estimates by specifying 'slot_shape=slot_PsfShape' 

127 instead of the default 'slot_shape=slot_Shape'. 

128 Returns 

129 ------- 

130 e, e1, e2 : complex, float, float 

131 Complex ellipticity, real part, imaginary part 

132 """ 

133 i_xx, i_xy, i_yy = cat.get(slot_shape+'_xx'), cat.get(slot_shape+'_xy'), cat.get(slot_shape+'_yy') 

134 return ellipticity(i_xx, i_xy, i_yy) 

135 

136 

137def ellipticity(i_xx, i_xy, i_yy): 

138 """Calculate ellipticity from second moments. 

139 Parameters 

140 ---------- 

141 i_xx : float or `numpy.array` 

142 i_xy : float or `numpy.array` 

143 i_yy : float or `numpy.array` 

144 Returns 

145 ------- 

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

147 Complex ellipticity, real component, imaginary component 

148 """ 

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

150 e1 = np.real(e) 

151 e2 = np.imag(e) 

152 return e, e1, e2 

153 

154 

155def make_matched_photom(vIds, catalogs, photo_calibs): 

156 # inputs: vIds, catalogs, photo_calibs 

157 

158 # Match all input bands: 

159 bands = list(set([f['band'] for f in vIds])) 

160 

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

162 

163 empty_cat = catalogs[0].copy() 

164 empty_cat.clear() 

165 

166 cat_dict = {} 

167 mags_dict = {} 

168 magerrs_dict = {} 

169 for band in bands: 

170 cat_dict[band] = empty_cat.copy() 

171 mags_dict[band] = [] 

172 magerrs_dict[band] = [] 

173 

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

175 for band in bands: 

176 if (vIds[i]['band'] in band): 

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

178 mags = photo_calibs[i].instFluxToMagnitude(catalogs[i], 'base_PsfFlux') 

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

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

181 

182 for band in bands: 

183 cat_tmp = cat_dict[band] 

184 if cat_tmp: 

185 if not cat_tmp.isContiguous(): 

186 cat_tmp = cat_tmp.copy(deep=True) 

187 cat_tmp_final = cat_tmp.asAstropy() 

188 cat_tmp_final['base_PsfFlux_mag'] = mags_dict[band] 

189 cat_tmp_final['base_PsfFlux_magErr'] = magerrs_dict[band] 

190 # Put the bandpass name in the column names: 

191 for c in cat_tmp_final.colnames: 

192 if c not in 'id': 

193 cat_tmp_final[c].name = c+'_'+str(band) 

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

195 cat_dict[band] = cat_tmp_final 

196 

197 cat_combined = join(cat_dict[bands[1]], cat_dict[bands[0]], keys='id') 

198 if len(bands) > 2: 

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

200 cat_combined = join(cat_combined, cat_dict[bands[i]], keys='id') 

201 

202 qual_cuts = (cat_combined['base_ClassificationExtendedness_value_g'] < 0.5) &\ 

203 (cat_combined['base_PixelFlags_flag_saturated_g'] == False) &\ 

204 (cat_combined['base_PixelFlags_flag_cr_g'] == False) &\ 

205 (cat_combined['base_PixelFlags_flag_bad_g'] == False) &\ 

206 (cat_combined['base_PixelFlags_flag_edge_g'] == False) &\ 

207 (cat_combined['base_ClassificationExtendedness_value_r'] < 0.5) &\ 

208 (cat_combined['base_PixelFlags_flag_saturated_r'] == False) &\ 

209 (cat_combined['base_PixelFlags_flag_cr_r'] == False) &\ 

210 (cat_combined['base_PixelFlags_flag_bad_r'] == False) &\ 

211 (cat_combined['base_PixelFlags_flag_edge_r'] == False) &\ 

212 (cat_combined['base_ClassificationExtendedness_value_i'] < 0.5) &\ 

213 (cat_combined['base_PixelFlags_flag_saturated_i'] == False) &\ 

214 (cat_combined['base_PixelFlags_flag_cr_i'] == False) &\ 

215 (cat_combined['base_PixelFlags_flag_bad_i'] == False) &\ 

216 (cat_combined['base_PixelFlags_flag_edge_i'] == False) # noqa: E712 

217 

218 # Return the astropy table of matched catalogs: 

219 return(cat_combined[qual_cuts]) 

220 

221 

222def mergeCatalogs(catalogs, 

223 photoCalibs=None, astromCalibs=None, 

224 models=['slot_PsfFlux'], applyExternalWcs=False): 

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

226 """ 

227 

228 schema = catalogs[0].schema 

229 mapper = SchemaMapper(schema) 

230 mapper.addMinimalSchema(schema) 

231 aliasMap = schema.getAliasMap() 

232 for model in models: 

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

234 mapper.addOutputField(Field[float](f'{modelName}_mag', 

235 f'{modelName} magnitude')) 

236 mapper.addOutputField(Field[float](f'{modelName}_magErr', 

237 f'{modelName} magnitude uncertainty')) 

238 newSchema = mapper.getOutputSchema() 

239 newSchema.setAliasMap(schema.getAliasMap()) 

240 

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

242 catalog = SourceCatalog(newSchema) 

243 catalog.reserve(size) 

244 

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

246 cat = catalogs[ii] 

247 

248 # Create temporary catalog. Is this step needed? 

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

250 tempCat.extend(cat, mapper=mapper) 

251 

252 if applyExternalWcs and astromCalibs is not None: 

253 wcs = astromCalibs[ii] 

254 updateSourceCoords(wcs, tempCat) 

255 

256 if photoCalibs is not None: 

257 photoCalib = photoCalibs[ii] 

258 for model in models: 

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

260 photoCalib.instFluxToMagnitude(tempCat, modelName, modelName) 

261 

262 catalog.extend(tempCat) 

263 

264 return catalog