Coverage for python/lsst/pipe/tasks/colorterms.py : 29%

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#
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#
22import fnmatch
24import numpy as np
25import astropy.units as u
27from lsst.afw.image import abMagErrFromFluxErr
28from lsst.pex.config import Config, Field, ConfigDictField
30__all__ = ["ColortermNotFoundError", "Colorterm", "ColortermDict", "ColortermLibrary"]
33class ColortermNotFoundError(LookupError):
34 """Exception class indicating we couldn't find a colorterm
35 """
36 pass
39class Colorterm(Config):
40 """!Colorterm correction for one pair of filters
42 The transformed magnitude p' is given by
43 p' = primary + c0 + c1*(primary - secondary) + c2*(primary - secondary)**2
45 To construct a Colorterm, use keyword arguments:
46 Colorterm(primary=primaryFilterName, secondary=secondaryFilterName, c0=c0value, c1=c1Coeff, c2=c2Coeff)
47 where c0-c2 are optional. For example (omitting c2):
48 Colorterm(primary="g", secondary="r", c0=-0.00816446, c1=-0.08366937)
50 This is subclass of Config. That is a bit of a hack to make it easy to store the data
51 in an appropriate obs_* package as a config override file. In the long term some other
52 means of persistence will be used, at which point the constructor can be simplified
53 to not require keyword arguments. (Fixing DM-2831 will also allow making a custom constructor).
54 """
55 primary = Field(dtype=str, doc="name of primary filter")
56 secondary = Field(dtype=str, doc="name of secondary filter")
57 c0 = Field(dtype=float, default=0.0, doc="Constant parameter")
58 c1 = Field(dtype=float, default=0.0, doc="First-order parameter")
59 c2 = Field(dtype=float, default=0.0, doc="Second-order parameter")
61 def getCorrectedMagnitudes(self, refCat, filterName):
62 """Return the colorterm corrected magnitudes for a given filter.
64 Parameters
65 ----------
66 refCat : `lsst.afw.table.SimpleCatalog`
67 The reference catalog to apply color corrections to.
68 filterName : `str`
69 The camera filter to correct the reference catalog into.
71 Returns
72 -------
73 RefMag : `np.ndarray`
74 The corrected AB magnitudes.
75 RefMagErr : `np.ndarray`
76 The corrected AB magnitude errors.
78 Raises
79 ------
80 KeyError
81 Raised if the reference catalog does not have a flux uncertainty
82 for that filter.
84 Notes
85 -----
86 WARNING: I do not know that we can trust the propagation of magnitude
87 errors returned by this method. They need more thorough tests.
88 """
90 def getFluxes(fluxField):
91 """Get the flux and fluxErr of this field from refCat."""
92 fluxKey = refCat.schema.find(fluxField).key
93 refFlux = refCat[fluxKey]
94 try:
95 fluxErrKey = refCat.schema.find(fluxField + "Err").key
96 refFluxErr = refCat[fluxErrKey]
97 except KeyError as e:
98 raise KeyError("Reference catalog does not have flux uncertainties for %s" % fluxField) from e
100 return refFlux, refFluxErr
102 primaryFlux, primaryErr = getFluxes(self.primary + "_flux")
103 secondaryFlux, secondaryErr = getFluxes(self.secondary + "_flux")
105 primaryMag = u.Quantity(primaryFlux, u.nJy).to_value(u.ABmag)
106 secondaryMag = u.Quantity(secondaryFlux, u.nJy).to_value(u.ABmag)
108 refMag = self.transformMags(primaryMag, secondaryMag)
109 refFluxErrArr = self.propagateFluxErrors(primaryErr, secondaryErr)
111 # HACK convert to Jy until we have a replacement for this (DM-16903)
112 refMagErr = abMagErrFromFluxErr(refFluxErrArr*1e-9, primaryFlux*1e-9)
114 return refMag, refMagErr
116 def transformSource(self, source):
117 """!Transform the brightness of a source
119 @param[in] source source whose brightness is to be converted; must support get(filterName)
120 (e.g. source.get("r")) method, as do afw::table::Source and dicts.
121 @return the transformed source magnitude
122 """
123 return self.transformMags(source.get(self.primary), source.get(self.secondary))
125 def transformMags(self, primary, secondary):
126 """!Transform brightness
128 @param[in] primary brightness in primary filter (magnitude)
129 @param[in] secondary brightness in secondary filter (magnitude)
130 @return the transformed brightness (as a magnitude)
131 """
132 color = primary - secondary
133 return primary + self.c0 + color*(self.c1 + color*self.c2)
135 def propagateFluxErrors(self, primaryFluxErr, secondaryFluxErr):
136 return np.hypot((1 + self.c1)*primaryFluxErr, self.c1*secondaryFluxErr)
139class ColortermDict(Config):
140 """!A mapping of filterName to Colorterm
142 Different reference catalogs may need different ColortermDicts; see ColortermLibrary
144 To construct a ColortermDict use keyword arguments:
145 ColortermDict(data=dataDict)
146 where dataDict is a Python dict of filterName: Colorterm
147 For example:
148 ColortermDict(data={
149 'g': Colorterm(primary="g", secondary="r", c0=-0.00816446, c1=-0.08366937, c2=-0.00726883),
150 'r': Colorterm(primary="r", secondary="i", c0= 0.00231810, c1= 0.01284177, c2=-0.03068248),
151 'i': Colorterm(primary="i", secondary="z", c0= 0.00130204, c1=-0.16922042, c2=-0.01374245),
152 })
153 The constructor will likely be simplified at some point.
155 This is subclass of Config. That is a bit of a hack to make it easy to store the data
156 in an appropriate obs_* package as a config override file. In the long term some other
157 means of persistence will be used, at which point the constructor can be made saner.
158 """
159 data = ConfigDictField(
160 doc="Mapping of filter name to Colorterm",
161 keytype=str,
162 itemtype=Colorterm,
163 default={},
164 )
167class ColortermLibrary(Config):
168 """!A mapping of photometric reference catalog name or glob to ColortermDict
170 This allows photometric calibration using a variety of reference catalogs.
172 To construct a ColortermLibrary, use keyword arguments:
173 ColortermLibrary(data=dataDict)
174 where dataDict is a Python dict of catalog_name_or_glob: ColortermDict
176 For example:
177 ColortermLibrary(data = {
178 "hsc*": ColortermDict(data={
179 'g': Colorterm(primary="g", secondary="g"),
180 'r': Colorterm(primary="r", secondary="r"),
181 ...
182 }),
183 "sdss*": ColortermDict(data={
184 'g': Colorterm(primary="g", secondary="r", c0=-0.00816446, c1=-0.08366937, c2=-0.00726883),
185 'r': Colorterm(primary="r", secondary="i", c0= 0.00231810, c1= 0.01284177, c2=-0.03068248),
186 ...
187 }),
188 })
190 This is subclass of Config. That is a bit of a hack to make it easy to store the data
191 in an appropriate obs_* package as a config override file. In the long term some other
192 means of persistence will be used, at which point the constructor can be made saner.
193 """
194 data = ConfigDictField(
195 doc="Mapping of reference catalog name (or glob) to ColortermDict",
196 keytype=str,
197 itemtype=ColortermDict,
198 default={},
199 )
201 def getColorterm(self, filterName, photoCatName, doRaise=True):
202 """!Get the appropriate Colorterm from the library
204 Use dict of color terms in the library that matches the photoCatName.
205 If the photoCatName exactly matches an entry in the library, that
206 dict is used; otherwise if the photoCatName matches a single glob (shell syntax,
207 e.g., "sdss-*" will match "sdss-dr8"), then that is used. If there is no
208 exact match and no unique match to the globs, raise an exception.
210 @param filterName name of filter
211 @param photoCatName name of photometric reference catalog from which to retrieve the data.
212 This argument is not glob-expanded (but the catalog names in the library are,
213 if no exact match is found).
214 @param[in] doRaise if True then raise ColortermNotFoundError if no suitable Colorterm found;
215 if False then return a null Colorterm with filterName as the primary and secondary filter
216 @return the appropriate Colorterm
218 @throw ColortermNotFoundError if no suitable Colorterm found and doRaise true;
219 other exceptions may be raised for unexpected errors, regardless of the value of doRaise
220 """
221 try:
222 trueRefCatName = None
223 ctDictConfig = self.data.get(photoCatName)
224 if ctDictConfig is None:
225 # try glob expression
226 matchList = [libRefNameGlob for libRefNameGlob in self.data
227 if fnmatch.fnmatch(photoCatName, libRefNameGlob)]
228 if len(matchList) == 1:
229 trueRefCatName = matchList[0]
230 ctDictConfig = self.data[trueRefCatName]
231 elif len(matchList) > 1:
232 raise ColortermNotFoundError(
233 "Multiple library globs match photoCatName %r: %s" % (photoCatName, matchList))
234 else:
235 raise ColortermNotFoundError(
236 "No colorterm dict found with photoCatName %r" % photoCatName)
237 ctDict = ctDictConfig.data
238 if filterName not in ctDict:
239 errMsg = "No colorterm found for filter %r with photoCatName %r" % (
240 filterName, photoCatName)
241 if trueRefCatName is not None:
242 errMsg += " = catalog %r" % (trueRefCatName,)
243 raise ColortermNotFoundError(errMsg)
244 return ctDict[filterName]
245 except ColortermNotFoundError:
246 if doRaise:
247 raise
248 else:
249 return Colorterm(filterName, filterName)