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 

41from lsst.meas.algorithms import ReferenceObjectLoader 

42 

43import lsst.geom 

44 

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

46 

47 

48class FgcmLoadReferenceCatalogConfig(pexConfig.Config): 

49 """Config for FgcmLoadReferenceCatalogTask""" 

50 

51 refObjLoader = pexConfig.ConfigurableField( 

52 target=LoadIndexedReferenceObjectsTask, 

53 doc="Reference object loader for photometry", 

54 ) 

55 refFilterMap = pexConfig.DictField( 

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

57 keytype=str, 

58 itemtype=str, 

59 default={}, 

60 deprecated=("This field is no longer used, and has been deprecated by " 

61 "DM-28088. It will be removed after v22. Use " 

62 "filterMap instead.") 

63 ) 

64 filterMap = pexConfig.DictField( 

65 doc="Mapping from physicalFilter label to reference filter name.", 

66 keytype=str, 

67 itemtype=str, 

68 default={}, 

69 ) 

70 applyColorTerms = pexConfig.Field( 

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

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

73 dtype=bool, 

74 default=True 

75 ) 

76 colorterms = pexConfig.ConfigField( 

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

78 dtype=ColortermLibrary, 

79 ) 

80 referenceSelector = pexConfig.ConfigurableField( 

81 target=ReferenceSourceSelectorTask, 

82 doc="Selection of reference sources", 

83 ) 

84 

85 def validate(self): 

86 super().validate() 

87 if not self.filterMap: 

88 msg = 'Must set filterMap' 

89 raise pexConfig.FieldValidationError(FgcmLoadReferenceCatalogConfig.filterMap, self, msg) 

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

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

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

93 

94 

95class FgcmLoadReferenceCatalogTask(pipeBase.Task): 

96 """ 

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

98 

99 Parameters 

100 ---------- 

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

102 Data butler for reading catalogs 

103 """ 

104 ConfigClass = FgcmLoadReferenceCatalogConfig 

105 _DefaultName = 'fgcmLoadReferenceCatalog' 

106 

107 def __init__(self, butler=None, refObjLoader=None, **kwargs): 

108 """Construct an FgcmLoadReferenceCatalogTask 

109 

110 Parameters 

111 ---------- 

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

113 Data butler for reading catalogs. 

114 """ 

115 pipeBase.Task.__init__(self, **kwargs) 

116 if refObjLoader is None and butler is not None: 

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

118 else: 

119 self.refObjLoader = refObjLoader 

120 

121 self.makeSubtask('referenceSelector') 

122 self._fluxFilters = None 

123 self._fluxFields = None 

124 self._referenceFilter = None 

125 

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

127 """ 

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

129 filters. In addition, apply colorterms if available. 

130 

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

132 

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

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

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

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

137 

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

139 

140 Parameters 

141 ---------- 

142 nside: `int` 

143 Healpix nside of pixel to load 

144 pixel: `int` 

145 Healpix pixel of pixel to load 

146 filterList: `list` 

147 list of `str` of camera filter names. 

148 nest: `bool`, optional 

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

150 

151 Returns 

152 ------- 

153 fgcmRefCat: `np.recarray` 

154 """ 

155 

156 # Determine the size of the sky circle to load 

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

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

159 

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

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

162 

163 radius = 0.0 * lsst.geom.radians 

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

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

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

167 if (rad > radius): 

168 radius = rad 

169 

170 # Load the fgcm-format reference catalog 

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

172 center.getDec().asDegrees(), 

173 radius.asDegrees(), 

174 filterList) 

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

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

177 

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

179 

180 return fgcmRefCat[inPix] 

181 

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

183 """ 

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

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

186 

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

188 

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

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

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

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

193 

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

195 

196 Parameters 

197 ---------- 

198 ra: `float` 

199 ICRS right ascension, degrees. 

200 dec: `float` 

201 ICRS declination, degrees. 

202 radius: `float` 

203 Radius to search, degrees. 

204 filterList: `list` 

205 list of `str` of camera filter names. 

206 

207 Returns 

208 ------- 

209 fgcmRefCat: `np.recarray` 

210 """ 

211 

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

213 

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

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

216 self._determine_flux_fields(center, filterList) 

217 

218 skyCircle = self.refObjLoader.loadSkyCircle(center, 

219 radius * lsst.geom.degrees, 

220 self._referenceFilter) 

221 

222 if not skyCircle.refCat.isContiguous(): 

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

224 else: 

225 refCat = skyCircle.refCat 

226 

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

228 goodSources = self.referenceSelector.selectSources(refCat) 

229 selected = goodSources.selected 

230 

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

232 ('dec', 'f8'), 

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

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

235 if fgcmRefCat.size == 0: 

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

237 return fgcmRefCat 

238 

239 # The ra/dec native Angle format is radians 

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

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

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

243 # be approximately 600x slower. 

244 

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

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

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

248 

249 # Default (unset) values are 99.0 

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

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

252 

253 if self.config.applyColorTerms: 

254 if isinstance(self.refObjLoader, ReferenceObjectLoader): 

255 # Gen3 

256 refCatName = self.refObjLoader.config.value.ref_dataset_name 

257 else: 

258 # Gen2 

259 refCatName = self.refObjLoader.ref_dataset_name 

260 

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

262 if fluxField is None: 

263 continue 

264 

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

266 

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

268 

269 refMag, refMagErr = colorterm.getCorrectedMagnitudes(refCat) 

270 

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

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

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

274 

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

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

277 & (np.nan_to_num(refMagErr[selected]) > 0.0)) 

278 

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

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

281 

282 else: 

283 # No colorterms 

284 

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

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

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

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

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

290 refMag = (refCat[fluxField][selected][good] * units.nJy).to_value(units.ABmag) 

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

292 refCat[fluxField][selected][good]) 

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

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

295 

296 return fgcmRefCat 

297 

298 def _determine_flux_fields(self, center, filterList): 

299 """ 

300 Determine the flux field names for a reference catalog. 

301 

302 Will set self._fluxFields, self._referenceFilter. 

303 

304 Parameters 

305 ---------- 

306 center: `lsst.geom.SpherePoint` 

307 The center around which to load test sources. 

308 filterList: `list` 

309 list of `str` of camera filter names. 

310 """ 

311 

312 # Record self._fluxFilters for checks on subsequent calls 

313 self._fluxFilters = filterList 

314 

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

316 # via the refObjLoader task which requires a valid filterName 

317 foundReferenceFilter = False 

318 for filterName in filterList: 

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

320 if refFilterName is None: 

321 continue 

322 

323 try: 

324 results = self.refObjLoader.loadSkyCircle(center, 

325 0.05 * lsst.geom.degrees, 

326 refFilterName) 

327 foundReferenceFilter = True 

328 self._referenceFilter = refFilterName 

329 break 

330 except RuntimeError: 

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

332 # in the reference catalog. This is okay. 

333 pass 

334 

335 if not foundReferenceFilter: 

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

337 (", ".join(filterList))) 

338 

339 # Retrieve all the fluxField names 

340 self._fluxFields = [] 

341 for filterName in filterList: 

342 fluxField = None 

343 

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

345 

346 if refFilterName is not None: 

347 try: 

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

349 except RuntimeError: 

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

351 fluxField = None 

352 

353 if fluxField is None: 

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

355 

356 self._fluxFields.append(fluxField)