lsst.pipe.tasks  17.0.1-6-g640019af
colorterms.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010, 2011 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 import fnmatch
23 
24 import numpy as np
25 import astropy.units as u
26 
27 from lsst.afw.image import abMagErrFromFluxErr
28 import lsst.pex.exceptions as pexExcept
29 from lsst.pex.config import Config, Field, ConfigDictField
30 from lsst.afw.image import Filter
31 
32 __all__ = ["ColortermNotFoundError", "Colorterm", "ColortermDict", "ColortermLibrary"]
33 
34 
35 class ColortermNotFoundError(LookupError):
36  """Exception class indicating we couldn't find a colorterm
37  """
38  pass
39 
40 
41 class Colorterm(Config):
42  """!Colorterm correction for one pair of filters
43 
44  The transformed magnitude p' is given by
45  p' = primary + c0 + c1*(primary - secondary) + c2*(primary - secondary)**2
46 
47  To construct a Colorterm, use keyword arguments:
48  Colorterm(primary=primaryFilterName, secondary=secondaryFilterName, c0=c0value, c1=c1Coeff, c2=c2Coeff)
49  where c0-c2 are optional. For example (omitting c2):
50  Colorterm(primary="g", secondary="r", c0=-0.00816446, c1=-0.08366937)
51 
52  This is subclass of Config. That is a bit of a hack to make it easy to store the data
53  in an appropriate obs_* package as a config override file. In the long term some other
54  means of persistence will be used, at which point the constructor can be simplified
55  to not require keyword arguments. (Fixing DM-2831 will also allow making a custom constructor).
56  """
57  primary = Field(dtype=str, doc="name of primary filter")
58  secondary = Field(dtype=str, doc="name of secondary filter")
59  c0 = Field(dtype=float, default=0.0, doc="Constant parameter")
60  c1 = Field(dtype=float, default=0.0, doc="First-order parameter")
61  c2 = Field(dtype=float, default=0.0, doc="Second-order parameter")
62 
63  def getCorrectedMagnitudes(self, refCat, filterName):
64  """Return the colorterm corrected magnitudes for a given filter.
65 
66  Parameters
67  ----------
69  The reference catalog to apply color corrections to.
70  filterName : `str`
71  The camera filter to correct the reference catalog into.
72 
73  Returns
74  -------
75  RefMag : `np.ndarray`
76  The corrected AB magnitudes.
77  RefMagErr : `np.ndarray`
78  The corrected AB magnitude errors.
79 
80  Raises
81  ------
82  KeyError
83  Raised if the reference catalog does not have a flux uncertainty
84  for that filter.
85 
86  Notes
87  -----
88  WARNING: I do not know that we can trust the propagation of magnitude
89  errors returned by this method. They need more thorough tests.
90  """
91 
92  def getFluxes(fluxField):
93  """Get the flux and fluxErr of this field from refCat."""
94  fluxKey = refCat.schema.find(fluxField).key
95  refFlux = refCat[fluxKey]
96  try:
97  fluxErrKey = refCat.schema.find(fluxField + "Err").key
98  refFluxErr = refCat[fluxErrKey]
99  except KeyError as e:
100  raise KeyError("Reference catalog does not have flux uncertainties for %s" % fluxField) from e
101 
102  return refFlux, refFluxErr
103 
104  primaryFlux, primaryErr = getFluxes(self.primary + "_flux")
105  secondaryFlux, secondaryErr = getFluxes(self.secondary + "_flux")
106 
107  primaryMag = u.Quantity(primaryFlux, u.Jy).to_value(u.ABmag)
108  secondaryMag = u.Quantity(secondaryFlux, u.Jy).to_value(u.ABmag)
109 
110  refMag = self.transformMags(primaryMag, secondaryMag)
111  refFluxErrArr = self.propagateFluxErrors(primaryErr, secondaryErr)
112 
113  refMagErr = abMagErrFromFluxErr(refFluxErrArr, primaryFlux)
114 
115  return refMag, refMagErr
116 
117  def transformSource(self, source):
118  """!Transform the brightness of a source
119 
120  @param[in] source source whose brightness is to be converted; must support get(filterName)
121  (e.g. source.get("r")) method, as do afw::table::Source and dicts.
122  @return the transformed source magnitude
123  """
124  return self.transformMags(source.get(self.primary), source.get(self.secondary))
125 
126  def transformMags(self, primary, secondary):
127  """!Transform brightness
128 
129  @param[in] primary brightness in primary filter (magnitude)
130  @param[in] secondary brightness in secondary filter (magnitude)
131  @return the transformed brightness (as a magnitude)
132  """
133  color = primary - secondary
134  return primary + self.c0 + color*(self.c1 + color*self.c2)
135 
136  def propagateFluxErrors(self, primaryFluxErr, secondaryFluxErr):
137  return np.hypot((1 + self.c1)*primaryFluxErr, self.c1*secondaryFluxErr)
138 
139 
140 class ColortermDict(Config):
141  """!A mapping of filterName to Colorterm
142 
143  Different reference catalogs may need different ColortermDicts; see ColortermLibrary
144 
145  To construct a ColortermDict use keyword arguments:
146  ColortermDict(data=dataDict)
147  where dataDict is a Python dict of filterName: Colorterm
148  For example:
149  ColortermDict(data={
150  'g': Colorterm(primary="g", secondary="r", c0=-0.00816446, c1=-0.08366937, c2=-0.00726883),
151  'r': Colorterm(primary="r", secondary="i", c0= 0.00231810, c1= 0.01284177, c2=-0.03068248),
152  'i': Colorterm(primary="i", secondary="z", c0= 0.00130204, c1=-0.16922042, c2=-0.01374245),
153  })
154  The constructor will likely be simplified at some point.
155 
156  This is subclass of Config. That is a bit of a hack to make it easy to store the data
157  in an appropriate obs_* package as a config override file. In the long term some other
158  means of persistence will be used, at which point the constructor can be made saner.
159  """
160  data = ConfigDictField(
161  doc="Mapping of filter name to Colorterm",
162  keytype=str,
163  itemtype=Colorterm,
164  default={},
165  )
166 
167 
168 class ColortermLibrary(Config):
169  """!A mapping of photometric reference catalog name or glob to ColortermDict
170 
171  This allows photometric calibration using a variety of reference catalogs.
172 
173  To construct a ColortermLibrary, use keyword arguments:
174  ColortermLibrary(data=dataDict)
175  where dataDict is a Python dict of catalog_name_or_glob: ColortermDict
176 
177  For example:
178  ColortermLibrary(data = {
179  "hsc*": ColortermDict(data={
180  'g': Colorterm(primary="g", secondary="g"),
181  'r': Colorterm(primary="r", secondary="r"),
182  ...
183  }),
184  "sdss*": ColortermDict(data={
185  'g': Colorterm(primary="g", secondary="r", c0=-0.00816446, c1=-0.08366937, c2=-0.00726883),
186  'r': Colorterm(primary="r", secondary="i", c0= 0.00231810, c1= 0.01284177, c2=-0.03068248),
187  ...
188  }),
189  })
190 
191  This is subclass of Config. That is a bit of a hack to make it easy to store the data
192  in an appropriate obs_* package as a config override file. In the long term some other
193  means of persistence will be used, at which point the constructor can be made saner.
194  """
195  data = ConfigDictField(
196  doc="Mapping of reference catalog name (or glob) to ColortermDict",
197  keytype=str,
198  itemtype=ColortermDict,
199  default={},
200  )
201 
202  def getColorterm(self, filterName, photoCatName, doRaise=True):
203  """!Get the appropriate Colorterm from the library
204 
205  Use dict of color terms in the library that matches the photoCatName.
206  If the photoCatName exactly matches an entry in the library, that
207  dict is used; otherwise if the photoCatName matches a single glob (shell syntax,
208  e.g., "sdss-*" will match "sdss-dr8"), then that is used. If there is no
209  exact match and no unique match to the globs, raise an exception.
210 
211  @param filterName name of filter
212  @param photoCatName name of photometric reference catalog from which to retrieve the data.
213  This argument is not glob-expanded (but the catalog names in the library are,
214  if no exact match is found).
215  @param[in] doRaise if True then raise ColortermNotFoundError if no suitable Colorterm found;
216  if False then return a null Colorterm with filterName as the primary and secondary filter
217  @return the appropriate Colorterm
218 
219  @throw ColortermNotFoundError if no suitable Colorterm found and doRaise true;
220  other exceptions may be raised for unexpected errors, regardless of the value of doRaise
221  """
222  try:
223  trueRefCatName = None
224  ctDictConfig = self.data.get(photoCatName)
225  if ctDictConfig is None:
226  # try glob expression
227  matchList = [libRefNameGlob for libRefNameGlob in self.data
228  if fnmatch.fnmatch(photoCatName, libRefNameGlob)]
229  if len(matchList) == 1:
230  trueRefCatName = matchList[0]
231  ctDictConfig = self.data[trueRefCatName]
232  elif len(matchList) > 1:
233  raise ColortermNotFoundError(
234  "Multiple library globs match photoCatName %r: %s" % (photoCatName, matchList))
235  else:
236  raise ColortermNotFoundError(
237  "No colorterm dict found with photoCatName %r" % photoCatName)
238  ctDict = ctDictConfig.data
239  if filterName not in ctDict:
240  # Perhaps it's an alias
241  try:
242  filterName = Filter(Filter(filterName).getId()).getName()
243  except pexExcept.NotFoundError:
244  pass # this will be handled shortly
245  if filterName not in ctDict:
246  errMsg = "No colorterm found for filter %r with photoCatName %r" % (
247  filterName, photoCatName)
248  if trueRefCatName is not None:
249  errMsg += " = catalog %r" % (trueRefCatName,)
250  raise ColortermNotFoundError(errMsg)
251  return ctDict[filterName]
252  except ColortermNotFoundError:
253  if doRaise:
254  raise
255  else:
256  return Colorterm(filterName, filterName)
A mapping of filterName to Colorterm.
Definition: colorterms.py:140
A mapping of photometric reference catalog name or glob to ColortermDict.
Definition: colorterms.py:168
Colorterm correction for one pair of filters.
Definition: colorterms.py:41