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

115 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-01 03:28 -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 hpgeom as hpg 

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 lon, lat = hpg.pixel_to_angle(nside, pixel, nest=nest, degrees=False) 

147 center = lsst.geom.SpherePoint(lon * lsst.geom.degrees, lat * lsst.geom.radians) 

148 

149 theta_phi = hpg.boundaries(nside, pixel, step=1, nest=nest, lonlat=False) 

150 

151 radius = 0.0 * lsst.geom.radians 

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

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

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

155 if (rad > radius): 

156 radius = rad 

157 

158 # Load the fgcm-format reference catalog 

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

160 center.getDec().asDegrees(), 

161 radius.asDegrees(), 

162 filterList) 

163 catPix = hpg.angle_to_pixel(nside, fgcmRefCat['ra'], fgcmRefCat['dec'], nest=nest) 

164 

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

166 

167 return fgcmRefCat[inPix] 

168 

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

170 """ 

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

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

173 

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

175 

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

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

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

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

180 

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

182 

183 Parameters 

184 ---------- 

185 ra: `float` 

186 ICRS right ascension, degrees. 

187 dec: `float` 

188 ICRS declination, degrees. 

189 radius: `float` 

190 Radius to search, degrees. 

191 filterList: `list` 

192 list of `str` of camera filter names. 

193 

194 Returns 

195 ------- 

196 fgcmRefCat: `np.recarray` 

197 """ 

198 

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

200 

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

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

203 self._determine_flux_fields(center, filterList) 

204 

205 skyCircle = self.refObjLoader.loadSkyCircle(center, 

206 radius * lsst.geom.degrees, 

207 self._referenceFilter) 

208 

209 if not skyCircle.refCat.isContiguous(): 

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

211 else: 

212 refCat = skyCircle.refCat 

213 

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

215 goodSources = self.referenceSelector.selectSources(refCat) 

216 selected = goodSources.selected 

217 

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

219 ('dec', 'f8'), 

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

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

222 if fgcmRefCat.size == 0: 

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

224 return fgcmRefCat 

225 

226 # The ra/dec native Angle format is radians 

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

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

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

230 # be approximately 600x slower. 

231 

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

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

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

235 

236 # Default (unset) values are 99.0 

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

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

239 

240 if self.config.applyColorTerms: 

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

242 if fluxField is None: 

243 continue 

244 

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

246 

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

248 

249 refMag, refMagErr = colorterm.getCorrectedMagnitudes(refCat) 

250 

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

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

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

254 

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

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

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

258 

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

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

261 

262 else: 

263 # No colorterms 

264 

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

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

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

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

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

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

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

272 refCat[fluxField][selected][good]) 

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

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

275 

276 return fgcmRefCat 

277 

278 def _determine_flux_fields(self, center, filterList): 

279 """ 

280 Determine the flux field names for a reference catalog. 

281 

282 Will set self._fluxFields, self._referenceFilter. 

283 

284 Parameters 

285 ---------- 

286 center: `lsst.geom.SpherePoint` 

287 The center around which to load test sources. 

288 filterList: `list` 

289 list of `str` of camera filter names. 

290 """ 

291 

292 # Record self._fluxFilters for checks on subsequent calls 

293 self._fluxFilters = filterList 

294 

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

296 # via the refObjLoader task which requires a valid filterName 

297 foundReferenceFilter = False 

298 for filterName in filterList: 

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

300 if refFilterName is None: 

301 continue 

302 

303 try: 

304 results = self.refObjLoader.loadSkyCircle(center, 

305 0.05 * lsst.geom.degrees, 

306 refFilterName) 

307 foundReferenceFilter = True 

308 self._referenceFilter = refFilterName 

309 break 

310 except RuntimeError: 

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

312 # in the reference catalog. This is okay. 

313 pass 

314 

315 if not foundReferenceFilter: 

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

317 (", ".join(filterList))) 

318 

319 # Retrieve all the fluxField names 

320 self._fluxFields = [] 

321 for filterName in filterList: 

322 fluxField = None 

323 

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

325 

326 if refFilterName is not None: 

327 try: 

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

329 except RuntimeError: 

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

331 fluxField = None 

332 

333 if fluxField is None: 

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

335 

336 self._fluxFields.append(fluxField)