Coverage for python/lsst/faro/utils/matcher.py : 5%

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)
5import numpy as np
6from astropy.table import join, Table
8__all__ = ("match_catalogs", "ellipticity_from_cat", "ellipticity", "make_matched_photom",
9 "mergeCatalogs")
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())
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)
52 # create the new extended source catalog
53 srcVis = SourceCatalog(newSchema)
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}
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'))
65 for ind in sortinds:
66 oldSrc = inputs[ind]
67 photoCalib = photoCalibs[ind]
68 wcs = astromCalibs[ind]
69 vId = vIds[ind]
71 if logger:
72 logger.debug(f"{len(oldSrc)} sources in ccd {vId['detector']} visit {vId['visit']}")
74 # create temporary catalog
75 tmpCat = SourceCatalog(SourceCatalog(newSchema).table)
76 tmpCat.extend(oldSrc, mapper=mapper)
78 filtnum = filter_dict[vId['band']]
79 tmpCat['filt'] = np.repeat(filtnum, len(oldSrc))
81 tmpCat['base_PsfFlux_snr'][:] = tmpCat['base_PsfFlux_instFlux'] \
82 / tmpCat['base_PsfFlux_instFluxErr']
84 if apply_external_wcs and wcs is not None:
85 updateSourceCoords(wcs, tmpCat)
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")
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
99 srcVis.extend(tmpCat, False)
100 mmatch.add(catalog=tmpCat, dataId=vId)
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()
106 # Create a mapping object that allows the matches to be manipulated
107 # as a mapping of object ID to catalog of sources.
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)
112 return srcVis, matchCat
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)
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
155def make_matched_photom(vIds, catalogs, photo_calibs):
156 # inputs: vIds, catalogs, photo_calibs
158 # Match all input bands:
159 bands = list(set([f['band'] for f in vIds]))
161 # Should probably add an "assert" that requires bands>1...
163 empty_cat = catalogs[0].copy()
164 empty_cat.clear()
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] = []
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])
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
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')
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
218 # Return the astropy table of matched catalogs:
219 return(cat_combined[qual_cuts])
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 """
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())
241 size = sum([len(cat) for cat in catalogs])
242 catalog = SourceCatalog(newSchema)
243 catalog.reserve(size)
245 for ii in range(0, len(catalogs)):
246 cat = catalogs[ii]
248 # Create temporary catalog. Is this step needed?
249 tempCat = SourceCatalog(SourceCatalog(newSchema).table)
250 tempCat.extend(cat, mapper=mapper)
252 if applyExternalWcs and astromCalibs is not None:
253 wcs = astromCalibs[ii]
254 updateSourceCoords(wcs, tempCat)
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)
262 catalog.extend(tempCat)
264 return catalog