Coverage for python/lsst/fgcmcal/fgcmLoadReferenceCatalog.py: 19%
116 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 03:50 -0700
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 03:50 -0700
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
42import lsst.geom
44__all__ = ['FgcmLoadReferenceCatalogConfig', 'FgcmLoadReferenceCatalogTask']
47class FgcmLoadReferenceCatalogConfig(pexConfig.Config):
48 """Config for FgcmLoadReferenceCatalogTask"""
50 refObjLoader = pexConfig.ConfigurableField(
51 target=LoadIndexedReferenceObjectsTask,
52 doc="Reference object loader for photometry",
53 deprecated="refObjLoader is deprecated, and will be removed after v24.",
54 )
55 filterMap = pexConfig.DictField(
56 doc="Mapping from physicalFilter label to reference filter name.",
57 keytype=str,
58 itemtype=str,
59 default={},
60 )
61 applyColorTerms = pexConfig.Field(
62 doc=("Apply photometric color terms to reference stars?"
63 "Requires that colorterms be set to a ColorTermLibrary"),
64 dtype=bool,
65 default=True
66 )
67 colorterms = pexConfig.ConfigField(
68 doc="Library of photometric reference catalog name to color term dict.",
69 dtype=ColortermLibrary,
70 )
71 referenceSelector = pexConfig.ConfigurableField(
72 target=ReferenceSourceSelectorTask,
73 doc="Selection of reference sources",
74 )
76 def validate(self):
77 super().validate()
78 if not self.filterMap:
79 msg = 'Must set filterMap'
80 raise pexConfig.FieldValidationError(FgcmLoadReferenceCatalogConfig.filterMap, self, msg)
81 if self.applyColorTerms and len(self.colorterms.data) == 0:
82 msg = "applyColorTerms=True requires the `colorterms` field be set to a ColortermLibrary."
83 raise pexConfig.FieldValidationError(FgcmLoadReferenceCatalogConfig.colorterms, self, msg)
86class FgcmLoadReferenceCatalogTask(pipeBase.Task):
87 """
88 Load multi-band reference objects from a reference catalog.
90 Parameters
91 ----------
92 refObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`
93 Reference object loader.
94 refCatName : `str`
95 Name of reference catalog (for color term lookups).
96 """
97 ConfigClass = FgcmLoadReferenceCatalogConfig
98 _DefaultName = 'fgcmLoadReferenceCatalog'
100 def __init__(self, refObjLoader=None, refCatName=None, **kwargs):
101 """Construct an FgcmLoadReferenceCatalogTask
102 """
103 pipeBase.Task.__init__(self, **kwargs)
104 self.refObjLoader = refObjLoader
105 self.refCatName = refCatName
107 if refObjLoader is None or refCatName is None:
108 raise RuntimeError("FgcmLoadReferenceCatalogTask requires a refObjLoader and refCatName.")
110 self.makeSubtask('referenceSelector')
111 self._fluxFilters = None
112 self._fluxFields = None
113 self._referenceFilter = None
115 def getFgcmReferenceStarsHealpix(self, nside, pixel, filterList, nest=False):
116 """
117 Get a reference catalog that overlaps a healpix pixel, using multiple
118 filters. In addition, apply colorterms if available.
120 Return format is a numpy recarray for use with fgcm, with the format:
122 dtype = ([('ra', `np.float64`),
123 ('dec', `np.float64`),
124 ('refMag', `np.float32`, len(filterList)),
125 ('refMagErr', `np.float32`, len(filterList)])
127 Reference magnitudes (AB) will be 99 for non-detections.
129 Parameters
130 ----------
131 nside: `int`
132 Healpix nside of pixel to load
133 pixel: `int`
134 Healpix pixel of pixel to load
135 filterList: `list`
136 list of `str` of camera filter names.
137 nest: `bool`, optional
138 Is the pixel in nest format? Default is False.
140 Returns
141 -------
142 fgcmRefCat: `np.recarray`
143 """
145 # Determine the size of the sky circle to load
146 theta, phi = hp.pix2ang(nside, pixel, nest=nest)
147 center = lsst.geom.SpherePoint(phi * lsst.geom.radians, (np.pi/2. - theta) * lsst.geom.radians)
149 corners = hp.boundaries(nside, pixel, step=1, nest=nest)
150 theta_phi = hp.vec2ang(np.transpose(corners))
152 radius = 0.0 * lsst.geom.radians
153 for ctheta, cphi in zip(*theta_phi):
154 rad = center.separation(lsst.geom.SpherePoint(cphi * lsst.geom.radians,
155 (np.pi/2. - ctheta) * lsst.geom.radians))
156 if (rad > radius):
157 radius = rad
159 # Load the fgcm-format reference catalog
160 fgcmRefCat = self.getFgcmReferenceStarsSkyCircle(center.getRa().asDegrees(),
161 center.getDec().asDegrees(),
162 radius.asDegrees(),
163 filterList)
164 catPix = hp.ang2pix(nside, np.radians(90.0 - fgcmRefCat['dec']),
165 np.radians(fgcmRefCat['ra']), nest=nest)
167 inPix, = np.where(catPix == pixel)
169 return fgcmRefCat[inPix]
171 def getFgcmReferenceStarsSkyCircle(self, ra, dec, radius, filterList):
172 """
173 Get a reference catalog that overlaps a circular sky region, using
174 multiple filters. In addition, apply colorterms if available.
176 Return format is a numpy recarray for use with fgcm.
178 dtype = ([('ra', `np.float64`),
179 ('dec', `np.float64`),
180 ('refMag', `np.float32`, len(filterList)),
181 ('refMagErr', `np.float32`, len(filterList)])
183 Reference magnitudes (AB) will be 99 for non-detections.
185 Parameters
186 ----------
187 ra: `float`
188 ICRS right ascension, degrees.
189 dec: `float`
190 ICRS declination, degrees.
191 radius: `float`
192 Radius to search, degrees.
193 filterList: `list`
194 list of `str` of camera filter names.
196 Returns
197 -------
198 fgcmRefCat: `np.recarray`
199 """
201 center = lsst.geom.SpherePoint(ra * lsst.geom.degrees, dec * lsst.geom.degrees)
203 # Check if we haev previously cached values for the fluxFields
204 if self._fluxFilters is None or self._fluxFilters != filterList:
205 self._determine_flux_fields(center, filterList)
207 skyCircle = self.refObjLoader.loadSkyCircle(center,
208 radius * lsst.geom.degrees,
209 self._referenceFilter)
211 if not skyCircle.refCat.isContiguous():
212 refCat = skyCircle.refCat.copy(deep=True)
213 else:
214 refCat = skyCircle.refCat
216 # Select on raw (uncorrected) catalog, where the errors should make more sense
217 goodSources = self.referenceSelector.selectSources(refCat)
218 selected = goodSources.selected
220 fgcmRefCat = np.zeros(np.sum(selected), dtype=[('ra', 'f8'),
221 ('dec', 'f8'),
222 ('refMag', 'f4', len(filterList)),
223 ('refMagErr', 'f4', len(filterList))])
224 if fgcmRefCat.size == 0:
225 # Return an empty catalog if we don't have any selected sources
226 return fgcmRefCat
228 # The ra/dec native Angle format is radians
229 # We determine the conversion from the native units (typically
230 # radians) to degrees for the first observation. This allows us
231 # to treate ra/dec as numpy arrays rather than Angles, which would
232 # be approximately 600x slower.
234 conv = refCat[0]['coord_ra'].asDegrees() / float(refCat[0]['coord_ra'])
235 fgcmRefCat['ra'] = refCat['coord_ra'][selected] * conv
236 fgcmRefCat['dec'] = refCat['coord_dec'][selected] * conv
238 # Default (unset) values are 99.0
239 fgcmRefCat['refMag'][:, :] = 99.0
240 fgcmRefCat['refMagErr'][:, :] = 99.0
242 if self.config.applyColorTerms:
243 for i, (filterName, fluxField) in enumerate(zip(self._fluxFilters, self._fluxFields)):
244 if fluxField is None:
245 continue
247 self.log.debug("Applying color terms for filtername=%r" % (filterName))
249 colorterm = self.config.colorterms.getColorterm(filterName, self.refCatName, doRaise=True)
251 refMag, refMagErr = colorterm.getCorrectedMagnitudes(refCat)
253 # nan_to_num replaces nans with zeros, and this ensures that we select
254 # magnitudes that both filter out nans and are not very large (corresponding
255 # to very small fluxes), as "99" is a common sentinel for illegal magnitudes.
257 good, = np.where((np.nan_to_num(refMag[selected]) < 90.0)
258 & (np.nan_to_num(refMagErr[selected]) < 90.0)
259 & (np.nan_to_num(refMagErr[selected]) > 0.0))
261 fgcmRefCat['refMag'][good, i] = refMag[selected][good]
262 fgcmRefCat['refMagErr'][good, i] = refMagErr[selected][good]
264 else:
265 # No colorterms
267 for i, (filterName, fluxField) in enumerate(zip(self._fluxFilters, self._fluxFields)):
268 # nan_to_num replaces nans with zeros, and this ensures that we select
269 # fluxes that both filter out nans and are positive.
270 good, = np.where((np.nan_to_num(refCat[fluxField][selected]) > 0.0)
271 & (np.nan_to_num(refCat[fluxField+'Err'][selected]) > 0.0))
272 refMag = (refCat[fluxField][selected][good] * units.nJy).to_value(units.ABmag)
273 refMagErr = abMagErrFromFluxErr(refCat[fluxField+'Err'][selected][good],
274 refCat[fluxField][selected][good])
275 fgcmRefCat['refMag'][good, i] = refMag
276 fgcmRefCat['refMagErr'][good, i] = refMagErr
278 return fgcmRefCat
280 def _determine_flux_fields(self, center, filterList):
281 """
282 Determine the flux field names for a reference catalog.
284 Will set self._fluxFields, self._referenceFilter.
286 Parameters
287 ----------
288 center: `lsst.geom.SpherePoint`
289 The center around which to load test sources.
290 filterList: `list`
291 list of `str` of camera filter names.
292 """
294 # Record self._fluxFilters for checks on subsequent calls
295 self._fluxFilters = filterList
297 # Search for a good filter to use to load the reference catalog
298 # via the refObjLoader task which requires a valid filterName
299 foundReferenceFilter = False
300 for filterName in filterList:
301 refFilterName = self.config.filterMap.get(filterName)
302 if refFilterName is None:
303 continue
305 try:
306 results = self.refObjLoader.loadSkyCircle(center,
307 0.05 * lsst.geom.degrees,
308 refFilterName)
309 foundReferenceFilter = True
310 self._referenceFilter = refFilterName
311 break
312 except RuntimeError:
313 # This just means that the filterName wasn't listed
314 # in the reference catalog. This is okay.
315 pass
317 if not foundReferenceFilter:
318 raise RuntimeError("Could not find any valid flux field(s) %s" %
319 (", ".join(filterList)))
321 # Retrieve all the fluxField names
322 self._fluxFields = []
323 for filterName in filterList:
324 fluxField = None
326 refFilterName = self.config.filterMap.get(filterName)
328 if refFilterName is not None:
329 try:
330 fluxField = getRefFluxField(results.refCat.schema, filterName=refFilterName)
331 except RuntimeError:
332 # This flux field isn't available. Set to None
333 fluxField = None
335 if fluxField is None:
336 self.log.warning(f'No reference flux field for camera filter {filterName}')
338 self._fluxFields.append(fluxField)