Coverage for python/lsst/summit/utils/astrometry/utils.py: 14%

117 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-04 03:40 -0800

1# This file is part of summit_utils. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22import numpy as np 

23 

24import astropy.units as u 

25from astropy.time import Time 

26from astropy.coordinates import AltAz, EarthLocation, SkyCoord 

27 

28from lsst.afw.geom import SkyWcs 

29from lsst.daf.base import PropertySet 

30from lsst.pipe.tasks.characterizeImage import CharacterizeImageTask, CharacterizeImageConfig 

31 

32__all__ = [ 

33 'claverHeaderToWcs', 

34 'getAverageRaFromHeader', 

35 'getAverageDecFromHeader', 

36 'getAverageAzFromHeader', 

37 'getAverageElFromHeader', 

38 'genericCameraHeaderToWcs', 

39 'getIcrsAtZenith', 

40 'headerToWcs', 

41 'runCharactierizeImage', 

42 'filterSourceCatOnBrightest', 

43] 

44 

45 

46def claverHeaderToWcs(exp, nominalRa=None, nominalDec=None): 

47 """Given an exposure taken by Chuck Claver at his house, construct a wcs 

48 with the ra/dec set to zenith unless a better guess is supplied. 

49 

50 Automatically sets the platescale depending on the lens. 

51 

52 Parameters 

53 ---------- 

54 exp : `lsst.afw.image.Exposure` 

55 The exposure to construct the wcs for. 

56 nominalRa : `float`, optional 

57 The guess for the ra. 

58 nominalDec : `float`, optional 

59 The guess for the Dec. 

60 

61 Returns 

62 ------- 

63 wcs : `lsst.afw.geom.SkyWcs` 

64 The constructed wcs. 

65 """ 

66 header = exp.getMetadata().toDict() 

67 

68 # set the plate scale depending on the lens and put into CD matrix 

69 # plate scale info from: 

70 # https://confluence.lsstcorp.org/pages/viewpage.action?pageId=191987725 

71 lens = header['INSTLEN'] 

72 if '135mm' in lens: 

73 arcSecPerPix = 8.64 

74 elif '375mm' in lens: 

75 arcSecPerPix = 3.11 

76 elif '750mm' in lens: 

77 arcSecPerPix = 1.56 

78 else: 

79 raise ValueError(f'Unrecognised lens: {lens}') 

80 

81 header['CD1_1'] = arcSecPerPix / 3600 

82 header['CD1_2'] = 0 

83 header['CD2_1'] = 0 

84 header['CD2_2'] = arcSecPerPix / 3600 

85 

86 # calculate the ra/dec at zenith and assume Chuck pointed it vertically 

87 icrs = getIcrsAtZenith(float(header['OBSLON']), 

88 float(header['OBSLAT']), 

89 float(header['OBSHGT']), 

90 header['UTC']) 

91 header['CRVAL1'] = nominalRa if nominalRa else icrs.ra.degree 

92 header['CRVAL2'] = nominalDec if nominalDec else icrs.dec.degree 

93 

94 # just use the nomimal chip centre, not that it matters 

95 # given radec = zenith 

96 width, height = exp.image.array.shape 

97 header['CRPIX1'] = width/2 

98 header['CRPIX2'] = height/2 

99 

100 header['CTYPE1'] = 'RA---TAN-SIP' 

101 header['CTYPE2'] = 'DEC--TAN-SIP' 

102 

103 wcsPropSet = PropertySet.from_mapping(header) 

104 wcs = SkyWcs(wcsPropSet) 

105 return wcs 

106 

107 

108# don't be tempted to get cute and try to combine these 4 functions. It would 

109# be easy to do but it's not unlikley they will diverge in the future. 

110def getAverageRaFromHeader(header): 

111 raStart = header.get('RASTART') 

112 raEnd = header.get('RAEND') 

113 if not raStart or not raEnd: 

114 raise RuntimeError(f'Failed to get RA from header due to missing RASTART/END {raStart} {raEnd}') 

115 raStart = float(raStart) 

116 raEnd = float(raEnd) 

117 return (raStart + raEnd) / 2 

118 

119 

120def getAverageDecFromHeader(header): 

121 decStart = header.get('DECSTART') 

122 decEnd = header.get('DECEND') 

123 if not decStart or not decEnd: 

124 raise RuntimeError(f'Failed to get DEC from header due to missing DECSTART/END {decStart} {decEnd}') 

125 decStart = float(decStart) 

126 decEnd = float(decEnd) 

127 return (decStart + decEnd) / 2 

128 

129 

130def getAverageAzFromHeader(header): 

131 azStart = header.get('AZSTART') 

132 azEnd = header.get('AZEND') 

133 if not azStart or not azEnd: 

134 raise RuntimeError(f'Failed to get az from header due to missing AZSTART/END {azStart} {azEnd}') 

135 azStart = float(azStart) 

136 azEnd = float(azEnd) 

137 return (azStart + azEnd) / 2 

138 

139 

140def getAverageElFromHeader(header): 

141 elStart = header.get('ELSTART') 

142 elEnd = header.get('ELEND') 

143 if not elStart or not elEnd: 

144 raise RuntimeError(f'Failed to get el from header due to missing ELSTART/END {elStart} {elEnd}') 

145 elStart = float(elStart) 

146 elEnd = float(elEnd) 

147 return (elStart + elEnd) / 2 

148 

149 

150def genericCameraHeaderToWcs(exp): 

151 header = exp.getMetadata().toDict() 

152 width, height = exp.image.array.shape 

153 header['CRPIX1'] = width/2 

154 header['CRPIX2'] = height/2 

155 

156 header['CTYPE1'] = 'RA---TAN-SIP' 

157 header['CTYPE2'] = 'DEC--TAN-SIP' 

158 

159 header['CRVAL1'] = getAverageRaFromHeader(header) 

160 header['CRVAL2'] = getAverageDecFromHeader(header) 

161 

162 plateScale = header.get('SECPIX') 

163 if not plateScale: 

164 raise RuntimeError('Failed to find platescale in header') 

165 plateScale = float(plateScale) 

166 

167 header['CD1_1'] = plateScale / 3600 

168 header['CD1_2'] = 0 

169 header['CD2_1'] = 0 

170 header['CD2_2'] = plateScale / 3600 

171 

172 wcsPropSet = PropertySet.from_mapping(header) 

173 wcs = SkyWcs(wcsPropSet) 

174 return wcs 

175 

176 

177def getIcrsAtZenith(lon, lat, height, utc): 

178 """Get the icrs at zenith given a lat/long/height/time in UTC. 

179 

180 Parameters 

181 ---------- 

182 lon : `float` 

183 The longitude, in degrees. 

184 lat : `float` 

185 The latitude, in degrees. 

186 height : `float` 

187 The height above sea level in meters. 

188 utc : `str` 

189 The time in UTC as an ISO string, e.g. '2022-05-27 20:41:02' 

190 

191 Returns 

192 ------- 

193 skyCoordAtZenith : `astropy.coordinates.SkyCoord` 

194 The skyCoord at zenith. 

195 """ 

196 location = EarthLocation.from_geodetic(lon=lon*u.deg, 

197 lat=lat*u.deg, 

198 height=height) 

199 obsTime = Time(utc, format='iso', scale='utc') 

200 skyCoord = SkyCoord(AltAz(obstime=obsTime, 

201 alt=90.0*u.deg, 

202 az=180.0*u.deg, 

203 location=location)) 

204 return skyCoord.transform_to('icrs') 

205 

206 

207def headerToWcs(header): 

208 """Convert an astrometry.net wcs header dict to a DM wcs object. 

209 

210 Parameters 

211 ---------- 

212 header : `dict` 

213 The wcs header, as returned from from the astrometry_net fit. 

214 

215 Returns 

216 ------- 

217 wcs : `lsst.afw.geom.SkyWcs` 

218 The wcs. 

219 """ 

220 wcsPropSet = PropertySet.from_mapping(header) 

221 return SkyWcs(wcsPropSet) 

222 

223 

224def runCharactierizeImage(exp, snr, minPix): 

225 """Run the image characterization task, finding only bright sources. 

226 

227 Parameters 

228 ---------- 

229 exp : `lsst.afw.image.Exposure` 

230 The exposure to characterize. 

231 snr : `float` 

232 The SNR threshold for detection. 

233 minPix : `int` 

234 The minimum number of pixels to count as a source. 

235 

236 Returns 

237 ------- 

238 result : `lsst.pipe.base.Struct` 

239 The result from the image characterization task. 

240 """ 

241 charConfig = CharacterizeImageConfig() 

242 charConfig.doMeasurePsf = False 

243 charConfig.doApCorr = False 

244 charConfig.doDeblend = False 

245 charConfig.repair.doCosmicRay = False 

246 charConfig.repair.doInterpolate = True 

247 charConfig.detection.minPixels = minPix 

248 charConfig.detection.thresholdValue = snr 

249 

250 charTask = CharacterizeImageTask(config=charConfig) 

251 

252 charResult = charTask.run(exp) 

253 return charResult 

254 

255 

256def filterSourceCatOnBrightest(catalog, brightFraction, minSources=15, 

257 flux_field="base_CircularApertureFlux_3_0_instFlux"): 

258 """Filter a sourceCat on the brightness, leaving only the top fraction. 

259 

260 Return a catalog containing the brightest sources in the input. Makes an 

261 initial coarse cut, keeping those above 0.1% of the maximum finite flux, 

262 and then returning the specified fraction of the remaining sources, 

263 or minSources, whichever is greater. 

264 

265 Parameters 

266 ---------- 

267 catalog : `lsst.afw.table.SourceCatalog` 

268 Catalog to be filtered. 

269 brightFraction : `float` 

270 Return this fraction of the brightest sources. 

271 minSources : `int`, optional 

272 Always return at least this many sources. 

273 flux_field : `str`, optional 

274 Name of flux field to filter on. 

275 

276 Returns 

277 ------- 

278 result : `lsst.afw.table.SourceCatalog` 

279 Brightest sources in the input image, in ascending order of brightness. 

280 """ 

281 assert minSources > 0 

282 assert brightFraction > 0 and brightFraction <= 1 

283 maxFlux = np.nanmax(catalog[flux_field]) 

284 result = catalog.subset(catalog[flux_field] > maxFlux * 0.001) 

285 

286 print(f"Catalog had {len(catalog)} sources, of which {len(result)} were above 0.1% of max") 

287 

288 item = catalog.schema.find(flux_field) 

289 result = catalog.copy(deep=True) # sort() is in place; copy so we don't modify the original 

290 result.sort(item.key) 

291 result = result.copy(deep=True) # make it memory contiguous 

292 end = int(np.ceil(len(result)*brightFraction)) 

293 return result[-max(end, minSources):]