lsst.fgcmcal  21.0.0-7-g6531d7b+988fabe502
fgcmLoadReferenceCatalog.py
Go to the documentation of this file.
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.
24 
25 This task will load multi-band reference objects and apply color terms (if
26 configured). This wrapper around LoadReferenceObjects task also allows loading
27 by healpix pixel (the native pixelization of fgcm), and is self-contained so
28 the task can be called by third-party code.
29 """
30 
31 import numpy as np
32 import healpy as hp
33 from astropy import units
34 
35 import lsst.pex.config as pexConfig
36 import lsst.pipe.base as pipeBase
37 from lsst.meas.algorithms import LoadIndexedReferenceObjectsTask, ReferenceSourceSelectorTask
38 from lsst.meas.algorithms import getRefFluxField
39 from lsst.pipe.tasks.colorterms import ColortermLibrary
40 from lsst.afw.image import abMagErrFromFluxErr
41 from lsst.meas.algorithms import ReferenceObjectLoader
42 
43 import lsst.geom
44 
45 __all__ = ['FgcmLoadReferenceCatalogConfig', 'FgcmLoadReferenceCatalogTask']
46 
47 
48 class FgcmLoadReferenceCatalogConfig(pexConfig.Config):
49  """Config for FgcmLoadReferenceCatalogTask"""
50 
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  )
84 
85  def validate(self):
86  super().validate()
87  if not self.filterMapfilterMap:
88  msg = 'Must set filterMap'
89  raise pexConfig.FieldValidationError(FgcmLoadReferenceCatalogConfig.filterMap, self, msg)
90  if self.applyColorTermsapplyColorTerms and len(self.colortermscolorterms.data) == 0:
91  msg = "applyColorTerms=True requires the `colorterms` field be set to a ColortermLibrary."
92  raise pexConfig.FieldValidationError(FgcmLoadReferenceCatalogConfig.colorterms, self, msg)
93 
94 
95 class FgcmLoadReferenceCatalogTask(pipeBase.Task):
96  """
97  Load multi-band reference objects from a reference catalog.
98 
99  Parameters
100  ----------
101  butler: `lsst.daf.persistence.Butler`
102  Data butler for reading catalogs
103  """
104  ConfigClass = FgcmLoadReferenceCatalogConfig
105  _DefaultName = 'fgcmLoadReferenceCatalog'
106 
107  def __init__(self, butler=None, refObjLoader=None, **kwargs):
108  """Construct an FgcmLoadReferenceCatalogTask
109 
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.refObjLoaderrefObjLoader = refObjLoader
120 
121  self.makeSubtask('referenceSelector')
122  self._fluxFilters_fluxFilters = None
123  self._fluxFields_fluxFields = None
124  self._referenceFilter_referenceFilter = None
125 
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.
130 
131  Return format is a numpy recarray for use with fgcm, with the format:
132 
133  dtype = ([('ra', `np.float64`),
134  ('dec', `np.float64`),
135  ('refMag', `np.float32`, len(filterList)),
136  ('refMagErr', `np.float32`, len(filterList)])
137 
138  Reference magnitudes (AB) will be 99 for non-detections.
139 
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.
150 
151  Returns
152  -------
153  fgcmRefCat: `np.recarray`
154  """
155 
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)
159 
160  corners = hp.boundaries(nside, pixel, step=1, nest=nest)
161  theta_phi = hp.vec2ang(np.transpose(corners))
162 
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
169 
170  # Load the fgcm-format reference catalog
171  fgcmRefCat = self.getFgcmReferenceStarsSkyCirclegetFgcmReferenceStarsSkyCircle(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)
177 
178  inPix, = np.where(catPix == pixel)
179 
180  return fgcmRefCat[inPix]
181 
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.
186 
187  Return format is a numpy recarray for use with fgcm.
188 
189  dtype = ([('ra', `np.float64`),
190  ('dec', `np.float64`),
191  ('refMag', `np.float32`, len(filterList)),
192  ('refMagErr', `np.float32`, len(filterList)])
193 
194  Reference magnitudes (AB) will be 99 for non-detections.
195 
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.
206 
207  Returns
208  -------
209  fgcmRefCat: `np.recarray`
210  """
211 
212  center = lsst.geom.SpherePoint(ra * lsst.geom.degrees, dec * lsst.geom.degrees)
213 
214  # Check if we haev previously cached values for the fluxFields
215  if self._fluxFilters_fluxFilters is None or self._fluxFilters_fluxFilters != filterList:
216  self._determine_flux_fields_determine_flux_fields(center, filterList)
217 
218  skyCircle = self.refObjLoaderrefObjLoader.loadSkyCircle(center,
219  radius * lsst.geom.degrees,
220  self._referenceFilter_referenceFilter)
221 
222  if not skyCircle.refCat.isContiguous():
223  refCat = skyCircle.refCat.copy(deep=True)
224  else:
225  refCat = skyCircle.refCat
226 
227  # Select on raw (uncorrected) catalog, where the errors should make more sense
228  goodSources = self.referenceSelector.selectSources(refCat)
229  selected = goodSources.selected
230 
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
238 
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.
244 
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
248 
249  # Default (unset) values are 99.0
250  fgcmRefCat['refMag'][:, :] = 99.0
251  fgcmRefCat['refMagErr'][:, :] = 99.0
252 
253  if self.config.applyColorTerms:
254  if isinstance(self.refObjLoaderrefObjLoader, ReferenceObjectLoader):
255  # Gen3
256  refCatName = self.refObjLoaderrefObjLoader.config.value.ref_dataset_name
257  else:
258  # Gen2
259  refCatName = self.refObjLoaderrefObjLoader.ref_dataset_name
260 
261  for i, (filterName, fluxField) in enumerate(zip(self._fluxFilters_fluxFilters, self._fluxFields_fluxFields)):
262  if fluxField is None:
263  continue
264 
265  self.log.debug("Applying color terms for filtername=%r" % (filterName))
266 
267  colorterm = self.config.colorterms.getColorterm(
268  filterName=filterName, photoCatName=refCatName, doRaise=True)
269 
270  refMag, refMagErr = colorterm.getCorrectedMagnitudes(refCat, filterName)
271 
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.
275 
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))
279 
280  fgcmRefCat['refMag'][good, i] = refMag[selected][good]
281  fgcmRefCat['refMagErr'][good, i] = refMagErr[selected][good]
282 
283  else:
284  # No colorterms
285 
286  for i, (filterName, fluxField) in enumerate(zip(self._fluxFilters_fluxFilters, self._fluxFields_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
296 
297  return fgcmRefCat
298 
299  def _determine_flux_fields(self, center, filterList):
300  """
301  Determine the flux field names for a reference catalog.
302 
303  Will set self._fluxFields, self._referenceFilter.
304 
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  """
312 
313  # Record self._fluxFilters for checks on subsequent calls
314  self._fluxFilters_fluxFilters = filterList
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  for filterName in filterList:
320  refFilterName = self.config.filterMap.get(filterName)
321  if refFilterName is None:
322  continue
323 
324  try:
325  results = self.refObjLoaderrefObjLoader.loadSkyCircle(center,
326  0.05 * lsst.geom.degrees,
327  refFilterName)
328  foundReferenceFilter = True
329  self._referenceFilter_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
335 
336  if not foundReferenceFilter:
337  raise RuntimeError("Could not find any valid flux field(s) %s" %
338  (", ".join(filterList)))
339 
340  # Retrieve all the fluxField names
341  self._fluxFields_fluxFields = []
342  for filterName in filterList:
343  fluxField = None
344 
345  refFilterName = self.config.filterMap.get(filterName)
346 
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
353 
354  if fluxField is None:
355  self.log.warn(f'No reference flux field for camera filter {filterName}')
356 
357  self._fluxFields_fluxFields.append(fluxField)
def __init__(self, butler=None, refObjLoader=None, **kwargs)
def getFgcmReferenceStarsHealpix(self, nside, pixel, filterList, nest=False)