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 (
2 SchemaMapper,
3 Field,
4 MultiMatch,
5 SimpleRecord,
6 SourceCatalog,
7 updateSourceCoords,
8)
10import numpy as np
11from astropy.table import join, Table
13__all__ = (
14 "matchCatalogs",
15 "ellipticityFromCat",
16 "ellipticity",
17 "makeMatchedPhotom",
18 "mergeCatalogs",
19)
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())
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 )
60 # create the new extended source catalog
61 srcVis = SourceCatalog(newSchema)
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 }
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"))
85 for ind in sortinds:
86 oldSrc = inputs[ind]
87 photoCalib = photoCalibs[ind]
88 wcs = astromCalibs[ind]
89 dataId = dataIds[ind]
91 if logger:
92 logger.debug(
93 "%d sources in ccd %s visit %s",
94 len(oldSrc),
95 dataId["detector"],
96 dataId["visit"],
97 )
99 # create temporary catalog
100 tmpCat = SourceCatalog(SourceCatalog(newSchema).table)
101 tmpCat.extend(oldSrc, mapper=mapper)
103 filtnum = filter_dict[dataId["band"]]
104 tmpCat["filt"] = np.repeat(filtnum, len(oldSrc))
106 tmpCat["base_PsfFlux_snr"][:] = (
107 tmpCat["base_PsfFlux_instFlux"] / tmpCat["base_PsfFlux_instFluxErr"]
108 )
110 updateSourceCoords(wcs, tmpCat)
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")
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
125 srcVis.extend(tmpCat, False)
126 mmatch.add(catalog=tmpCat, dataId=dataId)
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()
132 # Create a mapping object that allows the matches to be manipulated
133 # as a mapping of object ID to catalog of sources.
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)
138 return srcVis, matchCat
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)
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
185def makeMatchedPhotom(dataIds, catalogs, photoCalibs):
186 # inputs: dataIds, catalogs, photoCalibs
188 # Match all input bands:
189 bands = list(set([f["band"] for f in dataIds]))
191 # Should probably add an "assert" that requires bands>1...
193 empty_cat = catalogs[0].copy()
194 empty_cat.clear()
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] = []
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])
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
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")
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
250 # Return the astropy table of matched catalogs:
251 return cat_combined[qual_cuts]
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 """
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())
279 size = sum([len(cat) for cat in catalogs])
280 catalog = SourceCatalog(newSchema)
281 catalog.reserve(size)
283 for ii in range(0, len(catalogs)):
284 cat = catalogs[ii]
286 # Create temporary catalog. Is this step needed?
287 tempCat = SourceCatalog(SourceCatalog(newSchema).table)
288 tempCat.extend(cat, mapper=mapper)
290 if applyExternalWcs and astromCalibs is not None:
291 wcs = astromCalibs[ii]
292 updateSourceCoords(wcs, tempCat)
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)
300 catalog.extend(tempCat)
302 return catalog