Coverage for python/lsst/fgcmcal/fgcmLoadReferenceCatalog.py : 18%

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
1# See COPYRIGHT file at the top of the source tree.
2#
3# This file is part of fgcmcal.
4#
5# Developed for the LSST Data Management System.
6# This product includes software developed by the LSST Project
7# (https://www.lsst.org).
8# See the COPYRIGHT file at the top-level directory of this distribution
9# for details of code ownership.
10#
11# This program is free software: you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation, either version 3 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program. If not, see <https://www.gnu.org/licenses/>.
23"""Load reference catalog objects for input to FGCM.
25This task will load multi-band reference objects and apply color terms (if
26configured). This wrapper around LoadReferenceObjects task also allows loading
27by healpix pixel (the native pixelization of fgcm), and is self-contained so
28the task can be called by third-party code.
29"""
31import numpy as np
32import healpy as hp
33from astropy import units
35import lsst.pex.config as pexConfig
36import lsst.pipe.base as pipeBase
37from lsst.meas.algorithms import LoadIndexedReferenceObjectsTask, ReferenceSourceSelectorTask
38from lsst.meas.algorithms import getRefFluxField
39from lsst.pipe.tasks.colorterms import ColortermLibrary
40from lsst.afw.image import abMagErrFromFluxErr
41from lsst.meas.algorithms import ReferenceObjectLoader
43import lsst.geom
45__all__ = ['FgcmLoadReferenceCatalogConfig', 'FgcmLoadReferenceCatalogTask']
48class FgcmLoadReferenceCatalogConfig(pexConfig.Config):
49 """Config for FgcmLoadReferenceCatalogTask"""
51 refObjLoader = pexConfig.ConfigurableField(
52 target=LoadIndexedReferenceObjectsTask,
53 doc="Reference object loader for photometry",
54 )
55 refFilterMap = pexConfig.DictField(
56 doc="Mapping from camera 'filterName' to reference filter name.",
57 keytype=str,
58 itemtype=str,
59 default={},
60 deprecated=("This field is no longer used, and has been deprecated by "
61 "DM-28088. It will be removed after v22. Use "
62 "filterMap instead.")
63 )
64 filterMap = pexConfig.DictField(
65 doc="Mapping from physicalFilter label to reference filter name.",
66 keytype=str,
67 itemtype=str,
68 default={},
69 )
70 applyColorTerms = pexConfig.Field(
71 doc=("Apply photometric color terms to reference stars?"
72 "Requires that colorterms be set to a ColorTermLibrary"),
73 dtype=bool,
74 default=True
75 )
76 colorterms = pexConfig.ConfigField(
77 doc="Library of photometric reference catalog name to color term dict.",
78 dtype=ColortermLibrary,
79 )
80 referenceSelector = pexConfig.ConfigurableField(
81 target=ReferenceSourceSelectorTask,
82 doc="Selection of reference sources",
83 )
85 def validate(self):
86 super().validate()
87 if not self.filterMap:
88 msg = 'Must set filterMap'
89 raise pexConfig.FieldValidationError(FgcmLoadReferenceCatalogConfig.filterMap, self, msg)
90 if self.applyColorTerms and len(self.colorterms.data) == 0:
91 msg = "applyColorTerms=True requires the `colorterms` field be set to a ColortermLibrary."
92 raise pexConfig.FieldValidationError(FgcmLoadReferenceCatalogConfig.colorterms, self, msg)
95class FgcmLoadReferenceCatalogTask(pipeBase.Task):
96 """
97 Load multi-band reference objects from a reference catalog.
99 Parameters
100 ----------
101 butler: `lsst.daf.persistence.Butler`
102 Data butler for reading catalogs
103 """
104 ConfigClass = FgcmLoadReferenceCatalogConfig
105 _DefaultName = 'fgcmLoadReferenceCatalog'
107 def __init__(self, butler=None, refObjLoader=None, **kwargs):
108 """Construct an FgcmLoadReferenceCatalogTask
110 Parameters
111 ----------
112 butler: `lsst.daf.persistence.Buter`
113 Data butler for reading catalogs.
114 """
115 pipeBase.Task.__init__(self, **kwargs)
116 if refObjLoader is None and butler is not None:
117 self.makeSubtask('refObjLoader', butler=butler)
118 else:
119 self.refObjLoader = refObjLoader
121 self.makeSubtask('referenceSelector')
122 self._fluxFilters = None
123 self._fluxFields = None
124 self._referenceFilter = None
126 def getFgcmReferenceStarsHealpix(self, nside, pixel, filterList, nest=False):
127 """
128 Get a reference catalog that overlaps a healpix pixel, using multiple
129 filters. In addition, apply colorterms if available.
131 Return format is a numpy recarray for use with fgcm, with the format:
133 dtype = ([('ra', `np.float64`),
134 ('dec', `np.float64`),
135 ('refMag', `np.float32`, len(filterList)),
136 ('refMagErr', `np.float32`, len(filterList)])
138 Reference magnitudes (AB) will be 99 for non-detections.
140 Parameters
141 ----------
142 nside: `int`
143 Healpix nside of pixel to load
144 pixel: `int`
145 Healpix pixel of pixel to load
146 filterList: `list`
147 list of `str` of camera filter names.
148 nest: `bool`, optional
149 Is the pixel in nest format? Default is False.
151 Returns
152 -------
153 fgcmRefCat: `np.recarray`
154 """
156 # Determine the size of the sky circle to load
157 theta, phi = hp.pix2ang(nside, pixel, nest=nest)
158 center = lsst.geom.SpherePoint(phi * lsst.geom.radians, (np.pi/2. - theta) * lsst.geom.radians)
160 corners = hp.boundaries(nside, pixel, step=1, nest=nest)
161 theta_phi = hp.vec2ang(np.transpose(corners))
163 radius = 0.0 * lsst.geom.radians
164 for ctheta, cphi in zip(*theta_phi):
165 rad = center.separation(lsst.geom.SpherePoint(cphi * lsst.geom.radians,
166 (np.pi/2. - ctheta) * lsst.geom.radians))
167 if (rad > radius):
168 radius = rad
170 # Load the fgcm-format reference catalog
171 fgcmRefCat = self.getFgcmReferenceStarsSkyCircle(center.getRa().asDegrees(),
172 center.getDec().asDegrees(),
173 radius.asDegrees(),
174 filterList)
175 catPix = hp.ang2pix(nside, np.radians(90.0 - fgcmRefCat['dec']),
176 np.radians(fgcmRefCat['ra']), nest=nest)
178 inPix, = np.where(catPix == pixel)
180 return fgcmRefCat[inPix]
182 def getFgcmReferenceStarsSkyCircle(self, ra, dec, radius, filterList):
183 """
184 Get a reference catalog that overlaps a circular sky region, using
185 multiple filters. In addition, apply colorterms if available.
187 Return format is a numpy recarray for use with fgcm.
189 dtype = ([('ra', `np.float64`),
190 ('dec', `np.float64`),
191 ('refMag', `np.float32`, len(filterList)),
192 ('refMagErr', `np.float32`, len(filterList)])
194 Reference magnitudes (AB) will be 99 for non-detections.
196 Parameters
197 ----------
198 ra: `float`
199 ICRS right ascension, degrees.
200 dec: `float`
201 ICRS declination, degrees.
202 radius: `float`
203 Radius to search, degrees.
204 filterList: `list`
205 list of `str` of camera filter names.
207 Returns
208 -------
209 fgcmRefCat: `np.recarray`
210 """
212 center = lsst.geom.SpherePoint(ra * lsst.geom.degrees, dec * lsst.geom.degrees)
214 # Check if we haev previously cached values for the fluxFields
215 if self._fluxFilters is None or self._fluxFilters != filterList:
216 self._determine_flux_fields(center, filterList)
218 skyCircle = self.refObjLoader.loadSkyCircle(center,
219 radius * lsst.geom.degrees,
220 self._referenceFilter)
222 if not skyCircle.refCat.isContiguous():
223 refCat = skyCircle.refCat.copy(deep=True)
224 else:
225 refCat = skyCircle.refCat
227 # Select on raw (uncorrected) catalog, where the errors should make more sense
228 goodSources = self.referenceSelector.selectSources(refCat)
229 selected = goodSources.selected
231 fgcmRefCat = np.zeros(np.sum(selected), dtype=[('ra', 'f8'),
232 ('dec', 'f8'),
233 ('refMag', 'f4', len(filterList)),
234 ('refMagErr', 'f4', len(filterList))])
235 if fgcmRefCat.size == 0:
236 # Return an empty catalog if we don't have any selected sources
237 return fgcmRefCat
239 # The ra/dec native Angle format is radians
240 # We determine the conversion from the native units (typically
241 # radians) to degrees for the first observation. This allows us
242 # to treate ra/dec as numpy arrays rather than Angles, which would
243 # be approximately 600x slower.
245 conv = refCat[0]['coord_ra'].asDegrees() / float(refCat[0]['coord_ra'])
246 fgcmRefCat['ra'] = refCat['coord_ra'][selected] * conv
247 fgcmRefCat['dec'] = refCat['coord_dec'][selected] * conv
249 # Default (unset) values are 99.0
250 fgcmRefCat['refMag'][:, :] = 99.0
251 fgcmRefCat['refMagErr'][:, :] = 99.0
253 if self.config.applyColorTerms:
254 if isinstance(self.refObjLoader, ReferenceObjectLoader):
255 # Gen3
256 refCatName = self.refObjLoader.config.value.ref_dataset_name
257 else:
258 # Gen2
259 refCatName = self.refObjLoader.ref_dataset_name
261 for i, (filterName, fluxField) in enumerate(zip(self._fluxFilters, self._fluxFields)):
262 if fluxField is None:
263 continue
265 self.log.debug("Applying color terms for filtername=%r" % (filterName))
267 colorterm = self.config.colorterms.getColorterm(
268 filterName=filterName, photoCatName=refCatName, doRaise=True)
270 refMag, refMagErr = colorterm.getCorrectedMagnitudes(refCat, filterName)
272 # nan_to_num replaces nans with zeros, and this ensures that we select
273 # magnitudes that both filter out nans and are not very large (corresponding
274 # to very small fluxes), as "99" is a common sentinel for illegal magnitudes.
276 good, = np.where((np.nan_to_num(refMag[selected]) < 90.0)
277 & (np.nan_to_num(refMagErr[selected]) < 90.0)
278 & (np.nan_to_num(refMagErr[selected]) > 0.0))
280 fgcmRefCat['refMag'][good, i] = refMag[selected][good]
281 fgcmRefCat['refMagErr'][good, i] = refMagErr[selected][good]
283 else:
284 # No colorterms
286 for i, (filterName, fluxField) in enumerate(zip(self._fluxFilters, self._fluxFields)):
287 # nan_to_num replaces nans with zeros, and this ensures that we select
288 # fluxes that both filter out nans and are positive.
289 good, = np.where((np.nan_to_num(refCat[fluxField][selected]) > 0.0)
290 & (np.nan_to_num(refCat[fluxField+'Err'][selected]) > 0.0))
291 refMag = (refCat[fluxField][selected][good] * units.nJy).to_value(units.ABmag)
292 refMagErr = abMagErrFromFluxErr(refCat[fluxField+'Err'][selected][good],
293 refCat[fluxField][selected][good])
294 fgcmRefCat['refMag'][good, i] = refMag
295 fgcmRefCat['refMagErr'][good, i] = refMagErr
297 return fgcmRefCat
299 def _determine_flux_fields(self, center, filterList):
300 """
301 Determine the flux field names for a reference catalog.
303 Will set self._fluxFields, self._referenceFilter.
305 Parameters
306 ----------
307 center: `lsst.geom.SpherePoint`
308 The center around which to load test sources.
309 filterList: `list`
310 list of `str` of camera filter names.
311 """
313 # Record self._fluxFilters for checks on subsequent calls
314 self._fluxFilters = filterList
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 for filterName in filterList:
320 refFilterName = self.config.filterMap.get(filterName)
321 if refFilterName is None:
322 continue
324 try:
325 results = self.refObjLoader.loadSkyCircle(center,
326 0.05 * lsst.geom.degrees,
327 refFilterName)
328 foundReferenceFilter = True
329 self._referenceFilter = refFilterName
330 break
331 except RuntimeError:
332 # This just means that the filterName wasn't listed
333 # in the reference catalog. This is okay.
334 pass
336 if not foundReferenceFilter:
337 raise RuntimeError("Could not find any valid flux field(s) %s" %
338 (", ".join(filterList)))
340 # Retrieve all the fluxField names
341 self._fluxFields = []
342 for filterName in filterList:
343 fluxField = None
345 refFilterName = self.config.filterMap.get(filterName)
347 if refFilterName is not None:
348 try:
349 fluxField = getRefFluxField(results.refCat.schema, filterName=refFilterName)
350 except RuntimeError:
351 # This flux field isn't available. Set to None
352 fluxField = None
354 if fluxField is None:
355 self.log.warn(f'No reference flux field for camera filter {filterName}')
357 self._fluxFields.append(fluxField)