Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+05816baf74,g1ec0fe41b4+f536777771,g1fd858c14a+a9301854fb,g35bb328faa+fcb1d3bbc8,g4af146b050+a5c07d5b1d,g4d2262a081+6e5fcc2a4e,g53246c7159+fcb1d3bbc8,g56a49b3a55+9c12191793,g5a012ec0e7+3632fc3ff3,g60b5630c4e+ded28b650d,g67b6fd64d1+ed4b5058f4,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g8352419a5c+fcb1d3bbc8,g87b7deb4dc+7b42cf88bf,g8852436030+e5453db6e6,g89139ef638+ed4b5058f4,g8e3bb8577d+d38d73bdbd,g9125e01d80+fcb1d3bbc8,g94187f82dc+ded28b650d,g989de1cb63+ed4b5058f4,g9d31334357+ded28b650d,g9f33ca652e+50a8019d8c,gabe3b4be73+1e0a283bba,gabf8522325+fa80ff7197,gb1101e3267+d9fb1f8026,gb58c049af0+f03b321e39,gb665e3612d+2a0c9e9e84,gb89ab40317+ed4b5058f4,gcf25f946ba+e5453db6e6,gd6cbbdb0b4+bb83cc51f8,gdd1046aedd+ded28b650d,gde0f65d7ad+941d412827,ge278dab8ac+d65b3c2b70,ge410e46f29+ed4b5058f4,gf23fb2af72+b7cae620c0,gf5e32f922b+fcb1d3bbc8,gf67bdafdda+ed4b5058f4,w.2025.16
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
loadReferenceCatalog.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
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"""Load a full reference catalog in numpy/table/dataframe format.
23
24This task will load multi-band reference objects, apply a reference selector,
25and apply color terms.
26"""
27
28__all__ = ['LoadReferenceCatalogConfig', 'LoadReferenceCatalogTask']
29
30import numpy as np
31from astropy import units
32
33import lsst.pex.config as pexConfig
34import lsst.pipe.base as pipeBase
35from lsst.meas.algorithms import ReferenceSourceSelectorTask
36from lsst.meas.algorithms import getRefFluxField
37from lsst.pipe.tasks.colorterms import ColortermLibrary
38from lsst.afw.image import abMagErrFromFluxErr
39from lsst.meas.algorithms import ReferenceObjectLoader, LoadReferenceObjectsConfig
40
41
42class LoadReferenceCatalogConfig(pexConfig.Config):
43 """Config for LoadReferenceCatalogTask"""
44 refObjLoader = pexConfig.ConfigField(
45 dtype=LoadReferenceObjectsConfig,
46 doc="Configuration for the reference object loader.",
47 )
48 doApplyColorTerms = pexConfig.Field(
49 doc=("Apply photometric color terms to reference stars? "
50 "Requires that colorterms be set to a ColorTermLibrary"),
51 dtype=bool,
52 default=True
53 )
54 colorterms = pexConfig.ConfigField(
55 doc="Library of photometric reference catalog name to color term dict.",
56 dtype=ColortermLibrary,
57 )
58 referenceSelector = pexConfig.ConfigurableField(
59 target=ReferenceSourceSelectorTask,
60 doc="Selection of reference sources",
61 )
62 doReferenceSelection = pexConfig.Field(
63 doc="Run the reference selector on the reference catalog?",
64 dtype=bool,
65 default=True
66 )
67
68 def validate(self):
69 super().validate()
70 if self.doApplyColorTerms and len(self.colorterms.data) == 0:
71 msg = "applyColorTerms=True requires the `colorterms` field be set to a ColortermLibrary."
72 raise pexConfig.FieldValidationError(LoadReferenceCatalogConfig.colorterms, self, msg)
73
74
75class LoadReferenceCatalogTask(pipeBase.Task):
76 """Load multi-band reference objects from a reference catalog.
77
78 Parameters
79 ----------
80 dataIds : iterable of `lsst.daf.butler.dataId`
81 An iterable object of dataIds which point to reference catalogs
82 in a Gen3 repository. Required for Gen3.
83 refCats : iterable of `lsst.daf.butler.DeferredDatasetHandle`
84 An iterable object of dataset refs for reference catalogs in
85 a Gen3 repository.
86 name : `str`
87 The name of the refcat that this object will load. This name is used
88 for applying colorterms, for example.
89
90 Raises
91 ------
92 RuntimeError if dataIds or refCats is None.
93 """
94 ConfigClass = LoadReferenceCatalogConfig
95 _DefaultName = "loadReferenceCatalog"
96
97 def __init__(self, *, dataIds, refCats, name, **kwargs):
98 pipeBase.Task.__init__(self, **kwargs)
99 refConfig = self.config.refObjLoader
100 self.refObjLoader = ReferenceObjectLoader(dataIds=dataIds,
101 refCats=refCats,
102 name=name,
103 config=refConfig,
104 log=self.log)
105
106 if self.config.doReferenceSelection:
107 self.makeSubtask('referenceSelector')
108 self._fluxFilters = None
109 self._fluxFields = None
111
112 def getPixelBoxCatalog(self, bbox, wcs, filterList, epoch=None,
113 bboxToSpherePadding=None):
114 """Get a multi-band reference catalog by specifying a bounding box and WCS.
115
116 The catalog will be in `numpy.ndarray`, with positions proper-motion
117 corrected to "epoch" (if specified, and if the reference catalog has
118 proper motions); sources cut on a reference selector (if
119 "config.doReferenceSelection = True"); and color-terms applied (if
120 "config.doApplyColorTerms = True").
121
122 The format of the reference catalog will be of the format:
123
124 dtype = [('ra', 'np.float64'),
125 ('dec', 'np.float64'),
126 ('refMag', 'np.float32', (len(filterList), )),
127 ('refMagErr', 'np.float32', (len(filterList), ))]
128
129 Reference magnitudes (AB) and errors will be NaN for non-detections
130 in a given band.
131
132 Parameters
133 ----------
134 bbox : `lsst.geom.Box2I`
135 Box which bounds a region in pixel space.
136 wcs : `lsst.afw.geom.SkyWcs`
137 Wcs object defining the pixel to sky (and reverse) transform for
138 the supplied bbox.
139 filterList : `List` [ `str` ]
140 List of camera physicalFilter names to retrieve magnitudes.
141 epoch : `astropy.time.Time`, optional
142 Epoch to which to correct proper motion and parallax
143 (if available), or `None` to not apply such corrections.
144 bboxToSpherePadding : `int`, optional
145 Padding to account for translating a set of corners into a
146 spherical (convex) boundary that is certain to encompass the
147 entire area covered by the bbox.
148
149 Returns
150 -------
151 refCat : `numpy.ndarray`
152 Reference catalog.
153 """
154 # Check if we have previously cached values for the fluxFields
155 if self._fluxFilters is None or self._fluxFilters != filterList:
156 center = wcs.pixelToSky(bbox.getCenter())
157 self._determineFluxFields(center, filterList)
158
159 skyBox = self.refObjLoader.loadPixelBox(bbox, wcs, self._referenceFilter,
160 epoch=epoch,
161 bboxToSpherePadding=bboxToSpherePadding)
162
163 if not skyBox.refCat.isContiguous():
164 refCat = skyBox.refCat.copy(deep=True)
165 else:
166 refCat = skyBox.refCat
167
168 return self._formatCatalog(refCat, filterList)
169
170 def getSkyCircleCatalog(self, center, radius, filterList, epoch=None,
171 catalogFormat='numpy'):
172 """Get a multi-band reference catalog by specifying a center and radius.
173
174 The catalog will be in `numpy.ndarray`, with positions proper-motion
175 corrected to "epoch" (if specified, and if the reference catalog has
176 proper motions); sources cut on a reference selector (if
177 "config.doReferenceSelection = True"); and color-terms applied (if
178 "config.doApplyColorTerms = True").
179
180 The format of the reference catalog will be of the format:
181
182 dtype = [('ra', 'np.float64'),
183 ('dec', 'np.float64'),
184 ('refMag', 'np.float32', (len(filterList), )),
185 ('refMagErr', 'np.float32', (len(filterList), ))]
186
187 Reference magnitudes (AB) and errors will be NaN for non-detections
188 in a given band.
189
190 Parameters
191 ----------
192 center : `lsst.geom.SpherePoint`
193 Point defining the center of the circular region.
194 radius : `lsst.geom.Angle`
195 Defines the angular radius of the circular region.
196 filterList : `List` [ `str` ]
197 List of camera physicalFilter names to retrieve magnitudes.
198 epoch : `astropy.time.Time`, optional
199 Epoch to which to correct proper motion and parallax
200 (if available), or `None` to not apply such corrections.
201
202 Returns
203 -------
204 refCat : `numpy.ndarray`
205 Reference catalog.
206 """
207 # Check if we have previously cached values for the fluxFields
208 if self._fluxFilters is None or self._fluxFilters != filterList:
209 self._determineFluxFields(center, filterList)
210
211 skyCircle = self.refObjLoader.loadSkyCircle(center, radius,
212 self._referenceFilter,
213 epoch=epoch)
214
215 if not skyCircle.refCat.isContiguous():
216 refCat = skyCircle.refCat.copy(deep=True)
217 else:
218 refCat = skyCircle.refCat
219
220 return self._formatCatalog(refCat, filterList)
221
222 def _formatCatalog(self, refCat, filterList):
223 """Format a reference afw table into the final format.
224
225 This method applies reference selections and color terms as specified
226 by the config.
227
228 Parameters
229 ----------
230 refCat : `lsst.afw.table.SourceCatalog`
231 Reference catalog in afw format.
232 filterList : `list` [`str`]
233 List of camera physicalFilter names to apply color terms.
234
235 Returns
236 -------
237 refCat : `numpy.ndarray`
238 Reference catalog.
239 """
240 if self.config.doReferenceSelection:
241 goodSources = self.referenceSelector.selectSources(refCat)
242 selected = goodSources.selected
243 else:
244 selected = np.ones(len(refCat), dtype=bool)
245
246 npRefCat = np.zeros(np.sum(selected), dtype=[('ra', 'f8'),
247 ('dec', 'f8'),
248 ('refMag', 'f4', (len(filterList), )),
249 ('refMagErr', 'f4', (len(filterList), ))])
250
251 if npRefCat.size == 0:
252 # Return an empty catalog if we don't have any selected sources.
253 return npRefCat
254
255 # Natively "coord_ra" and "coord_dec" are stored in radians.
256 # Doing this as an array rather than by row with the coord access is
257 # approximately 600x faster.
258 npRefCat['ra'] = np.rad2deg(refCat['coord_ra'][selected])
259 npRefCat['dec'] = np.rad2deg(refCat['coord_dec'][selected])
260
261 # Default (unset) values are np.nan
262 npRefCat['refMag'][:, :] = np.nan
263 npRefCat['refMagErr'][:, :] = np.nan
264
265 if self.config.doApplyColorTerms:
266 refCatName = self.refObjLoader.name
267
268 for i, (filterName, fluxField) in enumerate(zip(self._fluxFilters, self._fluxFields)):
269 if fluxField is None:
270 # There is no matching reference band.
271 # This will leave the column filled with np.nans
272 continue
273 self.log.debug("Applying color terms for filterName='%s'", filterName)
274
275 colorterm = self.config.colorterms.getColorterm(filterName, refCatName, doRaise=True)
276
277 refMag, refMagErr = colorterm.getCorrectedMagnitudes(refCat)
278
279 # nan_to_num below replaces nans with 99, and this ensures
280 # that we select magnitudes that both filter out nans and are
281 # not very large (corresponding to very small fluxes), as "99"
282 # is a common sentinel for illegal magnitudes in reference catalogs.
283 good, = np.where((np.nan_to_num(refMag[selected], nan=99.0) < 90.0)
284 & (np.nan_to_num(refMagErr[selected], nan=99.0) < 90.0)
285 & (np.nan_to_num(refMagErr[selected]) > 0.0))
286
287 npRefCat['refMag'][good, i] = refMag[selected][good]
288 npRefCat['refMagErr'][good, i] = refMagErr[selected][good]
289 else:
290 # No color terms to apply
291 for i, (filterName, fluxField) in enumerate(zip(self._fluxFilters, self._fluxFields)):
292 # nan_to_num below replaces nans with zeros, and this ensures that
293 # we select fluxes that both filter out nans and are positive.
294 good, = np.where((np.nan_to_num(refCat[fluxField][selected]) > 0.0)
295 & (np.nan_to_num(refCat[fluxField+'Err'][selected]) > 0.0))
296 refMag = (refCat[fluxField][selected][good]*units.nJy).to_value(units.ABmag)
297 refMagErr = abMagErrFromFluxErr(refCat[fluxField+'Err'][selected][good],
298 refCat[fluxField][selected][good])
299 npRefCat['refMag'][good, i] = refMag
300 npRefCat['refMagErr'][good, i] = refMagErr
301
302 return npRefCat
303
304 def _determineFluxFields(self, center, filterList):
305 """Determine the flux field names for a reference catalog.
306
307 This method sets self._fluxFields, self._referenceFilter.
308
309 Parameters
310 ----------
311 center : `lsst.geom.SpherePoint`
312 The center around which to load test sources.
313 filterList : `list` [`str`]
314 List of camera physicalFilter names.
315 """
316 # Search for a good filter to use to load the reference catalog
317 # via the refObjLoader task which requires a valid filterName
318 foundReferenceFilter = False
319
320 # Store the original config
321 _config = self.refObjLoader.config
322
323 configTemp = LoadReferenceObjectsConfig()
324 configIntersection = {k: getattr(self.refObjLoader.config, k)
325 for k, v in self.refObjLoader.config.toDict().items()
326 if (k in configTemp.keys() and k != "connections")}
327 # We must turn off the proper motion checking to find the refFilter.
328 configIntersection['requireProperMotion'] = False
329 configTemp.update(**configIntersection)
330
331 self.refObjLoader.config = configTemp
332
333 for filterName in filterList:
334 if self.config.refObjLoader.anyFilterMapsToThis is not None:
335 refFilterName = self.config.refObjLoader.anyFilterMapsToThis
336 else:
337 refFilterName = self.config.refObjLoader.filterMap.get(filterName)
338 if refFilterName is None:
339 continue
340 try:
341 results = self.refObjLoader.loadSchema(refFilterName)
342 foundReferenceFilter = True
343 self._referenceFilter = refFilterName
344 break
345 except RuntimeError as err:
346 # This just means that the filterName wasn't listed
347 # in the reference catalog. This is okay.
348 if 'not find flux' in err.args[0]:
349 # The filterName wasn't listed in the reference catalog.
350 # This is not a fatal failure (yet)
351 pass
352 else:
353 raise err
354
355 self.refObjLoader.config = _config
356
357 if not foundReferenceFilter:
358 raise RuntimeError("Could not find any valid flux field(s) %s" %
359 (", ".join(filterList)))
360
361 # Record self._fluxFilters for checks on subsequent calls
362 self._fluxFilters = filterList
363
364 # Retrieve all the fluxField names
365 self._fluxFields = []
366 for filterName in filterList:
367 fluxField = None
368
369 if self.config.refObjLoader.anyFilterMapsToThis is not None:
370 refFilterName = self.config.refObjLoader.anyFilterMapsToThis
371 else:
372 refFilterName = self.config.refObjLoader.filterMap.get(filterName)
373
374 if refFilterName is not None:
375 try:
376 fluxField = getRefFluxField(results.schema, filterName=refFilterName)
377 except RuntimeError:
378 # This flux field isn't available. Set to None
379 fluxField = None
380
381 if fluxField is None:
382 self.log.warning('No reference flux field for camera filter %s', filterName)
383
384 self._fluxFields.append(fluxField)
getPixelBoxCatalog(self, bbox, wcs, filterList, epoch=None, bboxToSpherePadding=None)
getSkyCircleCatalog(self, center, radius, filterList, epoch=None, catalogFormat='numpy')