Hide keyboard shortcuts

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

41import lsst.geom 

42 

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

44 

45 

46class FgcmLoadReferenceCatalogConfig(pexConfig.Config): 

47 """Config for FgcmLoadReferenceCatalogTask""" 

48 

49 refObjLoader = pexConfig.ConfigurableField( 

50 target=LoadIndexedReferenceObjectsTask, 

51 doc="Reference object loader for photometry", 

52 ) 

53 refFilterMap = pexConfig.DictField( 

54 doc="Mapping from camera 'filterName' to reference filter name.", 

55 keytype=str, 

56 itemtype=str, 

57 default={}, 

58 ) 

59 applyColorTerms = pexConfig.Field( 

60 doc=("Apply photometric color terms to reference stars?" 

61 "Requires that colorterms be set to a ColorTermLibrary"), 

62 dtype=bool, 

63 default=True 

64 ) 

65 colorterms = pexConfig.ConfigField( 

66 doc="Library of photometric reference catalog name to color term dict.", 

67 dtype=ColortermLibrary, 

68 ) 

69 referenceSelector = pexConfig.ConfigurableField( 

70 target=ReferenceSourceSelectorTask, 

71 doc="Selection of reference sources", 

72 ) 

73 

74 def validate(self): 

75 super().validate() 

76 if not self.refFilterMap: 

77 msg = 'Must set refFilterMap' 

78 raise pexConfig.FieldValidationError(FgcmLoadReferenceCatalogConfig.refFilterMap, self, msg) 

79 if self.applyColorTerms and len(self.colorterms.data) == 0: 

80 msg = "applyColorTerms=True requires the `colorterms` field be set to a ColortermLibrary." 

81 raise pexConfig.FieldValidationError(FgcmLoadReferenceCatalogConfig.colorterms, self, msg) 

82 

83 

84class FgcmLoadReferenceCatalogTask(pipeBase.Task): 

85 """ 

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

87 

88 Parameters 

89 ---------- 

90 butler: `lsst.daf.persistence.Butler` 

91 Data butler for reading catalogs 

92 """ 

93 ConfigClass = FgcmLoadReferenceCatalogConfig 

94 _DefaultName = 'fgcmLoadReferenceCatalog' 

95 

96 def __init__(self, butler, *args, **kwargs): 

97 """Construct an FgcmLoadReferenceCatalogTask 

98 

99 Parameters 

100 ---------- 

101 butler: `lsst.daf.persistence.Buter` 

102 Data butler for reading catalogs. 

103 """ 

104 pipeBase.Task.__init__(self, *args, **kwargs) 

105 self.butler = butler 

106 self.makeSubtask('refObjLoader', butler=butler) 

107 self.makeSubtask('referenceSelector') 

108 self._fluxFilters = None 

109 self._fluxFields = None 

110 self._referenceFilter = None 

111 

112 def getFgcmReferenceStarsHealpix(self, nside, pixel, filterList, nest=False): 

113 """ 

114 Get a reference catalog that overlaps a healpix pixel, using multiple 

115 filters. In addition, apply colorterms if available. 

116 

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

118 

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

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

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

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

123 

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

125 

126 Parameters 

127 ---------- 

128 nside: `int` 

129 Healpix nside of pixel to load 

130 pixel: `int` 

131 Healpix pixel of pixel to load 

132 filterList: `list` 

133 list of `str` of camera filter names. 

134 nest: `bool`, optional 

135 Is the pixel in nest format? Default is False. 

136 

137 Returns 

138 ------- 

139 fgcmRefCat: `np.recarray` 

140 """ 

141 

142 # Determine the size of the sky circle to load 

143 theta, phi = hp.pix2ang(nside, pixel, nest=nest) 

144 center = lsst.geom.SpherePoint(phi * lsst.geom.radians, (np.pi/2. - theta) * lsst.geom.radians) 

145 

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

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

148 

149 radius = 0.0 * lsst.geom.radians 

150 for ctheta, cphi in zip(*theta_phi): 

151 rad = center.separation(lsst.geom.SpherePoint(cphi * lsst.geom.radians, 

152 (np.pi/2. - ctheta) * lsst.geom.radians)) 

153 if (rad > radius): 

154 radius = rad 

155 

156 # Load the fgcm-format reference catalog 

157 fgcmRefCat = self.getFgcmReferenceStarsSkyCircle(center.getRa().asDegrees(), 

158 center.getDec().asDegrees(), 

159 radius.asDegrees(), 

160 filterList) 

161 catPix = hp.ang2pix(nside, np.radians(90.0 - fgcmRefCat['dec']), 

162 np.radians(fgcmRefCat['ra']), nest=nest) 

163 

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

165 

166 return fgcmRefCat[inPix] 

167 

168 def getFgcmReferenceStarsSkyCircle(self, ra, dec, radius, filterList): 

169 """ 

170 Get a reference catalog that overlaps a circular sky region, using 

171 multiple filters. In addition, apply colorterms if available. 

172 

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

174 

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

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

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

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

179 

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

181 

182 Parameters 

183 ---------- 

184 ra: `float` 

185 ICRS right ascension, degrees. 

186 dec: `float` 

187 ICRS declination, degrees. 

188 radius: `float` 

189 Radius to search, degrees. 

190 filterList: `list` 

191 list of `str` of camera filter names. 

192 

193 Returns 

194 ------- 

195 fgcmRefCat: `np.recarray` 

196 """ 

197 

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

199 

200 # Check if we haev previously cached values for the fluxFields 

201 if self._fluxFilters is None or self._fluxFilters != filterList: 

202 self._determine_flux_fields(center, filterList) 

203 

204 skyCircle = self.refObjLoader.loadSkyCircle(center, 

205 radius * lsst.geom.degrees, 

206 self._referenceFilter) 

207 

208 if not skyCircle.refCat.isContiguous(): 

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

210 else: 

211 refCat = skyCircle.refCat 

212 

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

214 goodSources = self.referenceSelector.selectSources(refCat) 

215 selected = goodSources.selected 

216 

217 fgcmRefCat = np.zeros(np.sum(selected), dtype=[('ra', 'f8'), 

218 ('dec', 'f8'), 

219 ('refMag', 'f4', len(filterList)), 

220 ('refMagErr', 'f4', len(filterList))]) 

221 if fgcmRefCat.size == 0: 

222 # Return an empty catalog if we don't have any selected sources 

223 return fgcmRefCat 

224 

225 # The ra/dec native Angle format is radians 

226 # We determine the conversion from the native units (typically 

227 # radians) to degrees for the first observation. This allows us 

228 # to treate ra/dec as numpy arrays rather than Angles, which would 

229 # be approximately 600x slower. 

230 

231 conv = refCat[0]['coord_ra'].asDegrees() / float(refCat[0]['coord_ra']) 

232 fgcmRefCat['ra'] = refCat['coord_ra'][selected] * conv 

233 fgcmRefCat['dec'] = refCat['coord_dec'][selected] * conv 

234 

235 # Default (unset) values are 99.0 

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

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

238 

239 if self.config.applyColorTerms: 

240 try: 

241 refCatName = self.refObjLoader.ref_dataset_name 

242 except AttributeError: 

243 # NOTE: we need this try:except: block in place until we've 

244 # completely removed a.net support 

245 raise RuntimeError("Cannot perform colorterm corrections with a.net refcats.") 

246 

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

248 if fluxField is None: 

249 continue 

250 

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

252 

253 colorterm = self.config.colorterms.getColorterm( 

254 filterName=filterName, photoCatName=refCatName, doRaise=True) 

255 

256 refMag, refMagErr = colorterm.getCorrectedMagnitudes(refCat, filterName) 

257 

258 # nan_to_num replaces nans with zeros, and this ensures that we select 

259 # magnitudes that both filter out nans and are not very large (corresponding 

260 # to very small fluxes), as "99" is a common sentinel for illegal magnitudes. 

261 

262 good, = np.where((np.nan_to_num(refMag[selected]) < 90.0) & 

263 (np.nan_to_num(refMagErr[selected]) < 90.0) & 

264 (np.nan_to_num(refMagErr[selected]) > 0.0)) 

265 

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

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

268 

269 else: 

270 # No colorterms 

271 

272 # TODO: need to use Jy here until RFC-549 is completed and refcats return nanojansky 

273 

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

275 # nan_to_num replaces nans with zeros, and this ensures that we select 

276 # fluxes that both filter out nans and are positive. 

277 good, = np.where((np.nan_to_num(refCat[fluxField][selected]) > 0.0) & 

278 (np.nan_to_num(refCat[fluxField+'Err'][selected]) > 0.0)) 

279 refMag = (refCat[fluxField][selected][good] * units.Jy).to_value(units.ABmag) 

280 refMagErr = abMagErrFromFluxErr(refCat[fluxField+'Err'][selected][good], 

281 refCat[fluxField][selected][good]) 

282 fgcmRefCat['refMag'][good, i] = refMag 

283 fgcmRefCat['refMagErr'][good, i] = refMagErr 

284 

285 return fgcmRefCat 

286 

287 def _determine_flux_fields(self, center, filterList): 

288 """ 

289 Determine the flux field names for a reference catalog. 

290 

291 Will set self._fluxFields, self._referenceFilter. 

292 

293 Parameters 

294 ---------- 

295 center: `lsst.geom.SpherePoint` 

296 The center around which to load test sources. 

297 filterList: `list` 

298 list of `str` of camera filter names. 

299 """ 

300 

301 # Record self._fluxFilters for checks on subsequent calls 

302 self._fluxFilters = filterList 

303 

304 # Search for a good filter to use to load the reference catalog 

305 # via the refObjLoader task which requires a valid filterName 

306 foundReferenceFilter = False 

307 for filterName in filterList: 

308 refFilterName = self.config.refFilterMap.get(filterName) 

309 if refFilterName is None: 

310 continue 

311 

312 try: 

313 results = self.refObjLoader.loadSkyCircle(center, 

314 0.05 * lsst.geom.degrees, 

315 refFilterName) 

316 foundReferenceFilter = True 

317 self._referenceFilter = refFilterName 

318 break 

319 except RuntimeError: 

320 # This just means that the filterName wasn't listed 

321 # in the reference catalog. This is okay. 

322 pass 

323 

324 if not foundReferenceFilter: 

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

326 (", ".join(filterList))) 

327 

328 # Retrieve all the fluxField names 

329 self._fluxFields = [] 

330 for filterName in filterList: 

331 fluxField = None 

332 

333 refFilterName = self.config.refFilterMap.get(filterName) 

334 

335 if refFilterName is not None: 

336 try: 

337 fluxField = getRefFluxField(results.refCat.schema, filterName=refFilterName) 

338 except RuntimeError: 

339 # This flux field isn't available. Set to None 

340 fluxField = None 

341 

342 if fluxField is None: 

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

344 

345 self._fluxFields.append(fluxField)