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

70 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-24 02:36 -0700

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 

23import warnings 

24 

25import numpy as np 

26import astropy.units as u 

27 

28from lsst.afw.image import abMagErrFromFluxErr 

29from lsst.pex.config import Config, Field, ConfigDictField 

30 

31__all__ = ["ColortermNotFoundError", "Colorterm", "ColortermDict", "ColortermLibrary"] 

32 

33 

34class ColortermNotFoundError(LookupError): 

35 """Exception class indicating we couldn't find a colorterm 

36 """ 

37 pass 

38 

39 

40class Colorterm(Config): 

41 """!Colorterm correction for one pair of filters 

42 

43 The transformed magnitude p' is given by 

44 p' = primary + c0 + c1*(primary - secondary) + c2*(primary - secondary)**2 

45 

46 To construct a Colorterm, use keyword arguments: 

47 Colorterm(primary=primaryFilterName, secondary=secondaryFilterName, c0=c0value, c1=c1Coeff, c2=c2Coeff) 

48 where c0-c2 are optional. For example (omitting c2): 

49 Colorterm(primary="g", secondary="r", c0=-0.00816446, c1=-0.08366937) 

50 

51 This is subclass of Config. That is a bit of a hack to make it easy to store the data 

52 in an appropriate obs_* package as a config override file. In the long term some other 

53 means of persistence will be used, at which point the constructor can be simplified 

54 to not require keyword arguments. (Fixing DM-2831 will also allow making a custom constructor). 

55 """ 

56 primary = Field(dtype=str, doc="name of primary filter") 

57 secondary = Field(dtype=str, doc="name of secondary filter") 

58 c0 = Field(dtype=float, default=0.0, doc="Constant parameter") 

59 c1 = Field(dtype=float, default=0.0, doc="First-order parameter") 

60 c2 = Field(dtype=float, default=0.0, doc="Second-order parameter") 

61 

62 def getCorrectedMagnitudes(self, refCat, filterName="deprecatedArgument"): 

63 """Return the colorterm corrected magnitudes for a given filter. 

64 

65 Parameters 

66 ---------- 

67 refCat : `lsst.afw.table.SimpleCatalog` 

68 The reference catalog to apply color corrections to. 

69 filterName : `str`, deprecated 

70 The camera filter to correct the reference catalog into. 

71 The ``filterName`` argument is unused and will be removed in v23. 

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 if filterName != "deprecatedArgument": 

93 msg = "Colorterm.getCorrectedMagnitudes() `filterName` arg is unused and will be removed in v23." 

94 warnings.warn(msg, category=FutureWarning) 

95 

96 def getFluxes(fluxField): 

97 """Get the flux and fluxErr of this field from refCat.""" 

98 fluxKey = refCat.schema.find(fluxField).key 

99 refFlux = refCat[fluxKey] 

100 try: 

101 fluxErrKey = refCat.schema.find(fluxField + "Err").key 

102 refFluxErr = refCat[fluxErrKey] 

103 except KeyError as e: 

104 raise KeyError("Reference catalog does not have flux uncertainties for %s" % fluxField) from e 

105 

106 return refFlux, refFluxErr 

107 

108 primaryFlux, primaryErr = getFluxes(self.primary + "_flux") 

109 secondaryFlux, secondaryErr = getFluxes(self.secondary + "_flux") 

110 

111 primaryMag = u.Quantity(primaryFlux, u.nJy).to_value(u.ABmag) 

112 secondaryMag = u.Quantity(secondaryFlux, u.nJy).to_value(u.ABmag) 

113 

114 refMag = self.transformMags(primaryMag, secondaryMag) 

115 refFluxErrArr = self.propagateFluxErrors(primaryErr, secondaryErr) 

116 

117 # HACK convert to Jy until we have a replacement for this (DM-16903) 

118 refMagErr = abMagErrFromFluxErr(refFluxErrArr*1e-9, primaryFlux*1e-9) 

119 

120 return refMag, refMagErr 

121 

122 def transformSource(self, source): 

123 """!Transform the brightness of a source 

124 

125 @param[in] source source whose brightness is to be converted; must support get(filterName) 

126 (e.g. source.get("r")) method, as do afw::table::Source and dicts. 

127 @return the transformed source magnitude 

128 """ 

129 return self.transformMags(source.get(self.primary), source.get(self.secondary)) 

130 

131 def transformMags(self, primary, secondary): 

132 """!Transform brightness 

133 

134 @param[in] primary brightness in primary filter (magnitude) 

135 @param[in] secondary brightness in secondary filter (magnitude) 

136 @return the transformed brightness (as a magnitude) 

137 """ 

138 color = primary - secondary 

139 return primary + self.c0 + color*(self.c1 + color*self.c2) 

140 

141 def propagateFluxErrors(self, primaryFluxErr, secondaryFluxErr): 

142 return np.hypot((1 + self.c1)*primaryFluxErr, self.c1*secondaryFluxErr) 

143 

144 

145class ColortermDict(Config): 

146 """A mapping of physical filter label to Colorterm 

147 

148 Different reference catalogs may need different ColortermDicts; see ColortermLibrary 

149 

150 To construct a ColortermDict use keyword arguments: 

151 ColortermDict(data=dataDict) 

152 where dataDict is a Python dict of filterName: Colorterm 

153 For example: 

154 ColortermDict(data={ 

155 'g': Colorterm(primary="g", secondary="r", c0=-0.00816446, c1=-0.08366937, c2=-0.00726883), 

156 'r': Colorterm(primary="r", secondary="i", c0= 0.00231810, c1= 0.01284177, c2=-0.03068248), 

157 'i': Colorterm(primary="i", secondary="z", c0= 0.00130204, c1=-0.16922042, c2=-0.01374245), 

158 }) 

159 The constructor will likely be simplified at some point. 

160 

161 This is subclass of Config. That is a bit of a hack to make it easy to store the data 

162 in an appropriate obs_* package as a config override file. In the long term some other 

163 means of persistence will be used, at which point the constructor can be made saner. 

164 """ 

165 data = ConfigDictField( 

166 doc="Mapping of filter name to Colorterm", 

167 keytype=str, 

168 itemtype=Colorterm, 

169 default={}, 

170 ) 

171 

172 

173class ColortermLibrary(Config): 

174 """!A mapping of photometric reference catalog name or glob to ColortermDict 

175 

176 This allows photometric calibration using a variety of reference catalogs. 

177 

178 To construct a ColortermLibrary, use keyword arguments: 

179 ColortermLibrary(data=dataDict) 

180 where dataDict is a Python dict of catalog_name_or_glob: ColortermDict 

181 

182 For example: 

183 ColortermLibrary(data = { 

184 "hsc*": ColortermDict(data={ 

185 'g': Colorterm(primary="g", secondary="g"), 

186 'r': Colorterm(primary="r", secondary="r"), 

187 ... 

188 }), 

189 "sdss*": ColortermDict(data={ 

190 'g': Colorterm(primary="g", secondary="r", c0=-0.00816446, c1=-0.08366937, c2=-0.00726883), 

191 'r': Colorterm(primary="r", secondary="i", c0= 0.00231810, c1= 0.01284177, c2=-0.03068248), 

192 ... 

193 }), 

194 }) 

195 

196 This is subclass of Config. That is a bit of a hack to make it easy to store the data 

197 in an appropriate obs_* package as a config override file. In the long term some other 

198 means of persistence will be used, at which point the constructor can be made saner. 

199 """ 

200 data = ConfigDictField( 

201 doc="Mapping of reference catalog name (or glob) to ColortermDict", 

202 keytype=str, 

203 itemtype=ColortermDict, 

204 default={}, 

205 ) 

206 

207 def getColorterm(self, physicalFilter, photoCatName, doRaise=True): 

208 """!Get the appropriate Colorterm from the library 

209 

210 Use dict of color terms in the library that matches the photoCatName. 

211 If the photoCatName exactly matches an entry in the library, that 

212 dict is used; otherwise if the photoCatName matches a single glob (shell syntax, 

213 e.g., "sdss-*" will match "sdss-dr8"), then that is used. If there is no 

214 exact match and no unique match to the globs, raise an exception. 

215 

216 @param physicalFilter Label of physical filter to correct to. 

217 @param photoCatName name of photometric reference catalog from which to retrieve the data. 

218 This argument is not glob-expanded (but the catalog names in the library are, 

219 if no exact match is found). 

220 @param[in] doRaise if True then raise ColortermNotFoundError if no suitable Colorterm found; 

221 if False then return a null Colorterm with physicalFilter as the primary and secondary filter 

222 @return the appropriate Colorterm 

223 

224 @throw ColortermNotFoundError if no suitable Colorterm found and doRaise true; 

225 other exceptions may be raised for unexpected errors, regardless of the value of doRaise 

226 """ 

227 try: 

228 trueRefCatName = None 

229 ctDictConfig = self.data.get(photoCatName) 

230 if ctDictConfig is None: 

231 # try glob expression 

232 matchList = [libRefNameGlob for libRefNameGlob in self.data 

233 if fnmatch.fnmatch(photoCatName, libRefNameGlob)] 

234 if len(matchList) == 1: 

235 trueRefCatName = matchList[0] 

236 ctDictConfig = self.data[trueRefCatName] 

237 elif len(matchList) > 1: 

238 raise ColortermNotFoundError( 

239 "Multiple library globs match photoCatName %r: %s" % (photoCatName, matchList)) 

240 else: 

241 raise ColortermNotFoundError( 

242 "No colorterm dict found with photoCatName %r" % photoCatName) 

243 ctDict = ctDictConfig.data 

244 if physicalFilter not in ctDict: 

245 errMsg = "No colorterm found for filter %r with photoCatName %r" % ( 

246 physicalFilter, photoCatName) 

247 if trueRefCatName is not None: 

248 errMsg += " = catalog %r" % (trueRefCatName,) 

249 raise ColortermNotFoundError(errMsg) 

250 return ctDict[physicalFilter] 

251 except ColortermNotFoundError: 

252 if doRaise: 

253 raise 

254 else: 

255 return Colorterm(physicalFilter, physicalFilter)