Coverage for python/lsst/fgcmcal/fgcmLoadReferenceCatalog.py: 19%

116 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-24 02:58 -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. 

24 

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""" 

30 

31import numpy as np 

32import healpy as hp 

33from astropy import units 

34 

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 

41 

42import lsst.geom 

43 

44__all__ = ['FgcmLoadReferenceCatalogConfig', 'FgcmLoadReferenceCatalogTask'] 

45 

46 

47class FgcmLoadReferenceCatalogConfig(pexConfig.Config): 

48 """Config for FgcmLoadReferenceCatalogTask""" 

49 

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 ) 

75 

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) 

84 

85 

86class FgcmLoadReferenceCatalogTask(pipeBase.Task): 

87 """ 

88 Load multi-band reference objects from a reference catalog. 

89 

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' 

99 

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 

106 

107 if refObjLoader is None or refCatName is None: 

108 raise RuntimeError("FgcmLoadReferenceCatalogTask requires a refObjLoader and refCatName.") 

109 

110 self.makeSubtask('referenceSelector') 

111 self._fluxFilters = None 

112 self._fluxFields = None 

113 self._referenceFilter = None 

114 

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. 

119 

120 Return format is a numpy recarray for use with fgcm, with the format: 

121 

122 dtype = ([('ra', `np.float64`), 

123 ('dec', `np.float64`), 

124 ('refMag', `np.float32`, len(filterList)), 

125 ('refMagErr', `np.float32`, len(filterList)]) 

126 

127 Reference magnitudes (AB) will be 99 for non-detections. 

128 

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. 

139 

140 Returns 

141 ------- 

142 fgcmRefCat: `np.recarray` 

143 """ 

144 

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) 

148 

149 corners = hp.boundaries(nside, pixel, step=1, nest=nest) 

150 theta_phi = hp.vec2ang(np.transpose(corners)) 

151 

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 

158 

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) 

166 

167 inPix, = np.where(catPix == pixel) 

168 

169 return fgcmRefCat[inPix] 

170 

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. 

175 

176 Return format is a numpy recarray for use with fgcm. 

177 

178 dtype = ([('ra', `np.float64`), 

179 ('dec', `np.float64`), 

180 ('refMag', `np.float32`, len(filterList)), 

181 ('refMagErr', `np.float32`, len(filterList)]) 

182 

183 Reference magnitudes (AB) will be 99 for non-detections. 

184 

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. 

195 

196 Returns 

197 ------- 

198 fgcmRefCat: `np.recarray` 

199 """ 

200 

201 center = lsst.geom.SpherePoint(ra * lsst.geom.degrees, dec * lsst.geom.degrees) 

202 

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) 

206 

207 skyCircle = self.refObjLoader.loadSkyCircle(center, 

208 radius * lsst.geom.degrees, 

209 self._referenceFilter) 

210 

211 if not skyCircle.refCat.isContiguous(): 

212 refCat = skyCircle.refCat.copy(deep=True) 

213 else: 

214 refCat = skyCircle.refCat 

215 

216 # Select on raw (uncorrected) catalog, where the errors should make more sense 

217 goodSources = self.referenceSelector.selectSources(refCat) 

218 selected = goodSources.selected 

219 

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 

227 

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. 

233 

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 

237 

238 # Default (unset) values are 99.0 

239 fgcmRefCat['refMag'][:, :] = 99.0 

240 fgcmRefCat['refMagErr'][:, :] = 99.0 

241 

242 if self.config.applyColorTerms: 

243 for i, (filterName, fluxField) in enumerate(zip(self._fluxFilters, self._fluxFields)): 

244 if fluxField is None: 

245 continue 

246 

247 self.log.debug("Applying color terms for filtername=%r" % (filterName)) 

248 

249 colorterm = self.config.colorterms.getColorterm(filterName, self.refCatName, doRaise=True) 

250 

251 refMag, refMagErr = colorterm.getCorrectedMagnitudes(refCat) 

252 

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. 

256 

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)) 

260 

261 fgcmRefCat['refMag'][good, i] = refMag[selected][good] 

262 fgcmRefCat['refMagErr'][good, i] = refMagErr[selected][good] 

263 

264 else: 

265 # No colorterms 

266 

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 

277 

278 return fgcmRefCat 

279 

280 def _determine_flux_fields(self, center, filterList): 

281 """ 

282 Determine the flux field names for a reference catalog. 

283 

284 Will set self._fluxFields, self._referenceFilter. 

285 

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 """ 

293 

294 # Record self._fluxFilters for checks on subsequent calls 

295 self._fluxFilters = filterList 

296 

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 

304 

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 

316 

317 if not foundReferenceFilter: 

318 raise RuntimeError("Could not find any valid flux field(s) %s" % 

319 (", ".join(filterList))) 

320 

321 # Retrieve all the fluxField names 

322 self._fluxFields = [] 

323 for filterName in filterList: 

324 fluxField = None 

325 

326 refFilterName = self.config.filterMap.get(filterName) 

327 

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 

334 

335 if fluxField is None: 

336 self.log.warning(f'No reference flux field for camera filter {filterName}') 

337 

338 self._fluxFields.append(fluxField)